Migration Check
БесплатноНе проверенDeterministic readiness checker for the MCP 2026-07-28 specification break - a graded web probe and an agent skill over one rule engine.
Описание
Deterministic readiness checker for the MCP 2026-07-28 specification break - a graded web probe and an agent skill over one rule engine.
README
CI Release npm Live demo Claude Code plugin License: MIT
Will your MCP server survive the 2026-07-28 rewrite?
Try it → mcp-migration-check.alpaycelik.workers.dev Paste an MCP endpoint, get a graded report. Nothing to install, nothing stored.
60.1% of public MCP servers still serve only the legacy protocol
State of MCP migration — 2026-08-23 — 13,380 unique remote endpoints from the official registry probed; 10,890 returned enough protocol or authentication signal to grade. 60.1% serve the legacy protocol only. A further 5.5% are dual-era — current and still answering the old handshake, which the revision permits and this report does not count against them. Narrowed to the 6,191 servers touched since the revision shipped, 63.8% are legacy-only. An earlier version of this snapshot could not tell dual-era and legacy-only apart; see the postmortem.
The Model Context Protocol revision dated 2026-07-28 is the largest breaking change in the protocol's history: it makes the transport stateless, formalizes OAuth 2.1 for remote servers, and deprecates several capabilities. Migrating is a refactor, not a version bump — and a large share of the thousands of public servers aren't actively maintained.
mcp-migration-check is a small, deterministic readiness checker. It points
at a running MCP endpoint (or scans a repo) and reports, with a letter grade and
per-finding fixes, exactly what breaks. No LLM, no API key, nothing stored.
It ships as four surfaces over one core:
| Surface | Use it to… |
|---|---|
| Web demo | paste a URL, get a graded report — nothing to install |
| CLI | npx mcp-migration-check <url> — one command, no install |
| GitHub Action | keep a server from regressing, with a grade on every PR |
| Skill | hand an agent the diagnosis and the migration procedure |
The split is deliberate. The demo and the CLI see a server from the outside and answer "am I broken?". The Action asks that question again on every commit. The skill sees the code and answers "fix it" — which is the part that actually takes a week.
Languages
A source scan reads TypeScript, Python, Rust, and Go MCP servers, each with a dependency check against the manifest that pins its SDK:
| Language | Manifest read | SDK rule |
|---|---|---|
| TypeScript / JavaScript | package.json |
MCP007 |
| Python | pyproject.toml, requirements*.txt, Pipfile, setup.cfg |
MCP009 |
| Rust | Cargo.toml |
MCP010 |
| Go | go.mod |
MCP011 |
| C# | — | none yet |
Every language with a rule above is also grepped for the protocol signals
behind MCP001–MCP005, which are language-neutral. C# is not scanned at all — the
scanner does not read .cs files and there is no C# SDK rule, so a source scan
of a C# server reports nothing and that clean result means nothing. That SDK
moved to a 2.x major of the ModelContextProtocol packages, so the TypeScript
and Python migration advice does not carry over either. Probe a C# server live
instead: a live probe needs no language at all, it only speaks HTTP.
Go is the one that does not rhyme. The other three SDKs announce the
protocol break in the version number — a new major, or a rename. Go did not:
github.com/modelcontextprotocol/go-sdk crossed it at the v1.6.1 → v1.7.0
minor, on the same module path, and there is no go-sdk/v2 to migrate to.
So MCP011 compares full release triples rather than majors, and its advice
never tells anyone to change an import path.
Quick start
GitHub Action
The check that keeps answering the question after you have stopped asking it.
Paste this as .github/workflows/mcp-check.yml and every push says whether the
server still survives the revision:
name: MCP 2026-07-28
on: [push, pull_request]
jobs:
mcp-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AlpayC/mcp-migration-check@v1
That is the entire file. source defaults to the checked-out repository and a
critical finding fails the step; both are inputs when you want something else:
- uses: AlpayC/mcp-migration-check@v1
with:
source: . # or: url: https://example.com/mcp
fail-on: critical # critical (default) | warning | never
It writes a graded table to the job summary and exposes grade, score,
critical, warnings, findings, report-path and badge-url as step
outputs. No setup-node, no install: the engine ships pre-bundled in the
action. An endpoint that cannot be reached never fails the build — an outage is
not the same claim as an unmigrated server.
Badges. The self-updating one is the workflow's own status badge:
[](https://github.com/OWNER/REPO/actions/workflows/mcp-check.yml)
The badge-url output is a shields.io badge carrying the actual letter grade.
It is a snapshot of the run that produced it, so it only stays true if something
writes it back — a README commit, or a gist the badge reads from. Say which one
you mean; a stale A is worse than no badge.
CLI
npx mcp-migration-check https://example.com/mcp # probe a live endpoint
npx mcp-migration-check --source ./my-server # scan a repository
npx mcp-migration-check --source . --json # machine-readable
The published package is one generated file and a README, with an empty
dependency tree — the same bundled engine the skill carries. Exit codes are
0 clean, 1 at least one critical finding, 2 inconclusive, so it works as
a CI gate on its own.
It is an ordinary package on the public registry, so whatever you already use reaches it — only the run-it-once command differs:
pnpm dlx mcp-migration-check https://example.com/mcp
yarn dlx mcp-migration-check https://example.com/mcp
bunx mcp-migration-check https://example.com/mcp
Web demo
Hosted at mcp-migration-check.alpaycelik.workers.dev, or run it yourself:
npm install
npm run dev:web # http://localhost:3000
Paste an endpoint and read the graded report. Because the handler fetches a
user-supplied URL server-side, it enforces two things: an SSRF guard that
refuses localhost, private ranges and the cloud metadata address, and a
rate limit of 20 requests per minute per IP via Cloudflare's Workers
binding. See DEPLOY.md for why neither lives where you might
expect.
Skill
In Claude Code, install it from this repository:
/plugin marketplace add AlpayC/mcp-migration-check
/plugin install mcp-migration@mcp-migration-check
Then ask it to migrate a server. /plugin marketplace update mcp-migration-check
picks up later changes — the skill is served from the repository rather than
copied out of it, so it cannot go stale against the rules it ships.
Or download mcp-migration.skill from the latest release and install that. Or build it yourself:
npm install
npm run pack:skill # → dist/mcp-migration.skill
The skill bundles a dependency-free copy of the rule engine, so the agent's
diagnosis step is deterministic rather than a guess from reading code — and
references/ carries the per-rule remediation guidance for the part that
follows.
Not using Claude? The bundled checker is a single file that needs nothing
but Node, so any agent that can run a shell command — Codex, Cursor, whatever —
can use it directly. Unzip the .skill (it is a zip) and run
scripts/mcpcheck.mjs, or take it from skill/mcp-migration/scripts/ in this
repo. references/remediation.md is plain Markdown and reads fine on its own.
Run the bundled checker directly if you want:
node skill/mcp-migration/scripts/mcpcheck.mjs --source ./my-server
node skill/mcp-migration/scripts/mcpcheck.mjs --local http://localhost:3000/mcp
What it checks
| Rule | Severity | Signal |
|---|---|---|
| MCP001 | critical | legacy-only: answers initialize, serves no modern surface |
| MCP002 | critical | Mcp-Session-Id minted for a modern request — the classic hazard |
| MCP003 | warning | deprecated logging capability |
| MCP004 | warning | deprecated sampling capability |
| MCP005 | warning | deprecated roots capability |
| MCP006 | critical | auth without RFC 9728 protected-resource metadata |
| MCP007 | warning | still on @modelcontextprotocol/sdk (the v1 line) |
| MCP008 | warning | modern server that does not implement server/discover |
| MCP009 | warning | Python mcp constrained to 1.x or importing the v1 FastMCP API |
| MCP010 | warning | Rust MCP crate on a pre-2026-07-28 line (source scan only) |
| MCP011 | warning | Go MCP SDK that cannot serve 2026-07-28 (source scan only) |
| MCP101 | info | dual-era: current and still accepts the legacy handshake |
| MCP102 | info | session ids issued to legacy clients only |
Live checks observe runtime behavior over HTTP; source scans grep for the same
signals in code. Beside the source they read each language's manifest — Python's
pyproject.toml, requirements files, Pipfile and setup.cfg (including
nested projects), Rust's Cargo.toml, and Go's go.mod (including nested
modules) — for the SDK rules in Languages. Each finding links the
spec or official SDK migration page it derives from.
Backwards compatibility is not a finding. The MCP1xx rules are
observations and cost zero points. 2026-07-28 says a server that wants to
serve both kinds of client MAY implement both
behaviours,
picking its semantics from how each request opens — so a server that answers
initialize for the v1 clients still in the field is being compatible, not
drifted. The live probe therefore opens as a modern client
(server/discover with per-request _meta and the required
MCP-Protocol-Version header) and only then tries the legacy handshake. Only
the absence of the modern surface is a defect.
Honest limitations
- These rules are not the whole revision. The 2026-07-28 changelog also makes
server/discovermandatory, requires aresultTypeon every result, replaces the GET stream andresources/subscribewithsubscriptions/listen, removesping,logging/setLeveland SSE resumability, and requiresMcp-Method/Mcp-Nameheaders. A server can pass every rule here and still be broken. This is a triage tool, not a conformance suite. - The source scan is heuristic. It greps for patterns, so it can miss dynamically-built capability names and can over-match inside comments. Treat source findings as signals to review, not proof. The live probe is more authoritative for runtime behavior; the two complement each other.
- The web demo only sees the outside. It probes over HTTP, so it cannot
reach the SDK rules: MCP007 needs
package.json, MCP009 needs Python project metadata or source, MCP010 needsCargo.toml, and MCP011 needsgo.mod. Use the skill or CLI for repository checks. - MCP001 proves absence, which is the weaker claim. It fires when the legacy handshake answers and no modern signal did. A server whose modern surface is hidden behind a WAF, a path-based gateway or an unfamiliar-method filter lands there wrongly. The dual-era observation cannot fail the same way — it needs a positive modern answer to fire at all.
- A source scan does not read C# at all.
.csis not in the scanned extension list, so a C# repository produces no findings and no signals — not a clean bill of health, an empty one. Probe those servers live. - The Rust
Cargo.tomlparser is line-oriented and reads only the root manifest. It reads[dependencies],[dev-dependencies]and[workspace.dependencies], in the inline-string, inline-table and[dependencies.rmcp]sub-table forms, and names the section in the finding. It does not see workspace member inheritance (workspace = true), renamed dependencies (package = "rmcp"), target-specific tables, or a dependency with no quoted version ({ git = "…" }). A clean MCP010 result means the root manifest is clean — workspace-inherited dependencies may still use an older crate version. - The
go.modparser walks, but it is not the Go toolchain. It readsrequirein both the single-line and block forms across everygo.modunder the scan root. It deliberately reports nothing for four kinds of requirement, because in each the version string does not describe what would actually build: one replaced by a local path or by a different module path — a fork, whose version describes the fork and not the SDK, while a same-pathreplaceis a version pin and is read from its right-hand side — a pseudo-version such asv1.6.2-0.20260801000000-abcdef123456(which names a commit, not a release), a+incompatibletag, and one marked// indirect(which the toolchain maintains to mean nothing here imports it).go.workis read forreplacedirectives only — a workspace replacement overrides the module-level one, so ignoring it reported versions the build never resolves — but not for anything else. A workspace governs only the modules itsusedirectives name, resolved against its own directory, and only those at or below it: Go discovers ago.workin the working directory or an ancestor, never a descendant, so a workspace buried in a subdirectory is not in effect for the module above it. Ausepointing through a symlink is ignored, and two workspaces that disagree about one module leave it unreadable rather than letting directory order decide. A clean MCP011 result therefore means no classifiable requirement is behind — a workspace or a replaced module can still be. - Go comments and strings are scanned, not parsed. The transport signals
skip
//comments and string literals, including multi-line backtick strings and/* … */comments — a server's own--statelesshelp text should not be read as configuration, and a backtick quoted inside a comment is not a string. The tracking is lexical, so a file that opens a backtick string or a block comment and never closes it hides the rest of itself from those two signals. Such a file does not compile, so it cannot be a working server, but it can suppress MCP011's second case in a scan. - MCP011's second case argues from absence. A modern
go-sdkserving streamable HTTP is only flagged when the stateless opt-in appears nowhere in the module that declares the requirement. Test files (*_test.go) and commented-out code are excluded from both halves of that judgement, andWithStateLess(false)is read as what it is. If your server setsStatelesssomewhere the scan cannot see — in another module, or from a config value — the finding is a false positive; it costs a warning and nothing else. It never fires for stdio servers, which need no opt-in, nor formcp-go, which advertises the revision by default. The absence is judged per module, not per binary, so a module holding both a stateless and a stateful command — acmd/layout with severalmainpackages — is not reported. Pairing them more tightly would break the equally ordinary shape where the options value is built in a shared helper package, which is the worse trade. - An unclassifiable requirement is quiet, not exonerating. A
replaced module, a pseudo-version and a+incompatibletag produce no MCP011 finding — and equally no modern-era evidence, because none is available. The language-neutral heuristics behind MCP001 and MCP002 still apply to that repository exactly as they would to one with no manifest at all.
Two rules that were wrong
Worth recording, because it shaped how the rest is verified.
MCP001 told servers to break their own users
The first version of MCP001 fired on any endpoint that answered initialize
and told the maintainer to "remove the initialize/initialized handshake".
Taking that advice would have cut off every v1 client still pointed at the
server, for no compliance gain — because 2026-07-28 never asked for it:
A server that wishes to support both legacy clients (which expect an
initializehandshake) and modern clients (which use per-request metadata) MAY implement both behaviors. — Versioning: Backward Compatibility
The probe made it worse by only ever speaking as a legacy client: it sent one
initialize carrying protocolVersion: 2025-11-25 and reported the answer as
drift. It never asked the modern question at all, so a dual-era server — the
mark of a maintained one — was indistinguishable from an abandoned v1 server.
What changed:
- The probe opens as a modern client (
server/discoverwith per-request_metaand the required headers), falls back to a moderntools/list, and only then tries the legacy handshake. -32601is explicitly not treated as modern evidence — every JSON-RPC server emits it for an unknown method. Only the-32020…-32099range the spec reserves for itself counts, along with a result carryingresultType.- "Still accepts legacy" (MCP101, info, zero points) is split from "only accepts legacy" (MCP001, critical). Only the second is a finding.
infofindings now cost nothing, so compatibility cannot pull a grade down.
Pinned by tests, including one asserting MCP001's fix text never says to remove the handshake.
MCP007 named a version that never existed
MCP007 originally fired on @modelcontextprotocol/sdk below 2.0.0 and told
you to upgrade to ^2 and run "the official v1→v2 codemod". Both halves were
wrong in different ways, and neither was caught by reading the code — only by
checking against npm and the spec:
@modelcontextprotocol/sdkhas never published a 2.x. It tops out at 1.30.0. So the fix text named a version that does not resolve.- v2 exists, but as a package rename:
@modelcontextprotocol/server,/client,/core,/nodeand the HTTP adapters, all published 2026-07-27. - The codemod is real, and is its own package:
npx @modelcontextprotocol/codemod@latest v1-to-v2 .
The first correction overshot — the rule was deleted outright on the conclusion
that no v2 line existed at all, which is what the package rename makes it look
like from the sdk package alone. It was reinstated once the new names turned
up. It now keys on the presence of the v1 package rather than a version
threshold, because the package name is the actual signal.
Two tests exist so this cannot come back:
assert.ok(!/@modelcontextprotocol\/sdk[@^ ]*\^?2/.test(f.fix)); // no phantom 2.x
assert.ok(!rule.specRef.includes("#")); // no dead anchor
The second one guards a related defect found the same way: every rule's
specRef pointed at …/2026-07-28#lifecycle and similar, but the spec is split
across subpages and has no such anchors — all seven links silently resolved to
the overview page. They are now verified subpage URLs.
Architecture
.claude-plugin marketplace manifest — the skill, served from this repo
packages/core pure, deterministic engine (rules · probe · scan · SSRF guard)
packages/core/test node:test suite over the engine — no network, no disk
packages/cli the npm package: nothing but the bundled engine
skill/mcp-migration SKILL.md + bundled engine + per-rule remediation guide
web Next.js demo (App Router) over the same core
action.yml composite GitHub Action over the bundled engine
scripts/bundle-engine.mjs one esbuild config, two generated copies of the engine
scripts/ecosystem-report.mjs registry-wide readiness snapshot
npm test # node --test via tsx; the full suite, no network
npm run typecheck
npm run build:bundles # regenerate both copies of the engine; CI fails if stale
The suite leans on the seams the engine already had: rules are pure functions
over a RuleContext, and probeEndpoint takes a fetchImpl. The SSRF guard
gets the most coverage — it is a security control on a public handler, so each
blocked range is paired with the adjacent address that must still pass.
One core, four consumers — the rules live in exactly one place, and the skill's and the CLI's copies of the engine are generated, never hand-edited. CI rebuilds both and fails if either moved.
Tech
TypeScript · Node 22 · npm workspaces · Next.js 16 (App Router, Turbopack) · React 19.2 · Tailwind v4 · Magic UI · Cloudflare Workers via OpenNext. No runtime LLM. MIT licensed.
Ecosystem report
The latest run is published at mcp-migration-check.alpaycelik.workers.dev/state-of-mcp, alongside the rendered Markdown in reports/. To produce a new one:
npm run report:ecosystem -- --limit 500 --concurrency 6
Pulls the remote endpoints out of the
official MCP registry, probes each
one with the same engine, and writes an aggregate snapshot to reports/: how
many endpoints answered, which protocol era each serves, the grade
distribution, and how often each rule fires. --name-servers adds a per-server
table.
The raw JSON is ~16 MB and gitignored, so the run's last step extracts the
aggregate totals into web/lib/ecosystem-snapshot.ts — a committed file the
site reads. Nothing on the page is a number typed in by hand; the one time the
JSX carried its own copy it drifted to 13,350 against the report's 13,380.
npm run build:ecosystem-snapshot redoes just that extraction from a report
already on disk.
Dating the sample. A dead endpoint that still returns 200 is
indistinguishable from a maintained one that chose not to migrate — unless you
can date it, and the registry skews heavily toward servers listed once and
never touched again. So each row is dated: from the linked GitHub repository's
last push where there is one, and from the registry entry's own updatedAt
otherwise. The report cross-tabs protocol era against that date, and restates
the headline over the servers whose code has been touched since the revision
shipped — the ones that could have migrated. Set GITHUB_TOKEN for it to
cover more than 60 repositories an hour; without one the run dates what it can
and prints the coverage rather than guessing. --no-dates skips the pass,
--active-window <days> moves the line (default 180).
The two dates are not the same claim and the report says which one each row rests on: a push is evidence about the code, a registry timestamp only says when somebody last published an entry.
Two things about it are deliberate.
The Markdown report counts servers, it does not name them. A public league
table of broken servers is a different project with a different ethics, and it
would poison the well with exactly the maintainers this tool exists to help.
The JSON alongside it does carry per-target detail, because it is a local file
rather than a publication — which is why reports/*.json is gitignored and the
Markdown is not.
Answering is not the same as being migrated. probeEndpoint sets
reachable on any HTTP response, which is right for a checker aimed at one
endpoint you own. Across a few thousand strangers it is not: 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 come back as a clean A. A snapshot built on
that would report the exact opposite of the truth. Those land in an
answered, but showed no MCP behaviour bucket and stay out of the denominator.
Contributing
Corrections to rules are the most useful thing you can send — this project has shipped one that was factually wrong, and the section above exists because of it. See CONTRIBUTING.md for what a rule change needs, and AGENTS.md for the invariants that CI enforces.
Found a way past the SSRF guard on the hosted demo? That one goes to SECURITY.md, not to a public issue.
License
Установка Migration Check
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/AlpayC/mcp-migration-checkFAQ
Migration Check MCP бесплатный?
Да, Migration Check MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Migration Check?
Нет, Migration Check работает без API-ключей и переменных окружения.
Migration Check — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Migration Check в Claude Desktop, Claude Code или Cursor?
Открой Migration Check на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectAmap Maps Mcp Server
MCP server for using the AMap Maps API
автор: duxiaohuiSupabase
Database, auth and storage
автор: SupabaseEverything
Reference / test server with prompts, resources, and tools.
Git
Tools to read, search, and manipulate Git repositories.
Sequential Thinking
Dynamic and reflective problem-solving through thought sequences.
Time
Time and timezone conversion capabilities.
Compare Migration Check with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
