DD-22 · Course 1 · Deep-Dive

MCP: Building Production Tool Servers

The wire protocol that turns hardcoded tool registries into discoverable, versioned, independently-deployable services.

Pi's 4 tools are hand-coded. An MCP server externalizes tool definitions into a discoverable, versioned, independently-deployable service. This is the difference between a personal harness and a platform.
The thesis

A tool is a function. A tool server is a process.

Pi (DD-01) has four tools hardcoded in tool-registry.ts. Adding a tool means editing the harness source, shipping a build, restarting the process.

Fine for a personal harness. Fatal for a platform.

A platform needs tools to be external. A new connector should not require a redeploy. A vendor integration should arrive as a server discovered at runtime.

MCP is the wire protocol that makes this possible.

The protocol

Three primitives. Two transports. One envelope.

Tool
Model calls it
A function with a name, description, and JSON Schema input. The primitive that replaces hardcoded registries.
Resource
Client reads it
Passive data addressed by URI. The model does not call it. The user or client opts to read.
Prompt
User invokes it
A server-authored template the user picks from a menu. Trusted, because the server wrote it.

Envelope: JSON-RPC 2.0 — request (id, method, params), response (id, result | error), notification (no id).

Transports: stdio (local subprocess) and HTTP+SSE (remote server). Same protocol, different pipes.

The trust boundaries

Why three primitives, not one

The three primitives look similar. They are not. Each maps to a different trust boundary, and confusing them is a security bug.

  • Tool — the model initiates. Input is untrusted. Must be schema-validated. Must be safe to execute with arbitrary arguments.
  • Resource — the user/client initiates. Data is semi-trusted. The server vouches for the content. The model never decides to read.
  • Prompt — the user initiates. Template is trusted. The server authored it. Parameterized, not freeform.

The bug: exposing as a tool something that should be a resource. Now the model can call it whenever it wants — including when an attacker steers it to.

The lifecycle

Initialize. Negotiate. Loop. Shutdown.

01
Initialize
Client sends protocol version + client capabilities. Server responds with version + server capabilities + identity. The contract begins.
02
Initialized notification
Client confirms the handshake. Only after this may any other request be sent.
03
Message loop
tools/list, tools/call, resources/read, notifications/* — the work of the session.
04
Graceful shutdown
SIGTERM or disconnect. Drain in-flight, close handles, flush logs, exit zero.

Most common bug: sending tools/call before initialized. The contract isn't in force; the server rejects it.

Capability negotiation

Only use what both sides advertised

Capabilities are not booleans. They are structured objects. The rule: the session may only use the intersection of client-offered and server-offered features.

Server offers: tools (listChanged: true), resources (subscribe: true), logging.

Client offers: tools, resources, roots (listChanged: true), prompts.

Negotiated: tools/list + tools/call, resources/read + resources/subscribe, prompts/list + prompts/get.

Forbidden: client cannot query roots (server didn't offer it). Server cannot send logs (client didn't offer it).

Why this matters: a client written against v2 degrades gracefully against a v1 server. A server upgrade doesn't break old clients. You build once; it works against everything.

The validation boundary

JSON Schema is not documentation — it is the defense

{
  "name": "search_tickets",
  "description": "Search support tickets. Returns up to 50.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query":  { "type": "string", "minLength": 1, "maxLength": 200 },
      "status": { "type": "string", "enum": ["open", "closed", "all"] },
      "limit":  { "type": "integer", "minimum": 1, "maximum": 50 }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}

Without additionalProperties: false: a steered model can inject { to, subject, body, bcc: "attacker@evil.com" } into a send_email tool. Forbidding extra fields is a security control.

The server re-validates on every call. Never trust the model to produce conforming arguments. Never trust the client to have validated.

Three classes of failure

The model needs to know which one it got

Class 1
Unknown tool
The model called a tool that doesn't exist. Recovery: stop calling it.
Class 2
Validation failure
Arguments don't conform to schema. Recovery: retry with corrected arguments.
Class 3
Execution failure
The tool ran and threw. Recovery: retry, or report to the user.

Each returns isError: true with a text payload the model reads. The text tells the model which class — and the model's response differs per class. Conflating them produces a confused loop.

Production operations

The four ways a deployment breaks

  • stdout corruption — a stray console.log on stdio transport. Client sees a parse error, disconnects, reconnects, loops. Fix: stderr only.
  • schema drift — server upgraded, tool input changed. Model produces args for the old schema, validation fails. Fix: namespace + version tools.
  • subscription flood — subscribed to a volatile resource. Context fills with update noise, model loses the conversation. Fix: debounce + unsubscribe.
  • no graceful shutdown — SIGTERM unhandled. Orchestrator force-kills. In-flight requests orphaned, temp files leak. Fix: handle SIGTERM from day one.

Production patterns: connection pooling (HTTP+SSE), health checks (outside the protocol), rate limiting per client, idempotency keys for side-effecting tools, structured logs to stderr.

The platform turn

From one server to a fleet

This deep-dive builds the single server. Course 4 Module E07 builds the fleet: the registry as a service, multi-tenancy, the policy layer that gates which agents call which servers, the audit log of every tool invocation.

A tool becomes a service. A service has a version, a health check, a deployment pipeline, and an owner. This is the structural difference between "my agent has four tools" and "my platform integrates twelve vendor systems."

Lab: build a TypeScript MCP server for a ticket system — two tools, schema validation, graceful shutdown, a test harness that exercises the lifecycle.