Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

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.

GitHubEmbed

Описание

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 web demo grading a live MCP endpoint: a C, one critical finding for the legacy initialize handshake, with the fix and a link to the spec section it derives from

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:

[![MCP 2026-07-28](https://github.com/OWNER/REPO/actions/workflows/mcp-check.yml/badge.svg)](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/discover mandatory, requires a resultType on every result, replaces the GET stream and resources/subscribe with subscriptions/listen, removes ping, logging/setLevel and SSE resumability, and requires Mcp-Method / Mcp-Name headers. 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 needs Cargo.toml, and MCP011 needs go.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. .cs is 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.toml parser 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.mod parser walks, but it is not the Go toolchain. It reads require in both the single-line and block forms across every go.mod under 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-path replace is a version pin and is read from its right-hand side — a pseudo-version such as v1.6.2-0.20260801000000-abcdef123456 (which names a commit, not a release), a +incompatible tag, and one marked // indirect (which the toolchain maintains to mean nothing here imports it). go.work is read for replace directives 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 its use directives name, resolved against its own directory, and only those at or below it: Go discovers a go.work in 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. A use pointing 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 --stateless help 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-sdk serving 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, and WithStateLess(false) is read as what it is. If your server sets Stateless somewhere 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 for mcp-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 — a cmd/ layout with several main packages — 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 +incompatible tag 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 initialize handshake) 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/discover with per-request _meta and the required headers), falls back to a modern tools/list, and only then tries the legacy handshake.
  • -32601 is explicitly not treated as modern evidence — every JSON-RPC server emits it for an unknown method. Only the -32020…-32099 range the spec reserves for itself counts, along with a result carrying resultType.
  • "Still accepts legacy" (MCP101, info, zero points) is split from "only accepts legacy" (MCP001, critical). Only the second is a finding.
  • info findings 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/sdk has 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, /node and 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

MIT

from github.com/AlpayC/mcp-migration-check

Установка Migration Check

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/AlpayC/mcp-migration-check

FAQ

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

Compare Migration Check with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории development