ASK KNOX
beta
LESSON 400

Designing the Tool Surface — Inputs, Outputs & Consumer Contracts

Grep your callers before you define your schema. The output shape serves the consumer, not the implementation — and breaking changes get a new tool name, not a version bump.

9 min read·Building Production MCP Servers

The Problem Nobody Reads the Spec For

Tool design failures are almost never protocol violations. The MCP spec is clear: define a name, write a JSON Schema input, return content. Developers follow the spec. The failures happen at a higher level — in the decisions about what to name things, what input fields to require, what output shape to return, and who the consumer is.

The most common production failure: a tool whose output shape changed between versions, breaking a caller that was parsing a specific field. The tool returned 200. The caller got undefined. Nobody noticed for three days.

The consumer-contract check prevents this. It is a discipline, not a framework. You run it before defining any tool, and you run it before changing any tool.

The Four-Step Design Flow

The design flow starts with the consumer (step 1) and works backward to the schema (steps 2-4). Most developers work forward — they define what data the implementation returns, then write a schema that matches it. This produces tool surfaces that serve the implementation rather than the caller.

Starting with the consumer name is not ceremonial. It is a forcing function. When you write "Consumer: Claude Code sessions querying for architectural context at session start," you have immediately constrained the output shape. The output needs to be something Claude Code can read at session start without additional parsing. That is different from "whatever ChromaDB returns."

Schema Design for Input

Required fields first, optional fields second. This is not aesthetic — it is how the model reads the schema. The model processes input definitions top-to-bottom. Required fields appear at the top where they are seen first.

The description field for each input parameter is as important as the description field for the tool itself. query: { type: "string", description: "search query" } tells the model nothing useful. query: { type: "string", description: "What you want to know. Write a natural language question or topic name. Be specific — 'Semantic Memory Layer split-brain incident' outperforms 'memory bug'." } teaches the model how to write a good query.

Constrain values explicitly. If a parameter takes one of three values, use an enum — do not trust the model to self-limit to valid values. If a string has a minimum length, declare it with minLength. The model respects schema constraints reliably when they are explicit.

Defining the Output Contract

The output contract is the most stable part of your tool. Input schemas can add optional fields safely. Output shapes are harder — adding optional fields is safe, but removing or renaming fields breaks callers.

The Semantic Memory Layer memory_query output contract has been stable for eight months:

interface MemoryQueryResult {
  results: Memory[]
  total_found: number
  query_time_ms: number
}

interface Memory {
  memory_id: string
  content: string
  namespace: string
  relevance: number
}

That shape has not changed. Agent Gateway parses results[0].content. Claude Code sessions read the full results array. Any change to that shape would require coordinating with both callers — a significant cost for a running production system.

The Description Field Is a Contract

The description field of a tool is a contract with the model. It defines when the tool gets called. A vague description causes two failure modes: the tool gets called when it should not (false positive), or it does not get called when it should (false negative).

For memory_query, the current description is: "Semantic search across all indexed memories. Call at session start with the project name to load architectural context. Call when you need to recall a past decision, failure mode, or correction. Do not call for every question — use for context loading and for retrieving specific known knowledge."

That description contains:

  1. When to call: session start, past decisions, failure modes, corrections
  2. How to call: project name for session start queries
  3. When NOT to call: not for every question

The "when NOT to call" clause is often omitted. It is as important as the "when to call" clause — without it, the model will call the tool opportunistically for every question, burning tool call overhead on queries that would be better handled by the model's own knowledge.

Errors Are a Shape Too

A tool handler that throws an unhandled exception crashes the server or returns an opaque 500. A tool handler that returns a structured error gives the model something to work with.

The Semantic Memory Layer error contract:

// Every tool can return this
interface ToolError {
  ok: false
  error: string        // human-readable, logged safely
  code: string         // machine-readable: QUERY_FAILED, MEMORY_NOT_FOUND, etc.
}

// Every successful tool returns
interface ToolSuccess<T> {
  ok: true
  data: T
}

When memory_query fails to connect to ChromaDB, it returns { ok: false, error: "ChromaDB unavailable", code: "DB_UNAVAILABLE" }. The model can read this error and tell the user something meaningful — and the tool handler never threw, so the server process never crashed.

Now apply the contract yourself. Below is a handler written the naive way — it queries ChromaDB and returns the raw result. If ChromaDB is down, it throws straight out of the handler and the model gets an opaque failure. Convert it to the ok-tagged contract.

What's Next

The next lesson covers transport and lifecycle — the decision between stdio and HTTP/SSE, the startup sequence, health verification, and the reconnection strategy that makes HTTP transport survive client disconnects without a server restart.