The MCP 2026-07-28 revision makes the protocol stateless and breaks nearly every server built for the old one. One rule engine with four ways in: a web demo, an `npx` command, a GitHub Action, and an agent skill that does not stop at the diagnosis but works the migration rule by rule. The skill now installs as a Claude Code plugin served from the repository, so it cannot go stale against the rules it ships, and the CLI sits on npm as a package with an empty dependency tree. I pointed the same engine at 13,380 registered endpoints: 60.1% speak only the old protocol.
The problem
A protocol revision that removes sessions breaks servers quietly: everything works locally with one process, and every second request fails behind a load balancer. Anyone wanting to know whether it affects them reads a changelog with nine major entries and guesses.
Detection is only half of it. An agent pointed at the code will find matches but cannot tell which are real, and will happily refactor a `sessionId` that actually belongs to an Express session for an admin panel. So what was needed was not “a tool that emits findings” but a procedure that hands an agent the diagnosis, the triage and the order to work in.
And nobody had answered how bad it actually is. There are thousands of registered servers and no number saying how many the break really hits, so I counted them with the same engine. The first version of that count could not tell a maintained server that serves both eras from one that only speaks the old protocol. The report separates them now: 60.1% legacy only, 5.5% dual-era, and among the 6,191 servers anyone has touched since the revision shipped, 63.8%.
Constraints
The spec was four days old: almost no secondary sources, everything verified first-hand
The service fetches a URL typed in by a stranger
Cloudflare free tier: 10 ms of CPU per request
The skill, the CLI and the action run on machines where no npm install ever happened
A report about other people's servers that names none of them
Decisions
A skill with a procedure, not just a tool
A skill prescribing diagnosis, triage, remediation and verification
Shipping the checker and letting the agent take it from there
The official codemod covers only the SDK rename and says itself that adopting the protocol revision is “architectural and not codemod-automatable”. The scanner, in turn, works with regular expressions, so it reports signals rather than proof. An agent working the list unchecked will refactor false positives, and a change that was never needed costs more than the finding was worth. So the skill inserts a triage step that has to justify each match at its location before anything is edited. Then it fixes the order: SDK first, then the session state, then the handshake, otherwise you refactor twice. The engine travels with it as one dependency-free file, so the diagnosis step runs where no npm install ever happened; the CLI carries the same generated file.
Fixed rules instead of a language model
A rule engine, 13 rules and one HTTP probe
Letting a model read the code and judge
A model gives a different answer every run and no way to argue with it. The engine returns the same result for the same input and cites the spec page where you can prove it wrong. The model gets the part that genuinely needs judgement afterwards: deciding whether a finding is real, and doing the refactor. That determinism is what later made it possible to point the engine at 13,380 strangers' endpoints. With a model it would have been neither affordable nor reproducible.
Ask the modern question first, the legacy one second
The probe opens as a modern client; “still accepts legacy” costs zero points
Flagging every server that answers the old handshake
My first version of this rule fired on any endpoint that answered `initialize` and advised removing the handshake. That would have cut off every v1 client still pointed at the server, for no compliance gain: the spec explicitly permits a server to implement both behaviours. Worse, the probe itself only ever spoke as an old client, so it could not tell a maintained server serving both eras from a dead v1 one. It now asks the modern question first, explicitly refuses to read `-32601` as evidence (every JSON-RPC server emits it for an unknown method), and splits “still accepts legacy” from “only accepts legacy”. Only the second is a finding. A test pins down that the fix text never again says to remove the handshake.
Key on the package name, not the version number
The rule fires as soon as @modelcontextprotocol/sdk appears at all
Comparing the version against a 2.0 threshold
The original rule did exactly that and recommended `@modelcontextprotocol/sdk@^2`, a version that has never existed. v2 shipped as a rename to @modelcontextprotocol/server and /client, which makes a version comparison meaningless; the package name is the signal. My first correction overshot and deleted the rule outright, because a rename looks identical to “there is no v2” from the outside. Two tests now pin both mistakes down. Go turned out to be the opposite case: there the break landed on a minor bump from v1.6.1 to v1.7.0, same module path, no /v2. So the Go rule compares full release triples and never advises changing an import path. There is no one signal that holds for all four SDKs.
Better to check nothing than to report a clean nothing
C# is documented as unscanned, with a pointer to the live probe
Adding a shallow C# rule so the list looks complete
The scanner does not read `.cs` files and there is no C# SDK rule. A source scan of a C# server therefore reports nothing, and that nothing reads like a clean result. Of the two failure modes that is the dangerous one: a finding you can disprove costs a minute, a green result that checked nothing costs you the migration. On top of that the C# SDK moved to a 2.x of the `ModelContextProtocol` packages, so the TypeScript and Python advice would not carry over anyway. So the README says plainly that C# is not scanned, and points at the path that needs no language at all: the live probe only speaks HTTP.
Count them, do not name them
The published report counts servers; the names stay in a local file
A public league table of broken servers
A list naming broken servers would be a different project with a different ethics, and it would alienate exactly the maintainers this tool exists to help. The second decision was the uncomfortable one: answering is not the same as being migrated. The probe sets `reachable` on any HTTP response, which is right for the one server you run yourself. Across a few thousand strangers it is wrong: a 403 from a WAF, a 404 from a moved path and a captive proxy all answer with something that is not MCP, and scored naively they came back as a clean A. The report would have claimed the exact opposite of the truth. Those cases now sit in a bucket of their own, outside the denominator.
What I built
Four surfaces over one core: demo, npx CLI, GitHub Action, agent skill
13 rules across TypeScript, Python, Rust and Go, each citing the spec page that could disprove it
State of MCP: 13,380 endpoints probed, 10,890 gradable, 60.1% legacy protocol only
Over 170 test cases; the engine is generated, never copied, and CI fails the moment a copy drifts