DEV Community

Christo
Christo

Posted on

MCP goes stateless on July 28 — catch every breaking change in your server before the spec locks

On July 28, 2026 the Model Context Protocol locks its 2026-07-28 specification — the biggest breaking change the protocol has shipped. The handshake and sessions are gone; the protocol goes stateless. If you run an MCP server, several things you rely on today stop working on that date.

I built mcp-vet, a zero-config CLI that scans your MCP server source (TypeScript, JavaScript, Python) for the exact patterns that break — and tells you what to change.

npx @booyaka/mcp-vet .
Enter fullscreen mode Exit fullscreen mode

mcp-vet output

No account, no API key, no network calls. It parses your code locally (ts-morph for TS/JS, a bundled Python ast script for .py) and exits non-zero on anything BREAKING, so it drops straight into CI.

What changes on 2026-07-28

Breaking — these stop working:

  • Mcp-Session-Id header and protocol-level sessions are removed. Client info and capabilities now travel in per-request _meta.
  • initialize / notifications/initialized handshake is removed. Protocol version, client info, and capabilities now arrive on every request.
  • Error code -32002 (missing resource) becomes the JSON-RPC standard -32602 Invalid Params.
  • Legacy Tasks methodstasks/get · tasks/update · tasks/cancel argument shapes change, and tasks/list and tasks/result are removed entirely (poll with tasks/get).

Deprecated — 12-month grace period: the roots, sampling, and logging capabilities.

It matches how servers are actually written

The trick to a useful migration linter is catching real code, not just raw method strings. mcp-vet matches:

  • literal method strings'tasks/list', 'sampling/createMessage', 'logging/setLevel', …
  • SDK schema-constant registrationserver.setRequestHandler(InitializeRequestSchema, …) (how the official SDKs register handlers) maps InitializeRequestSchema, ListRootsRequestSchema, CreateMessageRequestSchema, ListTasksRequestSchema, … to the right rule.
  • SDK capability constructors — the Python SDK's ClientCapabilities(roots=RootsCapability()) is recognized structurally.
  • sessionIdGenerator — flagged only when it's a real generator, not the migrated sessionIdGenerator: undefined.

Every finding carries a confidence (high / medium / low) so you can tune the signal with --min-confidence.

Validated on real servers — 0 false positives

Pointed at the official MCP TypeScript SDK's own example servers, it finds 6 real issues — including the sessionIdGenerator session usage that servers set but rarely spell out as a literal header — and stays quiet on the Mcp-Session-Id and initialize mentions that live only in comments:

legacy-routing.ts:36  BREAKING    MCP_SESSION_ID   req.headers['mcp-session-id']
legacy-routing.ts:41  BREAKING    MCP_SESSION_ID   sessionIdGenerator: () => randomUUID()
sse-polling.ts:34     DEPRECATED  LOGGING_CAP      capabilities: { logging: {} }

6 finding(s): 5 BREAKING, 1 DEPRECATED
Enter fullscreen mode Exit fullscreen mode

Run across a broad corpus of the official reference servers (TypeScript and Python), it produced 0 false positives — precision from structural AST checks, not text matching.

Fix the mechanical one automatically

The -32002 → -32602 swap is purely mechanical, so it's auto-fixable — with a preview first:

npx @booyaka/mcp-vet . --fix --dry-run   # show the rewrites, change nothing
npx @booyaka/mcp-vet . --fix             # apply them
Enter fullscreen mode Exit fullscreen mode

Drop it into CI

- uses: actions/setup-node@v4
  with: { node-version: '20' }
- run: npx @booyaka/mcp-vet . --github-annotations
Enter fullscreen mode Exit fullscreen mode

It also emits SARIF 2.1.0 for GitHub code scanning, plus Markdown/JSON reports, --json for piping, and inline suppression comments. And it's a typed library, not just a CLI:

import { scan, applyFixes } from '@booyaka/mcp-vet';
const { findings } = scan(['./src'], { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

What it can't tell you (on purpose)

A few 2026-07-28 changes have no reliable static signal — the removed long-lived SSE push channel, the new required Mcp-Method / Mcp-Name headers, auth hardening, and JSON-Schema-2020-12 tool schemas. mcp-vet documents these and prints a reminder after every scan, so a clean result never pretends to mean "fully migrated."

The spec locks in days — better to find out now than when a client stops talking to your server.

Repo: https://github.com/Booyaka101/mcp-vet
Install: npx @booyaka/mcp-vet .

Top comments (4)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

Useful static layer, especially the distinction between executable AST patterns and comments. One migration nuance I would make explicit: July 28 is the specification release date, not a switch that remotely disables existing deployments. Breakage appears when a client and server negotiate or require the new revision, so the production test matrix needs both 2025-11-25 and 2026-07-28 paths during rollout.

I’d pair the linter with protocol-level fixtures: server/discover, per-request protocol/client metadata, required HTTP routing headers, stateless auth context, explicit state-handle creation/resume, duplicate requests, retries on another server instance, and cache invalidation for tools/list. Also verify downgrade/refusal behavior rather than silently accepting a request under the wrong semantics.

“0 false positives” is encouraging but incomplete without corpus size, commit SHAs, labeled negatives, and recall. A regression suite of intentionally obfuscated/dynamic usages—aliased imports, wrappers, computed properties, generated registration, and framework adapters—would show what the AST scanner misses. Static analysis can prove known patterns are absent; the conformance and dual-version tests prove the server still speaks the intended wire contract.

Collapse
 
booyaka101 profile image
Christo

This is one of the most useful pieces of feedback I've had — almost all of v0.4.0 came out of it. Point by point:

  • Dual-version rollout — you're right that July 28 is a release date, not a remote kill switch. Added a 2025-11-25 + 2026-07-28 rollout matrix to the README and a post-scan notice saying exactly that.
  • Protocol-level fixturesmcp-vet fixtures now emits nine runtime scenarios that track your list almost 1:1: server/discover, per-request _meta, routing headers, stateless auth, task-handle create/resume, duplicate requests, retry on another instance, tools/list cache invalidation, and downgrade/refusal — plus a CHECKLIST to run against a live server.
  • Benchmark transparency — replaced the bald "0 false positives" with a BENCHMARK.md: pinned corpus SHAs (447 files / ~44k LOC), every finding hand-labeled (104/105 TP, 1 FP, disclosed), labeled negatives, and a recall discussion. It genuinely was incomplete without it.
  • Adversarial / obfuscated usages — aliased imports now resolve (import and usage sites), and there's a caught/ + missed/ regression suite that documents what the AST scanner catches vs. can't (wrappers, computed properties, generated registration) so the claims can't quietly rot.

All in v0.4.0 (npx @booyaka/mcp-vet .). Thank you — you raised the bar on the tool.

Collapse
 
raju_dandigam profile image
Raju Dandigam

Stateless MCP is the kind of change that sounds simple in the spec and turns messy in the glue code around auth, retries, and session assumptions. I like that this checks source patterns locally and fails CI, because most breakage will sit in wrappers and samples people forgot they copied six months ago. The part I'd add to migration plans is exercising client-side assumptions too: a lot of tool reliability bugs only show up when the server is stateless but the client still behaves as if it owns a session.

Collapse
 
booyaka101 profile image
Christo

This comment was the nudge for the headline feature in v0.4.0 — thank you. mcp-vet now flags client-side session ownership too: a client transport built with a real sessionId/session_id, or a read of transport.sessionId, gets flagged even when the server scans clean — and sessionId: undefined is recognized as the migrated form so it doesn't re-flag already-fixed code. That's exactly your "server is stateless but the client still behaves as if it owns a session" case. Appreciate you pointing at the glue code; that's where the copied-six-months-ago wrappers bite. Shipped in v0.4.0.