« 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
- The problem MCP solves: M×N integration
- Host, client, server
- The two layers: data and transport
- JSON-RPC 2.0 in five minutes
- The lifecycle: initialize and capability negotiation
- Server primitives: tools, resources, prompts
- Client primitives: sampling, elicitation, logging
- Discovery and notifications
- Transports: stdio and Streamable HTTP
- Security: an MCP server is untrusted code
- MCP vs plain function calling
- Common misconceptions
- Lab walkthrough
- Success criteria
- Interview Q&A
- 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.
- The client sends
initializewith itsprotocolVersion(e.g."2025-06-18"), itscapabilities(what it supports — e.g.elicitation), andclientInfo. - The server responds with its
protocolVersion, itscapabilities(e.g.{"tools": {"listChanged": true}, "resources": {}, "prompts": {}}), andserverInfo. - The client sends the
notifications/initializednotification 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 hasname,title,description,inputSchema— the JSON Schema from Phase 02); invoked viatools/callwith{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 viaresources/readwith auri. 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 viaprompts/getwith 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
idand 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/callstill carries schema-validated arguments.
13. Lab walkthrough
Open lab-01-mcp-server-client/ and fill the TODOs:
- Message helpers —
request,notification(noid!),ok_response,error_response. MCPServer.handle— the lifecycle (initialize; reject others before initialized; notifications update state and returnNone) and dispatch totools/,resources/,prompts/handlers, convertingRPCErrorto error responses._call_tool— unknown tool →-32601, not-allow-listed →-32600, missing required arg →-32602, tool exception → error content (not a crash).InMemoryTransport.send— serialize to JSON and back (prove it's on the wire).MCPClient—initialize(+ sendnotifications/initialized+ pump notifications),list_tools(cache),call_tool,read_resource,get_prompt, and thelist_changedrefresh.
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
initializehandshake and say what capabilities negotiate. -
Your server rejects calls before
initializeand 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
labandsolution.
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.