# DD-22 — Teaching Script: MCP Building Production Tool Servers

*Verbatim teaching transcript. Read aloud at ~140 wpm. Maps to the 10-slide deck in 03-slide-deck.html. ~2,700 words across 10 slide cues. Total runtime ~50 minutes.*

---

[SLIDE 0 — Title]

Welcome to DD-22. This deep-dive is about building production MCP servers. MCP — the Model Context Protocol — is the wire protocol that lets your agent talk to tool servers as services rather than as compiled-in function references.

Here is the thesis, in one sentence. Pi's four 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. Hold on to that sentence. Everything in this deep-dive is a decomposition of it.

A quick note on scope. This deep-dive covers building servers. The security side — rogue servers, prompt injection via tool descriptions, supply-chain attacks on the MCP ecosystem — is covered in Course 2B, specifically the SDD-B07 offensive expansion. We assume the server is trustworthy here and focus on building it well. Read this deep-dive first; SDD-B07 is the security counterpart.

---

[SLIDE 1 — The thesis]

Let's start with the structural difference. Pi, the reference harness from DD-01, has four tools: read_file, write_file, edit_file, bash. Each is a function reference in a file called tool-registry.ts. When the model wants to call a tool, the harness dispatches to the function. Adding a tool means editing that file, shipping a new build, restarting the process.

That's fine for a personal harness. It's fatal for a platform.

A platform needs tools to be external. A new database connector should not require redeploying the agent. A vendor integration should arrive as a server the agent discovers at runtime. The team that maintains the CRM should ship their MCP server on their own release cadence, and the agent should pick it up without a rebuild.

MCP is the wire protocol that makes this possible. It defines three primitives — tools, resources, prompts. It defines two transports — stdio for local, HTTP+SSE for remote. And it uses JSON-RPC 2.0 as the message envelope. The result: a tool becomes a service. A service has a version, a health check, a deployment pipeline, and an owner.

---

[SLIDE 2 — Three primitives, two transports]

Three primitives. Let's go through them, because confusing them is a bug.

A tool is a model-invoked function. The server advertises it with a name, a description, and a JSON Schema for its input. The model sees the name and description, decides to call it, produces arguments, the server executes and returns a result. This is the primitive that replaces Pi's tool-registry.ts.

A resource is server-exposed passive data. A file, a database row, a configuration object. The server exposes a URI scheme. The client reads resources by URI. The model does not call resources — the client surfaces them, the user or the client decides what to read. This is the primitive for "context the server knows about."

A prompt is a server-authored template. A named, parameterized message the server provides. The user invokes it from a menu, fills in parameters, and the rendered result goes into the conversation. This is the primitive for "the server knows the right way to ask for something."

The envelope is JSON-RPC 2.0. Three message types: request (has an id, expects a reply), response (carries the result or error for that id), notification (no id, no reply — fire and forget). This is the same substrate as the Language Server Protocol. Not exotic.

Two transports. stdio: the agent spawns the server as a subprocess. Requests go to stdin, responses come from stdout. This is the local transport. HTTP+SSE: the agent connects to an HTTP endpoint, the server pushes updates via Server-Sent Events. This is the remote transport. Same protocol, different pipes.

---

[SLIDE 3 — The trust boundaries]

Here's why three primitives matter. Each maps to a different trust boundary.

For a tool, the model initiates the call. The input is untrusted — the model produced it, possibly under adversarial steering. The server must validate it against the schema, and the tool must be safe to execute with arbitrary conforming arguments.

For a resource, the user or client initiates the read. The data is semi-trusted — the server vouches for it. The model never decides to read a resource. This matters: you don't want the model autonomously pulling context the user didn't ask for.

For a prompt, the user initiates. The template is trusted because the server authored it. The user picks it from a menu.

The common 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. If you have a "get customer data" capability, think carefully about whether the model should call it autonomously (a tool) or whether the user should opt in each time (a resource). The protocol separates these for a reason.

---

[SLIDE 4 — The lifecycle]

Now the lifecycle. An MCP session is a stateful conversation with a defined beginning, middle, and end. Four phases.

Phase one: initialize. The client sends a message with the protocol version it speaks, its own capabilities, and client identification. The server responds with the protocol version it will use — which may be lower than what the client offered — its capabilities, and server identification. This exchange is the contract.

Phase two: the initialized notification. The client sends a notification — no reply expected — confirming the handshake. Only after this notification may the client send any other request. This is not ceremony. It's a synchronization point. The most common integration bug is sending a tools/call before sending initialized. The server rejects it because the contract isn't in force yet.

Phase three: the message loop. tools/list to get the catalog. tools/call when the model acts. resources/read when the user wants context. The server pushes notifications when its state changes. This loop runs until the session ends.

Phase four: graceful shutdown. On stdio, the client closes stdin, waits for the server to exit, sends SIGTERM if it doesn't. On HTTP+SSE, the client closes the connection. In both cases, a well-behaved server finishes in-flight requests, closes resource handles, flushes logs, exits zero. The server must assume it can be killed at any time and must not leave the world broken — half-written files, open transactions, leaked temp directories.

---

[SLIDE 5 — Capability negotiation]

Capabilities. This is the part most teams skip, and it's the part that makes MCP robust.

Capabilities are not booleans. They're structured objects. The server's tools capability might include listChanged: true, meaning the server will notify the client when the tool set changes. The client's roots capability — the filesystem locations the client operates in — might include listChanged: true.

The negotiation rule: only use what both sides advertised. The session may only use the intersection of client-offered and server-offered features. If the server offers listChanged but the client didn't advertise it understands list-changed notifications, the server should not send them. If the client offers roots but the server didn't advertise roots support, the server cannot query the client's filesystem roots.

This is the difference between MCP and a typical REST API. A REST API documents its endpoints and assumes you read the docs. MCP negotiates the endpoint set at runtime. A client written against version two degrades gracefully against a server speaking version one — it just uses the subset both support. You build a client once; it works against every server.

---

[SLIDE 6 — The validation boundary]

Now the part that matters for security: JSON Schema as the validation boundary.

Look at this tool definition. search_tickets. The input schema says: query is a string, one to two hundred characters. status is an enum: open, closed, or all. limit is an integer, one to fifty. Required: query. And the line I want you to internalize: additionalProperties: false.

This schema is load-bearing for three reasons.

One: the model uses it to generate arguments. The description and schema are what the model sees. Tight schemas produce better calls than loose ones.

Two: the server uses it to validate before execution. The server re-validates on every call. Never trust the model to produce conforming arguments. Never trust the client to have validated. The schema is the server's defense.

Three — and this is the security control — additionalProperties: false. Without it, a steered model can inject fields the server doesn't expect. Imagine a send_email tool without this directive. The model, steered by injection, produces: to, subject, body, and bcc set to attacker at evil dot com. The server, not forbidding additional properties, happily processes the bcc. Forbidding extra fields closes this. It is not a schema nicety. It is a security control.

---

[SLIDE 7 — Three classes of failure]

A tool call can fail in three distinct ways. The model needs to know which one it got, because the recovery path differs.

Class one: unknown tool. The model called a tool that doesn't exist in the catalog. The server returns isError: true with a message. The model's recovery: stop calling it.

Class two: validation failure. The arguments don't conform to the schema. The server returns isError: true with the validation error. The model's recovery: retry with corrected arguments.

Class three: execution failure. The tool ran, the business logic threw. The server returns isError: true with the exception message. The model's recovery: retry, or report to the user.

Each returns isError: true with a text payload. The text tells the model which class. The model's response differs per class. If you conflate them — say, returning the same generic "tool failed" for all three — the model can't distinguish "I called the wrong thing" from "I called the right thing with bad arguments" from "the right thing broke." You get a confused loop. Distinguish the three classes. The teaching document has the full TypeScript server code that implements this distinction.

---

[SLIDE 8 — Production operations]

Now the operator's view. Four ways a production deployment breaks.

One: stdout corruption. A stray console.log on a stdio transport. stdout is the protocol stream. The log turns into a malformed JSON-RPC message. The client tries to parse it, fails, disconnects, reconnects, and loops. This is the single most common MCP server bug, and it's silent in development — where you might be using the HTTP transport — and fatal in production. Fix: log to stderr only. Always. No exceptions.

Two: schema drift. You have twelve servers. Server A is at version 1.2, server B is at 2.0, server C is still at 0.9. The tool names differ. The schemas differ. The model produces arguments for the old schema and validation fails. Fix: namespace tools by server — acme-crm underscore underscore search_tickets, not just search_tickets. Pin versions in the registry. Treat schema changes as breaking.

Three: subscription flood. You subscribed to a volatile resource — a live log, a busy event stream. The server sends a notification per change. Your client's context fills with resource-update noise. The model sees a wall of diffs instead of the conversation. Fix: subscribe narrowly to specific URIs. Debounce on the server — coalesce updates, send at most one per second. Unsubscribe aggressively when the model is done.

Four: no graceful shutdown. SIGTERM is unhandled. The orchestrator kills the server after a timeout. In-flight requests are orphaned, temp files leak, transactions stay open. Fix: handle SIGTERM from day one. Drain in-flight, close handles, flush logs, exit zero.

And the production patterns that matter beyond fixing breakage: connection pooling for HTTP+SSE servers, health checks outside the protocol, rate limiting per client — because a runaway model calling a paid API tool will cost real money — idempotency keys for side-effecting tools, and structured logging to stderr for every call.

---

[SLIDE 9 — The platform turn]

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

The arc is this. You start with Pi — four hardcoded tools. You add an MCP server — your tools become a discoverable service. You add a second server, and a third. Now you have a fleet, and you need the registry, the policy, the audit. That's the platform journey, and MCP is the protocol at every step.

The lab asks you to build a TypeScript MCP server for a ticket system — two tools, schema validation, graceful shutdown, and a test harness that exercises the full lifecycle. The code in the teaching document is real. It compiles against the official SDK. Start there.

Next is DD-23 — DeerFlow — which takes the minimal agent loop and specializes it for deep research. Then DD-24 on cost-aware agents. Both build on the platform turn this deep-dive introduced.

That's DD-22. The wire protocol that turns hardcoded tool registries into services. The difference between a personal harness and a platform.

---

*End of transcript. Runtime ~50 minutes at 140 wpm.*
