TypeScript
A minimal MCP tool with a guard
You want an MCP server that exposes one read-only tool plus one destructive variant an agent cannot trip into by accident. Zod schema is the fence.
12 Jun 2026
Read-only by default is the house rule. The one tool that changes state demands an explicit confirm: "yes" in the schema, so a model cannot stumble into it.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
type Content = { content: { type: "text"; text: string }[]; isError?: boolean };
const textResult = (text: string): Content => ({ content: [{ type: "text", text }] });
const jsonResult = (data: unknown): Content => textResult(JSON.stringify(data));
const errorResult = (text: string): Content => ({ ...textResult(text), isError: true });
const appName = z
.string()
.min(1)
.max(63)
.regex(/^[a-z0-9-]+$/, "lowercase letters, digits, hyphens only");
const server = new McpServer({ name: "ops", version: "1.0.0" });
// Read-only: safe to call any time.
server.tool(
"get_app_status",
"Read the deploy status of one app. Read-only; never changes anything.",
{ app: appName },
async ({ app }) => {
const res = await fetch(`https://ops.internal/apps/${app}/status`);
if (!res.ok) return errorResult(`lookup failed: ${res.status}`);
return jsonResult(await res.json());
},
);
// Destructive: guarded by a literal the model must supply on purpose.
server.tool(
"restart_app",
"Restart one app. Destructive; requires confirm:\"yes\".",
{ app: appName, confirm: z.literal("yes") },
async ({ app }) => {
const res = await fetch(`https://ops.internal/apps/${app}/restart`, {
method: "POST",
});
if (!res.ok) return errorResult(`restart failed: ${res.status}`);
return textResult(`restarted ${app}`);
},
);
await server.connect(new StdioServerTransport());Gotchas
confirm: z.literal("yes") is not cosmetic. The SDK rejects the call before your handler runs unless that exact value is present, so a model that fires restart_app on a hunch gets a schema error, not a restarted production app. Keep every state-changing tool behind one, and leave reads unguarded so the agent can look around freely.
- Giving agents real handsA fleet of thirteen small servers that give agents real hands. What they wrap, the two house styles I build them in, and why they all start read-only.Musing
- Wrap a CLI as an MCP tool when there is no APIThe thing you want an agent to drive only has a CLI. execFile it, hand back stdout, and put bounds on the call so it cannot eat your process.Snippet
- mcp-dokkuAn MCP server that drives a Dokku PaaS over SSH.Tool
- Small tool surfaces beat fat APIsA marketing API exposes 115 operations; my server hands the agent six tools. The boundary is set by token budget and model focus, not REST purity.Musing
- Guardrails for agents in productionA catalogue of the guards I actually ship: typed confirmations, blast-radius escalation, pay-to-play gating, ordered workflows, and read-only by default.Musing