MCP
Read the transcript
1. MCP in one breath, and the revision that broke old mental models
Host: So let’s start with the basics, because this episode is going to spend a lot of time on one change that only makes sense if you know what MCP normally does. At its simplest, MCP is a protocol for exposing tools, resources, and prompts to a model host, right? Before this existed, if you wanted to connect a model to GitHub or a filesystem, you wrote a custom integration for each app, each time.
Guest: Exactly, and that’s really the whole value proposition — not the wire format itself, but the fact that an integration written once as an MCP server now works against any compliant client. You write the GitHub server once, and every host that speaks MCP gets it for free. That reusability is the entire point.
Host: Okay, but here’s the hinge for this whole episode: the 2026-07-28 revision didn’t just polish that idea, it pulled out something people assumed was load-bearing — the initialize handshake and the Mcp-Session-Id header are just gone. The core protocol is now stateless, every request carries its own version and capabilities, and if your mental model still starts with ‘first we establish a session,’ that model is out of date. We’re going to trace that one removal through the architecture, the three primitives, and the security model for the rest of the show.
2. Three primitives, three owners of the invocation decision
Host: Before we get to what the stateless rewrite touches, let’s nail down what it’s operating on. MCP has three roles — host, client, server — and then three kinds of things a server can expose: tools, resources, prompts. That sounds like plain taxonomy, but you told me before we started recording that it’s actually a security boundary. Walk me through that.
Guest: Right, the roles first: the host is the app the user’s actually looking at, the client is a connection manager living inside it — one per server — and the server is a small, focused process exposing capabilities, the same one-integration-per-server shape as an aggregation gateway. But the part that matters is who’s allowed to pull the trigger on each primitive. Tools are model-invoked — the model itself decides to call one, same as function calling. Resources are application-controlled — the host decides what data gets attached to context, not the model, even though some hosts let the model request a read. And prompts are user-invoked — a person picks a slash command or menu item, the model and the host don’t get a vote. So if you’re deciding how to expose ‘read this file,’ making it a tool hands the model autonomy to read whenever it wants, and making it a resource keeps that read under host or user control — same capability, completely different trust boundary depending on which primitive you pick.
3. What statelessness actually buys you (and costs you)
Host: So walk me through what actually replaces the handshake, because ‘no session’ sounds simple until you ask where the protocol version and client capabilities go now. Every request just carries them?
Guest: Every single one — three keys in _meta, protocolVersion, clientInfo, clientCapabilities, on every request, and if a client wants the server’s capability list up front it just calls server/discover explicitly instead of getting it as handshake output. And there’s a neat wrinkle: when a tool call needs more input mid-flight, the server can’t hold the connection open and ask like it used to, so it returns input_required with an opaque requestState string, the client gathers the answer and retries with that state echoed back. That’s continuation-passing — the same trick as a pagination cursor or a resumable upload token. The state didn’t disappear, it just moved from something the server holds to something the client carries.
Host: Right, so it’s not stateless in the sense of no state existing, it’s stateless in the sense of nobody’s pinned to a process holding it. What does that actually buy you in production, versus what it costs?
Guest: The GitHub tools server in the production example is the clean case — three replicas behind a load balancer, no sticky sessions, Mcp-Method used to rate-limit tools/call harder than tools/list, rolling deploys that don’t strand anyone’s in-flight work. Under the old handshake-based session, every one of those needed session affinity or a shared session store you had to operate and could leak. The cost is real but boring: repetition, those three _meta keys and the routing headers riding along on every request instead of being negotiated once, plus ttlMs and cacheScope on list results so clients know how long to trust a cached tools/list and whether it’s safe to share across users. For a remote server behind a load balancer that trade is obviously worth it; for a local stdio server that was never getting load-balanced anyway, it’s pure overhead the protocol now imposes uniformly.
4. The credential-placement trap: where the SDK almost fools you
Host: So here’s the gotcha you promised. Under this stateless model, every request is self-describing — protocol version, client info, capabilities, all riding in _meta on every call. So putting your own application credential in _meta right next to them feels natural, right? Per-request identity for a per-request protocol.
Guest: It feels natural and it’s wrong, and the way it breaks is instructive. The Python SDK’s call_tool() doesn’t just send your tools/call — internally it also calls validate_tool_result(), which fires off its own tools/list to check the output schema against what came back. Both requests get the SDK’s protocol _meta stamp, because it adds that to everything automatically, but call_tool() is the only one that accepts a meta= argument for your application credential — list_tools() has no such parameter, so there’s no path for your token to reach that internal call. Authorize on _meta and your server rejects its own client mid-request; the tempting fix, exempting tools/list from auth, just reopens the hole you built the credential check to close.
Host: Which is why the fix is boring by comparison — put it on the Authorization header instead, where the transport carries it on every request the SDK sends, whether your code issued that request or not. And that’s really the generalizable lesson: ‘stateless protocol’ and ‘per-request credential in the body’ sound like the same design, but only one of them survives an SDK making calls on your behalf.
5. Multi-tenancy makes the stakes concrete
Host: Okay, let’s put this in a real setting: one platform, many tenants, one MCP server instead of one per team. That consolidation sounds like the sane engineering move, so where does it actually bite?
Guest: It bites in two new places the old model never had to worry about. First, since there’s no session, identity has to be re-established on literally every request, so a single missed check on any one call exposes another tenant’s tools — not just at connect time. Second, cacheScope means a filtered tools/list is now something a gateway or CDN might legitimately cache and replay, and if you mark that response public instead of private, you’ve told every cache on the path it’s fine to serve tenant A’s tool list to tenant B. That’s not a server bug you’d ever see in logs — the server behaved correctly, the leak happens in infrastructure it never even touches.
Host: So filtering the list correctly isn’t the same as securing the tool. What else looks safe in a demo but isn’t a real boundary?
Guest: Two things, and both fail the same demo perfectly. If you only filter tools/list but don’t independently authorize tools/call, a tenant can just name a tool it never saw and invoke it — you’ve built a UI convention, not a boundary. And if a refusal is distinguishable from a not-found — different status code, different error text — a tenant can enumerate every other tenant’s capabilities one call at a time, which is exactly why refusals have to be byte-identical to ‘doesn’t exist’ and audited just as carefully as successes.
6. Deprecation timeline and the discipline of checking your source’s revision
Host: Let’s close with the practical checklist, because a lot of people listening are going to go look this up right after. What’s actually deprecated, and when does the clock really run out?
Guest: Roots, Sampling, Logging, and Dynamic Client Registration are deprecated now but get the full twelve-month cushion — they’re not eligible for removal until the first revision on or after 2027-07-28, so no rush there. HTTP+SSE is the one to worry about: it’s on the fast track, eligible for removal just three months after SEP-2596 reaches Final, so treat it as already gone for new work. And the last thing I’ll leave you with is the one habit that would’ve saved us this whole conversation — check which revision your source actually targets, because most MCP material online, including SDK tutorials, still describes 2025-11-25 with its handshake and session IDs, and quietly following it will build you a server for a protocol that no longer exists.
Not covered
The planner wanted these and found nothing in the source to support them:
- A deep comparison of MCP’s tool-calling mechanics against the general agent-loop control problem from Module 5 was considered but left out, since the source material treats them as parallel topics rather than directly integrating MCP tool schemas into the agent loop’s validation step.
- Specific benchmark numbers on latency or cost savings from correct ttlMs caching were not included, since no excerpt provides quantified before/after figures.
Generated from this page by Claude Sonnet 5 on , spoken by Kokoro-82M running locally. Two synthetic voices, not a recorded conversation. Every claim is drawn from this page — where it differs from the text above, the text is correct.
At a Glance
Section titled “At a Glance”A JSON-RPC protocol for exposing tools, resources, and prompts to a model host over a standard transport, so an integration written once works against any compliant client. The value is not the wire format — it is that the tool surface stops being bespoke per host.
The 2026-07-28 revision is a protocol rewrite, not a documentation refresh. The core is now
stateless. The initialize/notifications/initialized handshake and the Mcp-Session-Id header
are gone; every request instead carries its own protocol version, client info, and capabilities in
_meta. If your mental model of MCP starts with a handshake that establishes a session, that model
is a revision out of date.
The practical consequence is that a server can be an ordinary horizontally-scaled HTTP service. No sticky routing, no session affinity, no shared session store. Servers that genuinely need cross-call state now mint explicit handles and pass them as ordinary tool arguments, which makes that state visible in the tool schema instead of hidden in the transport.
Key Concepts
Section titled “Key Concepts”| Concept | What it does |
|---|---|
| Host / client / server | The host runs the model, the client speaks the protocol, the server owns the capability. Three parties, and the invocation decision belongs to a different one for each primitive |
| Tool | Model-invoked. The server declares a schema; the model chooses when to call it |
| Resource | Application-controlled context, addressed by URI. The host decides what to attach, not the model |
| Prompt | User-invoked template the server publishes |
| Stateless core | No initialize, no Mcp-Session-Id. Protocol version, client info, and capabilities ride in _meta on every request |
server/discover |
Returns supported versions, capabilities, and identity. Servers MUST implement it; clients MAY skip it and handle UnsupportedProtocolVersionError inline |
MCP-Protocol-Version |
Required header on every POST, and it MUST match the _meta value or the server returns 400 with HeaderMismatch |
Mcp-Method / Mcp-Name |
Required Streamable HTTP headers, so gateways, WAFs, and rate limiters route and authorize without parsing the JSON body |
x-mcp-header |
Tool-schema annotation mirroring a parameter into a request header. Clients MUST drop tools whose annotations are invalid |
| MRTR | Multi Round-Trip Requests. Servers return resultType: "input_required" with inputRequests; the client retries the original request carrying inputResponses |
resultType |
Required on all results: "complete" or "input_required". Results from older servers that omit it are treated as "complete" |
subscriptions/listen |
One long-lived POST-response stream for opted-in change notifications, replacing the HTTP GET endpoint and resources/subscribe |
ttlMs / cacheScope |
Cache hints servers MUST include on complete results from server/discover, the three list methods, resources/templates/list, and resources/read |
| Extensions | Capabilities versioned outside the spec core, namespaced. io.modelcontextprotocol/tasks is the first that matters |
| Tasks | Long-running work: tasks/get polling, tasks/update for client input. Now an extension, previously experimental core |
| Issuer-bound credentials | Clients MUST key persisted credentials by issuer, MUST NOT reuse them against another authorization server, and MUST re-register when it changes |
Numbers That Matter
Section titled “Numbers That Matter”| Quantity | Value | Why it matters |
|---|---|---|
| Current protocol revision | 2026-07-28 |
Previous revision was 2025-11-25 |
| Requests a compliant POST must carry | 3 headers + 3 _meta keys |
MCP-Protocol-Version, Mcp-Method, Mcp-Name (where applicable); protocolVersion, clientInfo, clientCapabilities |
| Header/body version mismatch | 400 + HeaderMismatch (-32020) |
The header and the _meta field are two places to get one value right |
| Minimum deprecation window | Twelve months from the revision that deprecates | Roots, Sampling, Logging, and Dynamic Client Registration are eligible for removal in the first revision released on or after 2027-07-28 |
| HTTP+SSE transport | Three months after SEP-2596 reaches Final | The one deprecation that does not get twelve months |
| Tier 1 SDKs | TypeScript, Python, C#, Go | Tier 2: Java, Rust, Ruby. Tier 3: Swift, PHP, Kotlin |
| SSE stream resumability | Removed | No Last-Event-ID, no event IDs. A broken stream loses the in-flight request |
| Cache scope values | "public" or "private" |
private keeps shared intermediaries from caching the response |
ttlMs absent |
Treat as 0 — immediately stale |
Also the correct reading of a negative value |
| Error code range reserved for MCP | -32020 to -32099 |
-32000–-32019 stays implementation-defined |
Common Gotchas
Section titled “Common Gotchas”- Teaching
Mcp-Session-Idas the default design is teaching the previous revision. Examples built on session establishment do not describe a2026-07-28server, and they push readers toward sticky routing they no longer need. - A stateless protocol does not mean a stateless application. Conversation, task, and tenant state still live somewhere — the change is that the protocol stopped carrying them. Servers needing cross-call state mint explicit handles passed as tool arguments.
server/discoveris mandatory for servers and optional for clients — the opposite way round from how “optional discovery” usually reads. A server that skips it is non-compliant even though most clients will never call it.- The protocol version appears twice and must agree. The
MCP-Protocol-Versionheader and theio.modelcontextprotocol/protocolVersionfield in_metaare validated against each other; a mismatch is a400, not a warning. ttlMsandcacheScopeare required, not advisory extras. A server omitting them on cacheable results is out of spec, and a client that ignores them re-fetchestools/listforever. Caching aprivateresponse in a shared intermediary is how one tenant sees another’s tool list.- A broken response stream loses the request. SSE resumability and message redelivery were
removed, so there is no
Last-Event-IDto resume from — the client must re-issue the work as a new request with a new ID. Anything non-idempotent behind that call needs its own protection. - Tasks moved out of the core, so “spec-compliant” no longer implies “supports tasks”. Extensions are negotiated separately, and a fully compliant server may implement none.
- Deprecated features do not all expire together. Roots, Sampling, Logging, and Dynamic Client Registration have until at least 2027-07-28. HTTP+SSE has three months from SEP-2596 reaching Final — plan for it to disappear first.
- Per-request metadata is not an authorization channel. On the official Python SDK, a
call_tool()for a tool the client has not listed yet produces two server-visible requests: thetools/call, and an internaltools/listissued byvalidate_tool_result()to fetch the output schema. The internal call carries the protocol_metastamp but not the caller’s application_meta— the SDK sends it itself, so nothing the caller passed reaches it, even thoughlist_tools()acceptsmeta=when the caller does the listing. A server authorizing on a credential in_metatherefore rejects its own client’s internal call, and exemptingtools/listto compensate reopens the hole. Authorization belongs on the transport, where every request carries it. See Module 6 → Security. Mcp-MethodandMcp-Nameare attacker-controlled input to your gateway. They make routing cheap; they do not make it trustworthy. The server still authorizes the actual call.
Where to Go Deeper
Section titled “Where to Go Deeper”- Module 6: MCP — the architecture in full: the three primitives, who owns each invocation decision, and the transport-level security argument.
- Enterprise MCP Platform — multi-tenant hosting, isolation, and what a gateway in front of many servers has to enforce.
multi-tenant-mcp-server— a real server on the official SDK, tested through the SDK’s own client over Streamable HTTP.- Module 5: Agent Engineering — what the model does with a tool surface once it has one.