mcp-stateless
v0.3.1
Published
Check whether an MCP server is ready for the 2026-07-28 stateless specification.
Maintainers
Readme
mcp-stateless
Is your MCP server ready for the 2026-07-28 stateless specification?
Connects to a running MCP server, probes what it actually does, and tells you exactly which of the 2026-07-28 breaking changes it fails — with the wire traffic that proves it and the specific change that fixes it.
npx mcp-stateless --stdio "node dist/server.js"Documentation · User guide · Usage · In CI · Rules · Questions
Why this exists
MCP revision 2026-07-28
is the largest breaking change in the protocol's history. MCP went stateless.
| Removed | Added |
| -------------------------------------- | -------------------------------------- |
| initialize handshake | server/discover (mandatory) |
| Mcp-Session-Id and protocol sessions | Per-request _meta envelope |
| ping, logging/setLevel | resultType on every result |
| resources/subscribe / unsubscribe | ttlMs / cacheScope on list results |
| Server-initiated requests | Multi Round-Trip Requests |
| SSE stream resumability | Renumbered protocol error codes |
Every MCP server must migrate, on a twelve-month deprecation clock.
The official @modelcontextprotocol/codemod
rewrites the v1→v2 SDK API surface — imports, symbol renames, handler
signatures — and explicitly stops there. In its own words, adopting the
2026-07-28 protocol revision "is architectural and not codemod-automatable".
That leaves the question static rewriting cannot answer: does the server I am now running actually conform? This asks the server itself.
Quick start
No installation required.
# stdio
npx mcp-stateless --stdio "node dist/server.js"
# Streamable HTTP
npx mcp-stateless --http https://api.example.com/mcp
# with authentication
npx mcp-stateless --http https://api.example.com/mcp --header "Authorization: Bearer $TOKEN"Exit 0 ready · 1 findings · 2 usage error, unreachable server, an
incomplete probe, or an --emit file that could not be written.
Every release is published from CI by npm trusted publishing, so each version carries a signed SLSA provenance attestation tying it to the exact workflow run and commit that built it. No publish token exists. Verify with:
npm audit signaturesWhat a run looks like
mcp-stateless — checking against MCP 2026-07-28
target: node dist/server.js (stdio)
Breaking (7)
× MCP001 server/discover is not implemented (SDK)
found server/discover returned JSON-RPC error -32601: Method not found.
expected Servers MUST implement server/discover, advertising supported protocol
versions, capabilities and identity.
fix Add a server/discover handler returning { supportedVersions, capabilities },
with identity in _meta. Current SDKs implement this for you once upgraded.
spec https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle
× MCP004 Results are missing the required resultType field (SDK)
× MCP006 The removed ping method is still implemented (SDK)
× MCP009 subscriptions/listen is missing despite advertised listChanged (SDK)
…
NOT READY — 7 breaking issues across 14 checks.
7 of those are protocol plumbing owned by your MCP SDK — upgrading to a release
that targets 2026-07-28 resolves them with no change to your code.
None require a change to your own code.--verbose adds the JSON-RPC exchange behind each finding.
The SDK line is the point
Most of what breaks is plumbing your SDK owns — you never wrote a
pinghandler, the SDK registered one. Telling you to delete it would send you hunting through code you do not maintain.So every rule declares who fixes it, and the summary splits on that line. Seven findings can mean zero work for you and one SDK upgrade. Knowing which is worth more than the list above it.
Proven against real servers
Fixtures prove the rules against servers built to trip them. That is necessary but not sufficient, so this is also run against real software.
Unmigrated — the official @modelcontextprotocol/* servers:
| Server | Breaking | Advisory |
| ---------------------------- | -------- | -------- |
| server-everything | 10 | 2 |
| server-memory | 9 | 1 |
| server-filesystem | 7 | 1 |
| server-sequential-thinking | 7 | 1 |
Migrated — the TypeScript SDK at 2.0.0-alpha.0, built from source:
| Example | Transport | Verdict |
| -------------------- | --------- | -------------------------- |
| server-quickstart | stdio | READY, no findings |
| caching (dual-era) | http | READY, 1 dual-era advisory |
Every one produced a false result against a correctly migrated server, and none would have surfaced from the fixture suite — the fixtures encoded the same assumptions as the rules.
- The HTTP transport never sent the required
MCP-Protocol-Versionheader, so a real v2 server rejected every probe and seven rules fired spuriously server/discoverwas checked forprotocolVersions; the schema field issupportedVersionsserverInfowas expected at the top level ofDiscoverResult, where the schema does not define it- A dual-era server — the SDK's own recommended migration path — was reported NOT READY
MCP009blamed the server author for something only an SDK upgrade fixes
All fixed, all covered by regression tests. If a rule fires on a server you believe is correct, that is a bug and the most useful thing you can report.
A worked example
docs/migration-walkthrough.md takes a real server from 7 breaking findings to READY: the full report, what every finding means, the actual diff, and an honest account of what the tool got wrong and what it still cannot tell you.
What it checks
20 rules, each tied to a named changelog entry and SEP. Full detail in
docs/rules/.
| Rule | Check | Transports | Fixed by |
| ------------------------------ | ------------------------------------------------------ | ----------- | ----------- |
| MCP001 | server/discover not implemented | stdio, http | SDK upgrade |
| MCP002 | Still requires the initialize handshake | stdio, http | SDK upgrade |
| MCP003 | Still uses the removed Mcp-Session-Id header | http | SDK upgrade |
| MCP004 | Results missing required resultType | stdio, http | SDK upgrade |
| MCP005 | List results missing ttlMs / cacheScope | stdio, http | SDK upgrade |
| MCP006 | Removed ping still implemented | stdio, http | SDK upgrade |
| MCP007 | Removed logging/setLevel still implemented | stdio, http | SDK upgrade |
| MCP008 | Removed resources/subscribe still implemented | stdio, http | SDK upgrade |
| MCP009 | subscriptions/listen missing despite listChanged | stdio, http | SDK upgrade |
| MCP010 | Removed HTTP GET stream endpoint still served | http | SDK upgrade |
| MCP011 | Resource-not-found still returns -32002 | stdio, http | SDK upgrade |
| MCP012 | Protocol error codes not renumbered | stdio, http | SDK upgrade |
| MCP013 | Rejects requests carrying the _meta envelope | stdio, http | your code |
| MCP014 | Rejects the required Mcp-Method / Mcp-Name headers | http | SDK upgrade |
| Rule | Check | Transports | Fixed by |
| ------------------------------ | ------------------------------------------------------ | ----------- | ---------------- |
| MCP015 | Declares deprecated Roots / Sampling / Logging | stdio, http | your code |
| MCP016 | Deprecated HTTP+SSE transport | http | SDK upgrade |
| MCP017 | tools/list ordering not deterministic | stdio, http | your code |
| MCP018 | Results do not identify the server via _meta | stdio, http | SDK upgrade |
| MCP019 | Authorization server does not advertise RFC 9207 iss | http | your auth server |
| MCP020 | Authorization server offers only deprecated DCR | http | your auth server |
Some changes need an interactive scenario to probe honestly. Checking them unreliably would be worse than not checking them, so they are tracked as issues instead:
It also never calls a tool — tools have side effects. It checks the protocol envelope, not your logic.
In CI
- uses: Khanthtutzin/[email protected]
with:
stdio: node dist/server.js
fail-on: errorThe action ref pins the checker: it runs the npm version it was released with,
not whatever is latest. Override with the version input if you need to.
A moving v1 tag will exist from 1.0.0 onward — until then, pin the exact
release as above.
Or upload SARIF so findings appear in the Security tab and inline on pull requests:
- run: npx mcp-stateless --stdio "node dist/server.js" --format sarif --output results.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif--stdio <command> Command that starts the server on stdio.
--http <url> Streamable HTTP endpoint.
--header <k:v> Extra HTTP header. Repeatable.
--cwd <dir> Working directory for the --stdio command.
--format <fmt> text (default), json, sarif, markdown.
--output <file> Write the report to a file instead of stdout.
--emit <fmt>:<file> Also write this format to this file. Repeatable.
--verbose Include the JSON-RPC traffic behind each finding.
--no-color Disable ANSI colour (NO_COLOR is honoured too).
--only <ids> Comma-separated rule ids to run exclusively.
--skip <ids> Comma-separated rule ids to skip.
--timeout <ms> Per-request timeout. Default 10000.
--fail-on <level> error (default), warning, or never.
--list-rules Print the rule catalogue and exit.
--version, --helpA server that never answers is reported UNREACHABLE with no findings at
all — twenty confident verdicts about a server that failed to start would be
worse than nothing.
import { runChecks, StdioTransport } from 'mcp-stateless';
const transport = new StdioTransport('node dist/server.js');
const report = await runChecks(transport);
await transport.close();
for (const f of report.findings) {
console.log(`${f.ruleId} ${f.severity} [${f.remediation}] ${f.title}`);
console.log(` fix: ${f.fix}`);
}How it works
mcp-stateless speaks both protocol revisions. It hand-rolls JSON-RPC
rather than using an MCP SDK, because an SDK abstracts away precisely what needs
observing — it performs the handshake for you and normalises errors.
The probe runs a fixed opening sequence whose ordering is load-bearing:
server/discover— mandatory now, harmless against a legacy servertools/listbefore any handshake — on stdio a successfulinitializewould persist in the child process and mask a server that still requires it- Legacy
initialize— expected to fail on a compliant server tools/listagain, only if step 2 failed and step 3 worked
That combination is the signature of a still-stateful server, and it is what
separates "requires the handshake" from "chokes on _meta" from "rejects the
routing headers" — three failures that look identical from outside.
Zero runtime dependencies. Argument parsing uses node:util.parseArgs, HTTP
uses the built-in fetch, and the terminal colours are twelve lines of ANSI.
Nothing is installed into your CI beyond this package.
See docs/ARCHITECTURE.md for the full map.
Contributing
Adding a rule is one file plus one test — rules never touch the transport
layer, only a shared ProbeContext. See CONTRIBUTING.md and
the good first issue
label.
The suite runs every rule against real fixture MCP servers over real stdio and real HTTP — one built to 2025-11-25, one to 2026-07-28, plus servers that fail in specific ways. No mocks.
git clone https://github.com/Khanthtutzin/mcp-stateless.git
cd mcp-stateless && npm install && npm testAffiliation
An independent open-source project. Not affiliated with, endorsed by, or an official part of the Model Context Protocol project or Anthropic. "Model Context Protocol" and MCP are used only to describe what this tool checks against.
Findings reported about any third-party server describe that server's observable protocol behaviour at a point in time, nothing more — and every one of them is reproducible with a single command, printed alongside the finding.
