« Phase 03 README · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor · Staff Notes

Phase 03 Warmup — The Model Context Protocol (MCP)

Who this is for: you did Phases 00–02 and can write Python. You've heard "MCP" but never implemented it. By the end you'll have built a real JSON-RPC 2.0 MCP server and client with capability negotiation and permission gating, and you'll understand the protocol well enough to write a production server and reason about its security.

Table of Contents

  1. The problem MCP solves: M×N integration
  2. Host, client, server
  3. The two layers: data and transport
  4. JSON-RPC 2.0 in five minutes
  5. The lifecycle: initialize and capability negotiation
  6. Server primitives: tools, resources, prompts
  7. Client primitives: sampling, elicitation, logging
  8. Discovery and notifications
  9. Transports: stdio and Streamable HTTP
  10. Security: an MCP server is untrusted code
  11. MCP vs plain function calling
  12. Common misconceptions
  13. Lab walkthrough
  14. Success criteria
  15. Interview Q&A
  16. References

1. The problem MCP solves: M×N integration

Suppose you have M AI applications (a chat app, a coding agent, an internal assistant) and N data sources / tools (GitHub, Postgres, Slack, your CRM). If every app integrates every source directly, you write M×N bespoke integrations, and every new source means M more, every new app N more. It doesn't scale, and every integration is a slightly different, undocumented one-off.

The Model Context Protocol (MCP), released by Anthropic in November 2024 and since adopted across the industry, collapses this to M+N: each source author writes one MCP server; each app author writes one MCP client; any client can talk to any server because they share a protocol. It's the same move USB did for peripherals, or LSP (the Language Server Protocol) did for editors and languages — and MCP is consciously modeled on LSP. That analogy is worth keeping: MCP is to "AI apps ↔ tools/data" what LSP is to "editors ↔ language tooling."


2. Host, client, server

Three roles, precisely:

  • Host — the AI application the user interacts with (Claude Desktop, Claude Code, VS Code, your agent). It coordinates one or more clients.
  • Client — a component inside the host that maintains one dedicated connection to one server. If the host connects to three servers, it spins up three clients.
  • Server — a program that exposes context (tools/resources/prompts). It can run locally (as a subprocess over stdio) or remotely (over HTTP).

The one-client-per-server rule matters: it means each connection has its own lifecycle, capabilities, and permission scope. Your host decides which servers to trust and what each may do — the security boundary lives at the client-server connection.


3. The two layers: data and transport

MCP is defined in two layers, and separating them is the key to understanding it:

  • Data layer (the inner layer) — a JSON-RPC 2.0 protocol defining the messages: lifecycle management, and the primitives (tools, resources, prompts, notifications). This is the part you reason about as a developer, and the part the lab builds.
  • Transport layer (the outer layer) — how those JSON messages move: connection setup, message framing, authentication. Two transports exist (§9). The same JSON-RPC messages ride any transport unchanged, which is why the lab can use an in-memory transport and still be faithful to the protocol.

4. JSON-RPC 2.0 in five minutes

MCP's data layer is JSON-RPC 2.0, a tiny, decades-tested RPC format. Three message shapes:

Request — has an id, a method, optional params; expects a response:

{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

Response — echoes the id, carries either a result or an error:

{"jsonrpc": "2.0", "id": 1, "result": {"tools": [ ... ]}}
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32601, "message": "method not found"}}

Notification — a request with no id; fire-and-forget, no response:

{"jsonrpc": "2.0", "method": "notifications/initialized"}

The standard error codes you'll use: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error. The whole protocol is request/response plus notifications — nothing exotic. The lab builds these helpers first, and a test asserts a notification has no id (the detail that catches people).


5. The lifecycle: initialize and capability negotiation

MCP is a stateful protocol: before anything else, client and server perform a handshake.

  1. The client sends initialize with its protocolVersion (e.g. "2025-06-18"), its capabilities (what it supports — e.g. elicitation), and clientInfo.
  2. The server responds with its protocolVersion, its capabilities (e.g. {"tools": {"listChanged": true}, "resources": {}, "prompts": {}}), and serverInfo.
  3. The client sends the notifications/initialized notification to say "ready."

Why negotiate capabilities? So neither side calls a feature the other doesn't support. If the server declares tools.listChanged: true, the client knows it may receive notifications/tools/list_changed; if a client declares elicitation, the server knows it may ask the user for input. Version mismatch is handled here too — if the two can't agree on a compatible protocol version, the connection should terminate. In the lab, calling any method before initialize returns an error — the lifecycle is enforced, which is a real spec requirement and a common bug when people skip the handshake.


6. Server primitives: tools, resources, prompts

A server exposes three kinds of things, each with a */list (discover) and a retrieve/execute method:

  • Tools — executable functions the AI can invoke to act (query a DB, call an API). Listed via tools/list (each tool has name, title, description, inputSchema — the JSON Schema from Phase 02); invoked via tools/call with {name, arguments}, returning {"content": [{"type": "text", "text": ...}]}. Tools are model-controlled: the LLM decides to call them.
  • Resources — data the AI can read for context (a file, a DB schema, an API response). Listed via resources/list, read via resources/read with a uri. Resources are typically application-controlled (the host decides what to load).
  • Prompts — reusable, parameterized templates (a system prompt, a few-shot set). Listed via prompts/list, fetched via prompts/get with arguments substituted. Prompts are usually user-controlled (surfaced as slash-commands or menu items).

The three map cleanly to "act / read / template," and the lab implements all three including the argument substitution in prompts/get and the error paths (unknown tool → -32601, missing arg → -32602).


7. Client primitives: sampling, elicitation, logging

MCP is bidirectional — a server can ask the client for things, via primitives the client exposes:

  • Sampling (sampling/createMessage) — the server asks the client's LLM to generate a completion. This is elegant: a server author can use an LLM without shipping a model SDK or API key — they borrow the host's model. It also keeps the server model-agnostic.
  • Elicitation (elicitation/create) — the server asks the user for more input or a confirmation ("are you sure you want to delete this?"). This is the protocol-level hook for human-in-the-loop (Phase 10).
  • Logging — the server sends log messages to the client for debugging/monitoring.

These are what make MCP richer than a one-way tool list: a server can drive a small conversation with the host and user. (The lab focuses on the server primitives + lifecycle + notifications; sampling/elicitation are covered conceptually and are a great "extend the lab" exercise.)


8. Discovery and notifications

The */list methods make MCP dynamic: a client discovers what's available at runtime rather than hard-coding it. And when the available set changes — a server enables a new tool based on state or permissions — the server sends notifications/tools/list_changed (only if it declared listChanged: true). The client, on receiving it, re-fetches tools/list to stay current. This refresh cycle is why an MCP-connected agent can gain new capabilities mid-session. The lab implements exactly this: register a tool at runtime with notify=True, and the client's tool cache updates when it pumps notifications.


9. Transports: stdio and Streamable HTTP

Two transports, same JSON-RPC messages:

  • stdio — the server runs as a local subprocess; the client writes JSON-RPC to its stdin and reads from its stdout. No network, lowest latency, one client per server. This is how Claude Desktop launches the local filesystem server. Great for local tools and dev.
  • Streamable HTTP — the server runs remotely; the client sends HTTP POSTs, with optional Server-Sent Events for streaming. Supports standard HTTP auth — MCP recommends OAuth for obtaining tokens. This is how a hosted server (e.g. Sentry's) serves many clients.

The lab uses an in-memory transport (serialize to JSON, hand to the server, deserialize the response) so it's deterministic and offline — but because the data layer is transport-agnostic, the exact same server/client code would work over stdio or HTTP with only the transport swapped. That's the whole point of the two-layer design, and a good thing to articulate in an interview.


10. Security: an MCP server is untrusted code

This is the part that separates a Staff answer from a demo. When your host connects to an MCP server, you are running someone else's code (local) or trusting someone else's endpoint (remote) inside your agent's context. That is a supply-chain and injection surface:

  • Tool poisoning — a malicious server can put injection payloads in a tool's description (which the model reads) or return hostile content from a tool call, hijacking the agent (Phase 10). The tool description is untrusted text.
  • Over-broad permissions — a server that can read your filesystem or hit arbitrary hosts is a data-exfiltration risk. Permission-gate which tools each client may call (the lab enforces an allow-list; a denied call returns an error), and sandbox what a tool can touch (Phase 09).
  • Confused-deputy / consent — the human must approve connecting a server and, for dangerous tools, approve each call (elicitation, human-in-the-loop). Never auto-approve destructive tools.
  • Remote auth — remote servers need OAuth; a token that's too broad is a breach waiting to happen (Phase 13).

The protocol standardizes discovery and transport. It does not make a server trustworthy — your host's permission model, sandboxing, and human-approval gates do. Saying this unprompted is a strong signal.


11. MCP vs plain function calling

A frequent interview question. Phase 02's function calling defines tools inside your app; MCP defines tools behind a protocol boundary in a separate server. What MCP adds:

  • Standard discovery — any client can enumerate any server's tools/resources/prompts.
  • Standard transport + auth — stdio/HTTP + OAuth, not a bespoke API per integration.
  • Capability negotiation + lifecycle — versioned, feature-gated connections.
  • Reuse across hosts — one server, many AI apps (the M+N win).
  • A security boundary — the server is a separate, permission-gated, auditable component.

The tradeoff: a protocol hop and a server to run/operate. For a single app's private tools, plain function calling is simpler; for shared, reusable, cross-app integrations (the enterprise case in every JD), MCP is the right tool. Underneath, an MCP tools/call still carries the same name + JSON-Schema arguments you validate exactly as in Phase 02.


12. Common misconceptions

  • "MCP is a model / an SDK / an Anthropic product." It's an open protocol (JSON-RPC 2.0 messages + lifecycle + primitives). Anthropic authored it; it's vendor-neutral.
  • "An MCP server is safe because it's a protocol." The protocol is untrusted code you connect to. Gate permissions, sandbox, get human approval.
  • "Notifications get responses." They have no id and get none.
  • "You must use stdio." stdio (local) or Streamable HTTP + OAuth (remote); same messages.
  • "MCP replaces function calling." It standardizes and shares it across apps; a tools/call still carries schema-validated arguments.

13. Lab walkthrough

Open lab-01-mcp-server-client/ and fill the TODOs:

  1. Message helpersrequest, notification (no id!), ok_response, error_response.
  2. MCPServer.handle — the lifecycle (initialize; reject others before initialized; notifications update state and return None) and dispatch to tools/, resources/, prompts/ handlers, converting RPCError to error responses.
  3. _call_tool — unknown tool → -32601, not-allow-listed → -32600, missing required arg → -32602, tool exception → error content (not a crash).
  4. InMemoryTransport.send — serialize to JSON and back (prove it's on the wire).
  5. MCPClientinitialize (+ send notifications/initialized + pump notifications), list_tools (cache), call_tool, read_resource, get_prompt, and the list_changed refresh.

Run LAB_MODULE=solution pytest -v first to see the target, then match it. Read solution.py's main() — it runs a full session including a denied tool and a runtime list_changed.


14. Success criteria

  • You can hand-walk the initialize handshake and say what capabilities negotiate.
  • Your server rejects calls before initialize and denies non-allow-listed tools.
  • You can name the 3 server + 3 client primitives and their methods.
  • You can explain why an MCP server is a security boundary and how to gate it.
  • All 16 tests pass under lab and solution.

15. Interview Q&A

Q: What is MCP and what problem does it solve? A: It's an open JSON-RPC 2.0 protocol that standardizes how AI apps connect to tools and data — "USB-C for AI tools," modeled on LSP. It turns the M×N integration problem (every app wired to every source) into M+N: one server per source, one client per app, and any client can use any server. Anthropic opened it in late 2024 and it's now an industry standard.

Q: Walk me through the connection lifecycle. A: The client sends initialize with its protocol version, capabilities, and clientInfo; the server replies with its version, capabilities, and serverInfo; the client sends the notifications/initialized notification. That handshake negotiates a compatible version and which features each side supports, so nobody calls an unsupported method. Any method before initialize is an error.

Q: What are the primitives? A: Server-side: tools (model-invoked actions, tools/call), resources (readable context data, resources/read), prompts (parameterized templates, prompts/get). Client-side: sampling (sampling/createMessage — the server borrows the host's LLM), elicitation (elicitation/create — ask the user), and logging. All discovered via */list, with notifications/*/list_changed for dynamic updates.

Q: MCP vs plain function calling — when each? A: Function calling defines tools inside one app; MCP puts them behind a protocol boundary in a separate, reusable, permission-gated server with standard discovery, transport, and auth. For a single app's private tools, function calling is simpler; for shared, cross-app, enterprise integrations, MCP wins the M+N reuse and gives you a clean security boundary. A tools/call still carries schema-validated arguments underneath.

Q: What are the security risks of connecting to an MCP server? A: You're running untrusted code / trusting an untrusted endpoint inside your agent. Risks: tool poisoning (injection in the tool description or results — Phase 10), over-broad permissions enabling exfiltration, and confused-deputy without human consent. Mitigations: permission-gate which tools each client may call, sandbox what tools can touch (Phase 09), require human approval for dangerous tools (elicitation), and use scoped OAuth for remote servers. The protocol standardizes discovery and transport; it does not make the server safe.

Q: Why stdio vs Streamable HTTP? A: stdio runs the server as a local subprocess (no network, lowest latency, one client) — good for local/dev tools; Streamable HTTP runs it remotely (HTTP POST + optional SSE streaming, OAuth auth) — good for hosted, multi-client servers. Same JSON-RPC messages ride either, because the data and transport layers are separated.


16. References

  • Model Context Protocol — official site & spec. https://modelcontextprotocol.io/ · https://modelcontextprotocol.io/specification/latest
  • Anthropic — Introducing the Model Context Protocol (2024). https://www.anthropic.com/news/model-context-protocol
  • JSON-RPC 2.0 specification. https://www.jsonrpc.org/specification
  • MCP Python SDK & reference servers. https://github.com/modelcontextprotocol
  • Language Server Protocol (the design inspiration). https://microsoft.github.io/language-server-protocol/
  • MCP security discussions (tool poisoning, prompt injection over MCP) — see the spec's security section and Phase 10 of this track.