Dev.to · 7 min read

Your MCP server didn't break. The protocol did.

Your MCP server didn't break. The protocol did.

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. The short version 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 server-initiated requests entirely, which means sampling/createMessage, elicitation/create and roots/list no longer work the way they did Every MCP server written before mid-2026 relies on at least the first item. That's why yours broke. What the errors actually mean 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. Your server hangs and never answers 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. The replacement for server-initiated requests 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. Notifications moved too 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. So what do you actually do 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. Does it work 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. Things it can't do 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. The deadline 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.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Startup & VC News