Chapter 11

Design Tools as Safe Operations

A tool in MCP is a model-controlled capability: the client decides when to offer it to the model, and the model may form the arguments itself. Hence a non-obvious consequence: the schema, the description, the annotations, authorization, idempotency, and the response size are not conveniences but part of the security boundary. A badly designed tool gives the model power you then have nothing left to constrain.

A good tool starts with a strict schema and honest annotations.

TypeScript
const searchOutputSchema = z.object({
  hits: z.array(z.object({
    id: z.string(),
    title: z.string(),
    excerpt: z.string(),
    score: z.number()
  }))
});

const reviewOutputSchema = z.object({
  service: z.string(),
  version: z.string(),
  decision: z.enum(["ready", "blocked"]),
  checks: z.array(z.object({
    id: z.enum(["ci", "tests", "security", "migration", "rollback"]),
    title: z.string(),
    passed: z.boolean(),
    evidence: z.string()
  })),
  summary: z.string()
});

  server.registerTool(
    "search_runbooks",
    {
      title: "Runbook search",
      description: "Searches only approved engineering runbooks and returns short excerpts.",
      inputSchema: z.object({ query: z.string().min(2).max(120) }),
      outputSchema: searchOutputSchema,
      annotations: {
        readOnlyHint: true,
        destructiveHint: false,
        idempotentHint: true,
        openWorldHint: false
      }
    },
    async ({ query }) => {
      const output = { hits: searchRunbooks(query) };
      return {
        content: [{ type: "text", text: JSON.stringify(output) }],
        structuredContent: output
      };
    }
  );

It's important to understand what each field promises - and, above all, what it doesn't guarantee.

Field · What it tells you · What it doesn't guarantee

  • inputSchema - The allowed wire shape of arguments; The user's rights and business invariants
  • outputSchema - The expected structure of the result; The truth of the data
  • readOnlyHint - The operation should not change state; The safety of an external API
  • destructiveHint - A hint about a destructive effect; Human approval
  • idempotentHint - A repeat with the same args adds no effect; A correct retry policy
  • openWorldHint - Whether there is interaction with the outside world; Network isolation

This table is a defense against the dangerous illusion that a schema and annotations already make a tool safe. inputSchema sets the allowed shape of arguments, but not the user's rights and not a business invariant. readOnlyHint speaks of intent, but not of the safety of an external API. Annotations are hints for UI and approval policy in the host, not a defense for the server.

To feel which surface to choose for a given intent, run it through the builder - it suggests what fits better: a tool, a resource, or a prompt.

Interactive lab 3

Choose the MCP surface

Tool: a narrow name, Zod input/output, authorization, timeout, and annotations.

The difference between a naive and a professional tool is almost always the width of its semantics. run_shell(command) or query_database(sql) give the model a language with enormous power and no allowlist. review_release(service, version) gives a narrow operation with a clear schema, authorization, and a predictable answer. The second is more boring, but it's exactly the one that survives to production.

Annotations are hints. The client may use them for UI and approval policy, but the server must not treat them as a defense. The real constraints live in the handler and the downstream system.

Tools perform operations - but often the model needs not an effect but a fact. For that there is a separate surface: resources.

Knowledge check

A tool has readOnlyHint: true. What does it guarantee?

Links