Protocol revision 2026-07-28
Consuming an MCP Server You Did Not Write
Version negotiation without a handshake, detecting whether a server is modern or legacy, timeouts, cancellation, and the trust boundary you cross by connecting.
Writing a server is the easy half: you decide what it does. Writing a client means dealing with servers you did not write, running revisions you did not choose, on a protocol that just removed its handshake.
Version negotiation, without a negotiation
Every request declares its protocol version. If the server does not implement it, the answer names what it does support.
client tools/list with _meta.protocolVersion = "2026-07-28"
server -32022 Unsupported protocol version
data.supported = ["2025-11-25"]
client picks a mutually supported version and retriesCalling server/discover first is optional and often worth it anyway, because one round trip gets you supported versions, capabilities and identity instead of three separate list calls.
Telling the two eras apart
A modern server wants per-request metadata. A legacy one, 2025-11-25 or earlier, wants an initialize handshake first. There is no field that announces which you are talking to, so detection is a probe, and it differs by transport.
stdio: send server/discover first
DiscoverResult -> modern, continue
recognised modern error -> modern, retry with a supported version
anything else, or silence -> legacy, fall back to initialize
HTTP: send a modern request
400 with a modern JSON-RPC error body -> modern
400/404/405 without one -> legacy, fall backTwo details make this less obvious than it looks. First, the fallback must not key on a specific error code: a legacy server meeting an unknown method may answer -32601, or -32602, or nothing at all. Second, and worse: some legacy servers do not check that a request arrived after initialize, so a bare tools/call may be cheerfully executed under legacy semantics. Probing turns that ambiguity into a clean failure, which is why it is recommended even for clients that only speak the modern protocol.
The era is a property of the server, not of a request. Cache it for the life of the process or origin, and re-probe only if the assumption starts failing.
What the client owes the user
The protocol cannot enforce consent, so the client is the only place it can live. Three obligations fall out of that.
Approval before invocation. A tool call is arbitrary code with the server's privileges. The user should know what is about to run and be able to say no.
Show what changed. Tool lists are not static, and a server can change a tool's description after you approved it. A client that caches a list and never revalidates will not notice; one that revalidates and never re-prompts is worse, because it has laundered the change.
Keep servers apart. Output from one server lands in the same context window as another server's tool definitions. A malicious server does not need access to your filesystem if it can put text in front of a model that is holding a filesystem tool.
Timeouts and cancellation
Nothing in the protocol bounds how long a tool may take, so the client sets the limit. On Streamable HTTP, closing the response stream is the cancellation and needs no message. On stdio there is a single shared channel with nothing to close, so send notifications/cancelled and stop waiting.
Both cases share one rule worth writing down: after cancelling, discard anything that arrives for that request. A server that keeps working and answers late is misbehaving, but a client that accepts the late answer has a bug of its own.