<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alpay Celik</title>
    <description>The latest articles on DEV Community by Alpay Celik (@vertigo91).</description>
    <link>https://dev.to/vertigo91</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1202575%2F0a312b88-8ff8-446b-96f9-ef49d2650395.jpeg</url>
      <title>DEV Community: Alpay Celik</title>
      <link>https://dev.to/vertigo91</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vertigo91"/>
    <language>en</language>
    <item>
      <title>I built a Skill and checker for MCP's breaking change 2026-08-26. Then the checker was wrong about it.</title>
      <dc:creator>Alpay Celik</dc:creator>
      <pubDate>Sun, 02 Aug 2026 01:41:26 +0000</pubDate>
      <link>https://dev.to/vertigo91/i-built-a-skill-and-checker-for-mcps-breaking-change-2026-08-26-then-the-checker-was-wrong-about-1kb4</link>
      <guid>https://dev.to/vertigo91/i-built-a-skill-and-checker-for-mcps-breaking-change-2026-08-26-then-the-checker-was-wrong-about-1kb4</guid>
      <description>&lt;p&gt;If you are migrating an MCP server right now, here is the thing that will cost you&lt;br&gt;
an afternoon, before anything else in this post:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; has no 2.x. It never will.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It stops at &lt;code&gt;1.30.0&lt;/code&gt;. If you go looking for &lt;code&gt;@modelcontextprotocol/sdk@^2&lt;/code&gt; you&lt;br&gt;
will find nothing and conclude that v2 has not shipped yet. It has — on&lt;br&gt;
2026-07-27, under different names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know this because my own tool told people the opposite, with total confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;The MCP revision dated &lt;strong&gt;2026-07-28&lt;/strong&gt; is the largest change the protocol has had.&lt;br&gt;
The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The transport is &lt;strong&gt;stateless&lt;/strong&gt;. The &lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;notifications/initialized&lt;/code&gt;
handshake is gone. Every request now carries its protocol version and client
capabilities in &lt;code&gt;_meta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Mcp-Session-Id&lt;/code&gt; is gone&lt;/strong&gt; from Streamable HTTP. There are no protocol-level
sessions any more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/discover&lt;/code&gt; is a MUST.&lt;/strong&gt; Servers have to advertise their supported
protocol versions and capabilities over it.&lt;/li&gt;
&lt;li&gt;Server-initiated requests (&lt;code&gt;roots/list&lt;/code&gt;, &lt;code&gt;sampling/createMessage&lt;/code&gt;,
&lt;code&gt;elicitation/create&lt;/code&gt;) are replaced by &lt;strong&gt;Multi Round-Trip Requests&lt;/strong&gt;: the server
returns an &lt;code&gt;InputRequiredResult&lt;/code&gt;, the client retries with the answers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;logging&lt;/code&gt;, &lt;code&gt;sampling&lt;/code&gt; and &lt;code&gt;roots&lt;/code&gt; are &lt;strong&gt;deprecated, not removed&lt;/strong&gt; - twelve-month
minimum window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second point is the one that hurts. If you keep anything in a &lt;code&gt;Map&lt;/code&gt; keyed by&lt;br&gt;
session id, it works perfectly on your laptop with one process and fails&lt;br&gt;
intermittently the moment two instances sit behind a load balancer. It fails&lt;br&gt;
quietly, which is the worst way to fail.&lt;/p&gt;

&lt;p&gt;So I built a checker: seven rules, a deterministic engine, no model in the loop.&lt;br&gt;
Point it at a running endpoint or a repository and it tells you what breaks, with&lt;br&gt;
the spec page for each finding so you can prove it wrong. It ships as an agent&lt;br&gt;
skill that does the migration too, which I will come back to — because the&lt;br&gt;
scanner on its own turned out to be the smaller half.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then my own rule was wrong
&lt;/h2&gt;

&lt;p&gt;One rule, MCP007, fired when &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; resolved below &lt;code&gt;2.0.0&lt;/code&gt;&lt;br&gt;
and said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upgrade to &lt;code&gt;@modelcontextprotocol/sdk ^2&lt;/code&gt;. Run the official v1→v2 codemod for&lt;br&gt;
the mechanical renames, then re-check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both halves of that are wrong, and they are wrong in different ways.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@modelcontextprotocol/sdk@^2&lt;/code&gt; does not resolve. There has never been a 2.x of&lt;br&gt;
that package. So the tool was confidently recommending a version that does not&lt;br&gt;
exist - to people who would then spend twenty minutes wondering what they were doing wrong.&lt;/p&gt;

&lt;p&gt;I checked npm, found &lt;code&gt;1.30.0&lt;/code&gt; as the latest and no 2.x anywhere in the version&lt;br&gt;
list, read the SDK announcement, saw no major version named, and concluded the&lt;br&gt;
rule was fabricated wholesale. So I deleted it, wrote a comment explaining that no&lt;br&gt;
2.x line existed and no codemod existed, and - this is the part that stings -&lt;br&gt;
propagated that "correction" into the README, the skill and the remediation guide.&lt;/p&gt;

&lt;p&gt;I had replaced a wrong statement with a differently wrong statement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the second mistake was so easy
&lt;/h2&gt;

&lt;p&gt;Look at what I actually checked. I checked &lt;em&gt;the package the rule named&lt;/em&gt;. It ends&lt;br&gt;
at 1.30.0. From inside that one package, these two situations are indistinguishable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;there is no v2 yet
v2 exists under a different name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rename produces exactly the evidence you would expect from absence. Checking&lt;br&gt;
harder in the same place does not help — the answer is not there. I found it only&lt;br&gt;
when I went looking for the &lt;em&gt;codemod&lt;/em&gt;, which turned out to exist as its own&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;The rule now keys on the presence of the v1 package rather than on a version&lt;br&gt;
threshold, because the package name is the actual signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The package name IS the v1 line. It stops at 1.30.0 and speaks the&lt;/span&gt;
&lt;span class="c1"&gt;// pre-2026-07-28 protocol. v2 shipped under different names entirely,&lt;/span&gt;
&lt;span class="c1"&gt;// so a version comparison here is meaningless.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sdkVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a test now pins the first mistake down permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MCP007 names the real replacement packages and the real codemod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;withSdk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^1.17.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruleId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MCP007&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/codemod@latest v1-to-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/@modelcontextprotocol&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;sdk&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;@^ &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\^?&lt;/span&gt;&lt;span class="sr"&gt;2/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;must not advise upgrading @modelcontextprotocol/sdk to 2.x — no such release&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The positive assertions matter as much as the negative one. A test that only&lt;br&gt;
forbids the wrong package name would pass on a rule that names nothing at all —&lt;br&gt;
which is roughly what my deletion produced.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other thing I found while I was in there
&lt;/h2&gt;

&lt;p&gt;Every rule cites a spec section, so a user can check the tool rather than trust it.&lt;br&gt;
All seven of those links looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://modelcontextprotocol.io/specification/2026-07-28#lifecycle
https://modelcontextprotocol.io/specification/2026-07-28#transport
https://modelcontextprotocol.io/specification/2026-07-28#authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specification is split across subpages. It has no such anchors. Every one of&lt;br&gt;
those links resolved silently to the overview page — no 404, no broken-link&lt;br&gt;
warning, just a page that is not the one being cited. A "verify this against the&lt;br&gt;
spec" feature that quietly cannot be verified is worse than not having it, because&lt;br&gt;
it buys credibility it has not earned.&lt;/p&gt;

&lt;p&gt;There is now a test for that too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no specRef relies on a page anchor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;specRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;specRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; specRef relies on an anchor: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;specRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second one asserts that any link into the spec site points at a subpage rather&lt;br&gt;
than the revision root, so a future rule cannot quietly cite the front page again.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that generalises
&lt;/h2&gt;

&lt;p&gt;Two things I would take to any project, MCP or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic is not the same as correct.&lt;/strong&gt; The whole selling point of this&lt;br&gt;
engine is that it is reproducible: same input, same output, no model deciding&lt;br&gt;
things differently on Tuesday. That is a real property and it is worth having.&lt;br&gt;
It also gave me a rule that was reliably, repeatably, verifiably wrong. Determinism&lt;br&gt;
buys you the ability to &lt;em&gt;argue&lt;/em&gt; with the output. It does not buy you truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"No result" and "you looked in the wrong place" produce identical evidence.&lt;/strong&gt;&lt;br&gt;
This is the one I keep thinking about. Absence of a thing at the address you&lt;br&gt;
checked is not absence of the thing. The rename is a clean example, but the shape&lt;br&gt;
turns up everywhere: a config key that moved, an endpoint that was versioned, a&lt;br&gt;
function that was extracted to another module. When a check comes back negative and the negative result is &lt;em&gt;surprising&lt;/em&gt;, the next question should be "am I looking in&lt;br&gt;
the right place" before "so it does not exist".&lt;/p&gt;

&lt;p&gt;I write more agent-driven code than I used to, and the failure mode has shifted&lt;br&gt;
accordingly. It is no longer mostly syntax. It is confident, plausible, internally&lt;br&gt;
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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the scanner is the smaller half
&lt;/h2&gt;

&lt;p&gt;Here is the thing I got wrong about my own tool before I got the SDK wrong.&lt;/p&gt;

&lt;p&gt;The source scan is regex-based. It reports &lt;em&gt;signals&lt;/em&gt;, not proof. Point an agent at&lt;br&gt;
the output and tell it to fix what it finds, and sooner or later it will hit&lt;br&gt;
something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN_SESSION_SECRET&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/admin/whoami&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// ← MCP002 fires here&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPServerTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sessionIdGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ← already stateless. Nothing to do.&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an Express session for an admin panel, on a server whose MCP transport is&lt;br&gt;
already stateless. The rule is right that the string is there. Acting on it means&lt;br&gt;
refactoring something that was never broken — and a change nobody needed is more expensive than the finding was worth.&lt;/p&gt;

&lt;p&gt;So what actually ships is a procedure, not a scanner:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose&lt;/strong&gt; — run the bundled deterministic checker, both source scan and live
probe where a server is running. They see different things and neither is a
superset of the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; — justify every finding at its &lt;code&gt;file:line&lt;/code&gt; before touching anything,
and say out loud which ones are noise. This step exists because of the code
above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remediate&lt;/strong&gt; — in dependency order: SDK first (so you refactor against the API
you keep), then session state, then the handshake, then the deprecations, then
the OAuth posture. Re-run after each, so you can tell which change fixed what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — including the thing no static check can answer: does the server
still behave when two consecutive requests land on &lt;em&gt;different instances&lt;/em&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule ids are what ties it together. The checker emits &lt;code&gt;MCP002&lt;/code&gt;, and the&lt;br&gt;
remediation guide has a section keyed &lt;code&gt;MCP002&lt;/code&gt; explaining how to tell a real&lt;br&gt;
session dependency from an Express one. Diagnosis and fix are not two documents&lt;br&gt;
that drift.&lt;/p&gt;
&lt;h2&gt;
  
  
  It is a Claude skill. The payload is not Claude-specific.
&lt;/h2&gt;

&lt;p&gt;Worth being precise about, because "works with every agent" is the kind of&lt;br&gt;
confident claim this whole post is about not making.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.skill&lt;/code&gt; format — a &lt;code&gt;SKILL.md&lt;/code&gt; with frontmatter that an agent auto-invokes when the topic comes up — is Claude's. In &lt;strong&gt;Claude Code&lt;/strong&gt; or Claude.ai you install the file and ask it to migrate a server; the description field does the rest.&lt;/p&gt;

&lt;p&gt;The contents are not Claude's. A &lt;code&gt;.skill&lt;/code&gt; file is a zip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mcpcheck.mjs&lt;/code&gt; is an esbuild bundle of the rule engine that needs nothing but&lt;br&gt;
Node. So for &lt;strong&gt;Codex&lt;/strong&gt;, &lt;strong&gt;Cursor&lt;/strong&gt;, or anything else that can run a shell command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip mcp-migration.skill
node mcp-migration/scripts/mcpcheck.mjs &lt;span class="nt"&gt;--source&lt;/span&gt; ./my-server &lt;span class="nt"&gt;--json&lt;/span&gt;
node mcp-migration/scripts/mcpcheck.mjs https://example.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit codes are CI-friendly: &lt;code&gt;0&lt;/code&gt; no critical findings, &lt;code&gt;1&lt;/code&gt; at least one, &lt;code&gt;2&lt;/code&gt;&lt;br&gt;
inconclusive. &lt;code&gt;references/remediation.md&lt;/code&gt; reads fine on its own, or drop it next to&lt;br&gt;
an &lt;code&gt;AGENTS.md&lt;/code&gt; so whatever agent you use has the same triage rules Claude gets.&lt;/p&gt;

&lt;p&gt;The reason it is bundled rather than published as a dependency is exactly this: the&lt;br&gt;
skill lands on machines where no &lt;code&gt;npm install&lt;/code&gt; ever ran, and an import would just&lt;br&gt;
fail there. The cost is a generated file committed to the repo, which can go stale&lt;br&gt;
— so CI rebuilds it on every push and fails if it differs.&lt;/p&gt;
&lt;h2&gt;
  
  
  The practical bit, if you are migrating
&lt;/h2&gt;

&lt;p&gt;The codemod is real and handles the mechanical half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/codemod@latest v1-to-v2 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on a clean tree. It rewrites import paths, symbol renames&lt;br&gt;
(&lt;code&gt;McpError&lt;/code&gt; → &lt;code&gt;ProtocolError&lt;/code&gt;, &lt;code&gt;StreamableHTTPError&lt;/code&gt; → &lt;code&gt;SdkHttpError&lt;/code&gt;),&lt;br&gt;
&lt;code&gt;setRequestHandler(Schema, …)&lt;/code&gt; → &lt;code&gt;setRequestHandler('method/string', …)&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;.tool()&lt;/code&gt; → &lt;code&gt;registerTool&lt;/code&gt;, and &lt;code&gt;extra.*&lt;/code&gt; → &lt;code&gt;ctx.mcpReq.*&lt;/code&gt;. Where it cannot decide&lt;br&gt;
safely it leaves a marker instead of guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'@mcp-codemod-error'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that v2 requires &lt;strong&gt;Zod 4&lt;/strong&gt;, so a project on Zod 3 has a second upgrade in&lt;br&gt;
front of it.&lt;/p&gt;

&lt;p&gt;And then the part that is not mechanical, in the codemod's own words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28&lt;br&gt;
protocol revision (&lt;code&gt;createMcpHandler&lt;/code&gt;, multi-round-trip requests,&lt;br&gt;
&lt;code&gt;versionNegotiation&lt;/code&gt;) is architectural and not codemod-automatable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is accurate and worth taking seriously. Removing session state is a design&lt;br&gt;
decision — delete it, pass it as an explicit tool argument, or move it to a store&lt;br&gt;
both instances can reach. No tool makes that call for you, and the deprecated&lt;br&gt;
capabilities can wait twelve months while you do.&lt;/p&gt;




&lt;p&gt;All of it is open source, MIT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mcp-migration-check.alpaycelik.workers.dev" rel="noopener noreferrer"&gt;Hosted checker&lt;/a&gt;&lt;/strong&gt; — paste an
endpoint, get a graded report. Nothing to install, nothing stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/AlpayC/mcp-migration-check/releases/latest" rel="noopener noreferrer"&gt;Download the skill&lt;/a&gt;&lt;/strong&gt; —
install it in Claude, or unzip it and run the checker from any agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/AlpayC/mcp-migration-check" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/strong&gt; — 86 tests, mostly
pointed at the SSRF guard, since the hosted version fetches a URL a stranger
typed in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README has a section called &lt;em&gt;A rule that was wrong&lt;/em&gt;. It says roughly what this&lt;br&gt;
post says, in fewer words, and it stays there.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>typescript</category>
      <category>ai</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
