I updated my editor last week and three MCP servers stopped working. Same error each time:
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found: initialize"}}
I spent about forty minutes assuming I'd broken something. I hadn't. Neither had the server authors. The spec changed underneath all of us.
If you got here from googling -32601 method not found mcp
or initialize method not found
or you're just staring at a server that worked fine on Friday, this is what happened.
MCP revision 2026-07-28
made the protocol stateless. Not "added a stateless mode". Made it stateless, and deleted the parts that assumed otherwise.
Gone:
initialize
and notifications/initialized
ping
logging/setLevel
resources/subscribe
and resources/unsubscribe
notifications/roots/list_changed
sampling/createMessage
, elicitation/create
and roots/list
no longer work the way they didEvery MCP server written before mid-2026 relies on at least the first item. That's why yours broke.
Here's the mapping, because the error text on its own is not very helpful.
-32601 method not found
on initialize
Your client is modern, your server is not. The client never sent a handshake because there is no handshake anymore. Instead it sends server/discover
, which your server has never heard of, and then the reverse happens: your server waits for an initialize
that never comes.
This is the single most common symptom.
-32602
with "missing required request metadata"
Because there's no session, every single request now has to carry its own context. It goes 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. Every request. If you're writing a client by hand and skipped this, that's your error.
-32022 unsupported protocol version
The version in that _meta
block didn't match. Worth knowing that the error codes got renumbered in this revision too, so old code checking for -32001
will silently stop matching. -32020
is header mismatch, -32021
is a missing client capability, -32022
is the version one.
-32021
and a requiredCapabilities
object
The server needed something the client didn't declare. Since capabilities now arrive per request instead of being negotiated once, a client that forgets to declare sampling
will get this the moment a tool tries to use it.
If your server calls back to the client mid-request (an elicitation prompt, a sampling call, asking for roots), it's now waiting forever. Modern clients cannot receive pushes. There's nothing listening.
This one is nastier than the others because there's no error at all. It just sits there.
This is the part I found genuinely clever, and it's worth understanding even if you never write a server.
Old world: server interrupts its own call, asks the client something, waits for an answer, continues.
New world: the server can't push, so it returns early with a result that says "I need input", and the client calls the same thing again with the answers attached.
{
"resultType": "input_required",
"inputRequests": {
"ir_1000": {
"method": "elicitation/create",
"params": { "message": "Which environment?" }
}
},
"requestState": "0f3a...e21"
}
The client answers by re-sending the original request with requestState
and an inputResponses
map keyed the same way. The call picks up where it left off. It's called a multi round-trip request, MRTR if you read the SEPs.
The bit that catches people: the values in inputResponses
are the response body itself, not wrapped in a result
field. I got that wrong the first three times.
resources/subscribe
is gone. Change notifications now live on a single long-lived stream you opt into, and you name the types you want:
{
"jsonrpc": "2.0",
"id": "listen-1",
"method": "subscriptions/listen",
"params": {
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}
Two things here are easy to miss. The request stays open, and its JSON-RPC id doubles as the subscription id that gets stamped on every notification. And the server must not send you anything you didn't ask for.
Progress and logging notifications have no home on that stream, by the way. They belong to an in-flight request, and a stateless request/response shape has nowhere to put them. If you were relying on notifications/progress
for a progress bar, that's a real loss and there isn't a workaround yet.
Three options, in the order I'd try them.
Update the server. If you wrote it, or it's actively maintained, this is the right answer. The official SDKs handle most of the migration for you, and the TypeScript SDK ships a compatibility shim so handlers written in the new style still serve old clients. Check if there's a newer version before doing anything else.
Pin your client. Buys you time, costs you everything else in the update. Fine for a week, bad as a plan.
Wrap the server. This is the option nobody talks about, and it's the one I needed, because two of my three broken servers hadn't been touched in over a year and one was a vendor binary I don't have source for. No amount of "just update it" helps there.
I ended up writing the wrapper, so treat the rest of this as biased. It's called mcp-uplift. You point your client at it instead of at the server:
npx -y mcp-uplift -- npx -y @modelcontextprotocol/server-filesystem .
It keeps one legacy session warm behind the scenes and does the translation: synthesizes server/discover
from the old handshake, turns server-initiated requests into input_required
round trips, filters legacy notifications onto a subscriptions/listen
stream, and answers the deleted methods itself instead of forwarding them.
The server doesn't change. It doesn't even know.
Fair question, since a protocol translator that's subtly wrong is worse than nothing.
I didn't trust my own test suite, because I wrote both sides of it and it only proves I'm internally consistent. So I ran it against servers I didn't write: 79 published legacy MCP packages, on a clean CI runner, checking the whole lifecycle each time. Discovery, subscription acknowledgement, the acknowledged filter never claiming a capability the server didn't declare, no response arriving while the stream is open, and a graceful close on shutdown.
79 reached discovery, zero protocol failures, 36 completed a full subscription lifecycle. The run is public if you want to read the log rather than take my word.
I deliberately dropped every package that needs an API key. They stop at the missing credential, never exercise the bridge, and only make the number look bigger. 79 real ones beat 100 with a fifth of them unreachable.
Because I'd rather you find out here than after adopting it.
Calls are serialized. A legacy server can interrupt any call to ask the client something, and the old protocol never linked that question back to the call that caused it, so only one call runs upstream at a time. Correct attribution, worse throughput.
Progress and logging notifications are dropped, for the reason above. Nothing to be done about that one.
Parked calls don't survive a restart, because each one is waiting on a child process that dies with the bridge.
And wrapping a server runs that server with your permissions. It's a compatibility layer, not a sandbox.
Roots, sampling and logging are deprecated with about twelve months of runway. That's the window. After it, "just update it" stops being optional and wrapping stops being a bridge and starts being life support.
Update what you can. Wrap what you can't. Don't pin your client and forget about it.
If you hit an error I didn't cover, drop it in the comments and I'll add it. The list above is from servers that actually broke, not from reading the changelog.