Protocols · 7 min
Your MCP server didn't break. The protocol did.
Mohib Uddin · August 16, 2026
I updated my editor and three MCP servers stopped answering. Every one of them printed the same thing:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found: initialize"
}
}
I spent about forty minutes looking for what I had broken. Nothing. The servers were fine, the authors were fine, and the config had not changed. The spec had.
Every migration writeup I could find explains the change from the spec's side:
here is what 2026-07-28 altered, here is the rationale, here is the new shape.
That is the right document to read second. The one I wanted first was the one
that starts from the error on the screen, because at that point I did not know
a revision had happened at all.
Seven methods were deleted in a single revision
2026-07-28 made MCP stateless. Not "added a stateless mode" — the handshake,
the session, and the server's ability to talk back all went away together,
because none of them mean anything without state to attach them to.
initialize, notifications/initialized, ping, logging/setLevel,
notifications/roots/list_changed, resources/subscribe, and
resources/unsubscribe. Server-initiated requests went with them, which takes
sampling/createMessage, elicitation/create and roots/list out of the shape
they used to have.
Any server written before mid-2026 depends on at least the first of those. That is the whole explanation for most of what follows.
-32601 on initialize means your client moved and your server didn't
This is the common one, and it reads backwards. The error says method not
found, which sounds like the server is missing something. It isn't. The client
stopped sending a handshake, because there is no handshake to send. It sends
server/discover instead, which your server has never heard of, and meanwhile
your server sits waiting for an initialize that is never coming.
Two programs, each waiting for the other to speak a language it retired.
-32602 means you dropped the envelope
With no session, there is nowhere to keep the context a session used to hold. So
every request carries its own, in params._meta:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "roots": {} }
}
}
}
Protocol version and client capabilities. On every request, not once at startup. If you are writing a client by hand and skipped it, this is your error, and it will look like a params validation bug rather than a protocol one.
-32022 and -32021 are the same mistake at different depths
-32022 is a version mismatch: the string in that _meta block is not one the
server serves. -32021 is a capability the server needed and the client never
declared, and it arrives with a requiredCapabilities object naming what was
missing.
Worth knowing that the codes themselves were renumbered in this revision. Old
code branching on -32001 will stop matching and fail open, quietly. The
current set is -32020 for a header mismatch, -32021 for the missing
capability, -32022 for the version.
The one with no error at all
If your server calls back to the client in the middle of handling a request — an elicitation prompt, a sampling call, asking for roots — it now waits forever. Modern clients have no channel to receive that on. Nothing is listening and nothing will time out on your behalf.
This is the worst of them, because there is no code to search for. The call just never returns, and a hang looks like a network problem or a slow tool.
What replaced the push
The replacement is the most interesting part of the revision. The server cannot interrupt you, so instead it returns early with a result that says it needs something first:
{
"resultType": "input_required",
"inputRequests": {
"ir_1000": {
"method": "elicitation/create",
"params": { "message": "Which environment?" }
}
},
"requestState": "0f3a...e21"
}
You send the same request again with that requestState and an
inputResponses map keyed to match. The call resumes and finishes. A round trip
the old protocol did with a push, the new one does with a resumption.
The detail that cost me three attempts: the values in inputResponses are the
response body itself, not wrapped in a result field. The schema examples in
the spec repo are the only place that is unambiguous, and I had been working
from the prose.
Notifications moved the same way. resources/subscribe is gone, replaced by a
long-lived subscriptions/listen stream you opt into by naming the types you
want. Exactly four notification types have a home on it. Progress and logging
are not among them, because they belong to an in-flight request rather than to a
stream, and a stateless request/response shape has nowhere to put them.
Three things you can do, in the order I would try them
Update the server. If you wrote it or it is actively maintained, this is the right answer and everything else on this list is worse. The official SDKs carry most of the migration, and the TypeScript SDK ships a shim so handlers written in the new style still serve old clients. Check for a newer version before doing anything else.
Pin the client. Buys a week. Costs you every other fix in that release. Not a plan.
Wrap the server. This is the case nobody writes about, and it is the one I had: a server nobody maintains, and a vendor binary I have no source for. No amount of "just update it" reaches either.
So I wrote the wrapper. mcp-uplift launches the legacy server, keeps one
session warm against it, and presents that session as a stateless modern one:
server/discover synthesized from the old handshake, server-initiated requests
turned into input_required round trips, legacy notifications filtered onto a
subscriptions/listen stream, deleted methods answered rather than forwarded.
npx -y mcp-uplift -- npx -y @modelcontextprotocol/server-filesystem .
The server does not change. It does not know.
What this doesn't tell you
The error list above came from servers that actually broke on my machine, plus the spec repo's own schema examples. It is not exhaustive. There are failure modes in this revision I have not hit, and the absence of a code from that list means only that I did not personally trip over it.
The wrapper has been run against 79 published legacy servers written by other people, with no protocol failures and 36 completing a full subscription lifecycle. That number is worth what it is worth and no more: it says the translation holds across a wide sample of real servers, not that it will hold against yours. I wrote about why I stopped trusting my own fixtures and what the sample does and doesn't cover.
It also has real limits. Calls are serialized, one at a time upstream, because the legacy protocol never linked a server's question back to the call that provoked it. Parked calls die with the process. Progress and logging notifications are dropped outright. And wrapping a server runs that server with your permissions, which makes this a compatibility layer and not a sandbox.
The part worth keeping
Roots, sampling and logging are deprecated with roughly twelve months of runway. That is the window in which wrapping is a bridge. After it, wrapping is life support for software nobody is maintaining, which is a different and worse thing to be running.
The transferable bit is smaller than the migration, though. When a protocol removes a capability, the failure almost never names the removal. It surfaces as method not found, or a params error, or a hang, and every one of those reads as your bug. Forty minutes of my life went into looking for a mistake I had not made. If you maintain something that speaks a versioned protocol, the most useful thing you can write for your users is not the changelog. It is the list of what your absence looks like from the other end.