I built a Skill and checker for MCP's breaking change 2026-08-26. Then the checker was wrong about it. A developer built a seven-rule checker for the Model Context Protocol's breaking change of 2026-07-28, but one rule gave wrong advice because it checked the version of @modelcontextprotocol/sdk, which stops at 1.30.0 and has no 2.x. The v2 packages shipped under different names, such as @modelcontextprotocol/server and @modelcontextprotocol/client, so the checker's recommendation to upgrade to @modelcontextprotocol/sdk ^2 was impossible. The developer initially deleted the rule and propagated a wrong correction, but later discovered the codemod package and fixed the rule to key on the presence of the v1 package name. If you are migrating an MCP server right now, here is the thing that will cost you an afternoon, before anything else in this post: @modelcontextprotocol/sdk has no 2.x. It never will. It stops at 1.30.0 . If you go looking for @modelcontextprotocol/sdk@^2 you will find nothing and conclude that v2 has not shipped yet. It has — on 2026-07-27, under different names: @modelcontextprotocol/server 2.0.0 @modelcontextprotocol/client 2.0.0 @modelcontextprotocol/core 2.0.0 @modelcontextprotocol/node 2.0.0 @modelcontextprotocol/express 2.0.0 ┐ @modelcontextprotocol/fastify 2.0.0 ├ HTTP adapters @modelcontextprotocol/hono 2.0.0 ┘ @modelcontextprotocol/server-legacy 2.0.0 compat shim @modelcontextprotocol/codemod 2.0.0 I know this because my own tool told people the opposite, with total confidence. The MCP revision dated 2026-07-28 is the largest change the protocol has had. The short version: initialize / notifications/initialized handshake is gone. Every request now carries its protocol version and client capabilities in meta . Mcp-Session-Id is gone server/discover is a MUST. roots/list , sampling/createMessage , elicitation/create are replaced by InputRequiredResult , the client retries with the answers. logging , sampling and roots are The second point is the one that hurts. If you keep anything in a Map keyed by session id, it works perfectly on your laptop with one process and fails intermittently the moment two instances sit behind a load balancer. It fails quietly, which is the worst way to fail. So I built a checker: seven rules, a deterministic engine, no model in the loop. Point it at a running endpoint or a repository and it tells you what breaks, with the spec page for each finding so you can prove it wrong. It ships as an agent skill that does the migration too, which I will come back to — because the scanner on its own turned out to be the smaller half. One rule, MCP007, fired when @modelcontextprotocol/sdk resolved below 2.0.0 and said: Upgrade to @modelcontextprotocol/sdk ^2 . Run the official v1→v2 codemod for the mechanical renames, then re-check. Both halves of that are wrong, and they are wrong in different ways. @modelcontextprotocol/sdk@^2 does not resolve. There has never been a 2.x of that package. So the tool was confidently recommending a version that does not exist - to people who would then spend twenty minutes wondering what they were doing wrong. I checked npm, found 1.30.0 as the latest and no 2.x anywhere in the version list, read the SDK announcement, saw no major version named, and concluded the rule was fabricated wholesale. So I deleted it, wrote a comment explaining that no 2.x line existed and no codemod existed, and - this is the part that stings - propagated that "correction" into the README, the skill and the remediation guide. I had replaced a wrong statement with a differently wrong statement. Look at what I actually checked. I checked the package the rule named . It ends at 1.30.0. From inside that one package, these two situations are indistinguishable: there is no v2 yet v2 exists under a different name A rename produces exactly the evidence you would expect from absence. Checking harder in the same place does not help — the answer is not there. I found it only when I went looking for the codemod , which turned out to exist as its own package, and whose description read "Codemod to migrate MCP TypeScript SDK code from v1 to v2". A codemod for a v2 that does not exist would be a strange thing to publish. The rule now keys on the presence of the v1 package rather than on a version threshold, because the package name is the actual signal: // The package name IS the v1 line. It stops at 1.30.0 and speaks the // pre-2026-07-28 protocol. v2 shipped under different names entirely, // so a version comparison here is meaningless. if ctx.source?.sdkVersion return null; And a test now pins the first mistake down permanently: js test "MCP007 names the real replacement packages and the real codemod", = { const f = evaluate { source: withSdk "^1.17.0" } .find x = x.ruleId === "MCP007" ; assert.ok f.fix.includes "@modelcontextprotocol/server" ; assert.ok f.fix.includes "@modelcontextprotocol/client" ; assert.ok f.fix.includes "@modelcontextprotocol/codemod@latest v1-to-v2" ; assert.ok /@modelcontextprotocol\/sdk @^ \^?2/.test f.fix , "must not advise upgrading @modelcontextprotocol/sdk to 2.x — no such release", ; } ; The positive assertions matter as much as the negative one. A test that only forbids the wrong package name would pass on a rule that names nothing at all — which is roughly what my deletion produced. Every rule cites a spec section, so a user can check the tool rather than trust it. All seven of those links looked like this: https://modelcontextprotocol.io/specification/2026-07-28 lifecycle https://modelcontextprotocol.io/specification/2026-07-28 transport https://modelcontextprotocol.io/specification/2026-07-28 authorization The specification is split across subpages. It has no such anchors. Every one of those links resolved silently to the overview page — no 404, no broken-link warning, just a page that is not the one being cited. A "verify this against the spec" feature that quietly cannot be verified is worse than not having it, because it buys credibility it has not earned. There is now a test for that too: js test "no specRef relies on a page anchor", = { for const rule of rules { assert.ok rule.specRef.startsWith "https://" ; assert.ok rule.specRef.includes " " , ${rule.id} specRef relies on an anchor: ${rule.specRef} , ; } } ; A second one asserts that any link into the spec site points at a subpage rather than the revision root, so a future rule cannot quietly cite the front page again. Two things I would take to any project, MCP or not. Deterministic is not the same as correct. The whole selling point of this engine is that it is reproducible: same input, same output, no model deciding things differently on Tuesday. That is a real property and it is worth having. It also gave me a rule that was reliably, repeatably, verifiably wrong. Determinism buys you the ability to argue with the output. It does not buy you truth. "No result" and "you looked in the wrong place" produce identical evidence. This is the one I keep thinking about. Absence of a thing at the address you checked is not absence of the thing. The rename is a clean example, but the shape turns up everywhere: a config key that moved, an endpoint that was versioned, a function that was extracted to another module. When a check comes back negative and the negative result is surprising , the next question should be "am I looking in the right place" before "so it does not exist". I write more agent-driven code than I used to, and the failure mode has shifted accordingly. It is no longer mostly syntax. It is confident, plausible, internally consistent statements about an ecosystem, that happen not to be true. The defence is not to trust less, it is to make claims checkable and then actually check them — which is why every rule cites a page, and why the links being broken mattered more than it first looked. Here is the thing I got wrong about my own tool before I got the SDK wrong. The source scan is regex-based. It reports signals , not proof. Point an agent at the output and tell it to fix what it finds, and sooner or later it will hit something like this: app.use session { secret: process.env.ADMIN SESSION SECRET } ; app.get "/admin/whoami", req, res = { const sessionId = req.sessionID; // ← MCP002 fires here res.json { sessionId, user: req.session.user } ; } ; app.post "/mcp", async req, res = { const transport = new StreamableHTTPServerTransport { sessionIdGenerator: undefined, // ← already stateless. Nothing to do. } ; // … } ; That is an Express session for an admin panel, on a server whose MCP transport is already stateless. The rule is right that the string is there. Acting on it means refactoring something that was never broken — and a change nobody needed is more expensive than the finding was worth. So what actually ships is a procedure, not a scanner: file:line before touching anything, and say out loud which ones are noise. This step exists because of the code above.The rule ids are what ties it together. The checker emits MCP002 , and the remediation guide has a section keyed MCP002 explaining how to tell a real session dependency from an Express one. Diagnosis and fix are not two documents that drift. Worth being precise about, because "works with every agent" is the kind of confident claim this whole post is about not making. The .skill format — a SKILL.md with frontmatter that an agent auto-invokes when the topic comes up — is Claude's. In Claude Code or Claude.ai you install the file and ask it to migrate a server; the description field does the rest. The contents are not Claude's. A .skill file is a zip: mcp-migration/ ├── SKILL.md the procedure, plain markdown ├── references/remediation.md per-rule guidance, keyed by id └── scripts/mcpcheck.mjs the engine, one file, zero dependencies mcpcheck.mjs is an esbuild bundle of the rule engine that needs nothing but Node. So for Codex , Cursor , or anything else that can run a shell command: unzip mcp-migration.skill node mcp-migration/scripts/mcpcheck.mjs --source ./my-server --json node mcp-migration/scripts/mcpcheck.mjs https://example.com/mcp Exit codes are CI-friendly: 0 no critical findings, 1 at least one, 2 inconclusive. references/remediation.md reads fine on its own, or drop it next to an AGENTS.md so whatever agent you use has the same triage rules Claude gets. The reason it is bundled rather than published as a dependency is exactly this: the skill lands on machines where no npm install ever ran, and an import would just fail there. The cost is a generated file committed to the repo, which can go stale — so CI rebuilds it on every push and fails if it differs. The codemod is real and handles the mechanical half: npx @modelcontextprotocol/codemod@latest v1-to-v2 . Run it on a clean tree. It rewrites import paths, symbol renames McpError → ProtocolError , StreamableHTTPError → SdkHttpError , setRequestHandler Schema, … → setRequestHandler 'method/string', … , .tool → registerTool , and extra. → ctx.mcpReq. . Where it cannot decide safely it leaves a marker instead of guessing: grep -rn '@mcp-codemod-error' . Note that v2 requires Zod 4 , so a project on Zod 3 has a second upgrade in front of it. And then the part that is not mechanical, in the codemod's own words: The codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28 protocol revision createMcpHandler , multi-round-trip requests, versionNegotiation is architectural and not codemod-automatable. That is accurate and worth taking seriously. Removing session state is a design decision — delete it, pass it as an explicit tool argument, or move it to a store both instances can reach. No tool makes that call for you, and the deprecated capabilities can wait twelve months while you do. All of it is open source, MIT: The README has a section called A rule that was wrong . It says roughly what this post says, in fewer words, and it stays there.