« Phase 03 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Staff Notes
Phase 03 — Core Contributor Notes: The Model Context Protocol
This is the maintainer's-eye view: how the real MCP spec and SDKs are actually built, what our in-memory miniature maps to, what it faithfully reproduces, and — more importantly — everything it deliberately leaves out that a committer to the spec or the SDKs has to hold in their head.
What is the spec, actually
MCP is a written specification plus a set of reference SDKs and reference servers, all open-source and stewarded by the project. The spec is small on purpose. Its core is: "MCP messages are JSON-RPC 2.0," full stop — the same request/response/notification grammar and the same reserved error codes our lab uses. Layered on that is a lifecycle (the initialize handshake), a capability model, and the primitive definitions. The spec is versioned by date, e.g. 2025-06-18. That dated scheme is deliberate: it lets independently-shipped clients and servers negotiate a common version at initialize, and it lets the spec evolve without a semantic-version flag day. Our PROTOCOL_VERSION = "2025-06-18" constant mirrors exactly this. A contributor's rule of thumb: the version string is a negotiation token, not a marketing number — the whole point is that two peers on different dates can still find common ground or cleanly refuse.
The SDKs, and what our code is a hand-rolled version of
There are first-party Python and TypeScript SDKs (and community SDKs in other languages). When you use the real SDK you do not write handle or the message helpers — the SDK is the message router, the lifecycle enforcer, and the transport adapter. Map our miniature onto it:
- Our
MCPServer.handleis the SDK's message dispatcher — the thing that reads a decoded JSON-RPC message, enforces "initialized before anything else," routes to the right primitive handler, and marshals results and errors back. In the real SDK you register handlers (often via decorators) and the framework owns the routing; we hand-rolled the router to see it. - Our
MCPClientis the SDK's client session — it owns the connection, the negotiated capabilities, request-id allocation, and the notification pump. Real clients do the same bookkeeping; we exposed it as plain methods. - Our
InMemoryTransportstands in for the SDK's transport implementations — the real ones are a stdio transport (subprocess pipes) and a Streamable HTTP transport. The SDKs are structured so the session logic is transport-agnostic and you plug a transport in, which is precisely why our identical session code would run over stdio with only the transport swapped.
The reference servers (filesystem, git, and others the project ships) are the canonical examples to copy the shape of — how to declare tools, structure resources, and wire the lifecycle. The hosts — Claude Desktop, Claude Code, VS Code, Cursor — are the applications that run a client per configured server. A committer thinks of "host," "client," "server" as three distinct implementable surfaces, and the SDK gives you the client and server halves.
The LSP lineage
MCP is consciously modeled on the Language Server Protocol (LSP), and understanding that lineage explains most of the design. LSP solved the editor version of the M×N problem: N languages × M editors used to mean N×M language integrations, until LSP made each language ship one server and each editor ship one client, so any editor got any language. MCP is the same architecture pointed at "AI apps ↔ tools/data" instead of "editors ↔ language tooling."
Concretely, what MCP borrowed from LSP: JSON-RPC 2.0 as the wire format (LSP uses it too); the stateful initialize handshake with capability negotiation (LSP negotiates client/server capabilities at startup identically); the request/notification split (LSP has both, e.g. hover requests vs. diagnostics notifications); and stdio as the default local transport (an editor spawns the language server as a subprocess over stdio — exactly how a host spawns a local MCP server). If you have read the LSP spec, MCP feels like a re-skin, and that is by design: LSP is a decade-proven pattern for exactly this shape of problem.
Transport evolution: SSE → Streamable HTTP
The most important "the API changed and here's why" fact a contributor tracks: the remote transport was revised. The original remote transport paired a plain HTTP endpoint for client→server requests with a long-lived Server-Sent Events (SSE) channel for server→client messages — two separate connections. That worked but was operationally awkward: a persistent SSE connection is a stateful thing to hold open across load balancers and proxies, complicates horizontal scaling, and doesn't degrade gracefully. It was superseded by Streamable HTTP, which folds the interaction into a single HTTP endpoint that can return either a normal JSON response or, when streaming is needed, an SSE stream within that same request. The upgrade to streaming is negotiated per request rather than requiring a permanently-open side channel, which is far friendlier to standard HTTP infrastructure and stateless scaling. If you read older MCP material referencing "the SSE transport," recognize it as the predecessor of Streamable HTTP — the messages are the same JSON-RPC, only the framing changed.
The richer primitives our miniature omits
Our lab implements the three server primitives (tools, resources, prompts) and the lifecycle. A committer knows the fuller set:
sampling/createMessage— a client primitive: the server asks the host to run an LLM completion on its behalf. This is elegant and non-obvious: a server author can build agentic behavior without shipping a model SDK or holding an API key, because it borrows the host's model. It also keeps servers model-agnostic — the host decides which model answers. The host mediates and can gate these requests, which matters for cost and safety.elicitation/create— another client primitive: the server asks the user for structured input or confirmation mid-operation. This is the protocol-level human-in-the-loop hook — the sanctioned way for a server to request approval before something irreversible, rather than inventing its own prompt.- Logging — the server sends structured log messages to the client for debugging and monitoring.
- Server-side features we skipped: resource subscriptions (a client subscribes to a resource and gets change notifications, beyond our one-shot
resources/read); pagination (*/listresponses can return a cursor so large tool/resource sets don't come in one payload); progress notifications (long-running calls report progress); and real authorization (OAuth for remote servers, an entire sub-spec our miniature has no analog for).
Describe these by pattern, not by exact field names, if you're reconstructing them from memory — the shapes above are accurate; specific parameter keys should be checked against the current spec before you quote them.
The security discourse a maintainer tracks
The live, ongoing conversation in the MCP community is security, and specifically tool poisoning / prompt injection over MCP. The core worry: an LLM reads a tool's description (and its results) as trustworthy context, so a malicious or compromised server can smuggle instructions into those strings and hijack the agent. This has driven concrete spec and tooling attention — clearer guidance that servers are untrusted, host-side consent and permission models, and scrutiny of the remote-auth story. A contributor holds the position the whole track holds: the protocol standardizes discovery and transport; it does not make a server trustworthy. That is left to the host — permissioning, sandboxing, human approval, scoped auth.
What our miniature deliberately simplifies
To be honest about the map-versus-territory gap, our lab simplifies, in decreasing order of significance:
- No real transport — in-memory
json.dumps/json.loadsinstead of stdio pipes or Streamable HTTP; no framing, no connection management, no reconnection. - No auth — real remote servers require OAuth; we have none.
- No client primitives — no
sampling/createMessage, noelicitation/create; sampling and elicitation are conceptual here. - One-shot lists — no pagination, no resource subscriptions, no progress notifications.
- Synchronous, single-client, in-process — no concurrency, no interleaved requests, no multiple simultaneous clients; the notification "pump" is a manual drain rather than an async event loop.
- Argument checking is presence-only — we check required keys are present; full JSON-Schema validation of tool arguments lives in Phase 02's discipline, and the real SDKs lean on the schema harder.
What the miniature gets right is the load-bearing part: the exact method names, the three message shapes, the five error codes, the stateful handshake and its invariant, capability negotiation, the permission boundary at tools/call, and notifications/tools/list_changed. Those are the parts you must understand to read the spec, use the SDK, or reason about a real server — and they are faithful. Everything above is layered on the same JSON-RPC core you already built.