cd /news/developer-tools/your-mcp-server-didn-t-break-the-pro… · home topics developer-tools article
[ARTICLE · art-97203] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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

A developer reports that a recent MCP protocol revision (2026-07-28) made the protocol stateless, removing methods like initialize and ping, which breaks many existing MCP servers. The developer explains the new per-request metadata requirements and the multi-round-trip request pattern, offering a guide to common errors and fixes.

read6 min views1 publishedAug 14, 2026

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-mcp-server-didn…] indexed:0 read:6min 2026-08-14 ·