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.
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 three primitives look similar. They are not. Each maps to a different trust boundary, and confusing them is a security bug.
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.
Most common bug: sending tools/call before initialized. The contract isn't in force; the server rejects it.
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.
{
"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.
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.
console.log on stdio transport. Client sees a parse error, disconnects, reconnects, loops. Fix: stderr only.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.
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.
Lab: build a TypeScript MCP server for a ticket system — two tools, schema validation, graceful shutdown, a test harness that exercises the lifecycle.