The short version: The 2026-07-28 MCP spec — the largest revision since launch — goes final on July 28, and for most server authors the migration is smaller than the headlines suggest. Exactly two changes are strictly breaking; the rest is either backward-compatible statelessness or deprecations on a 12-month clock. This is the checklist: what to change now, what to schedule, and what you can safely ignore until 2027.
If you want the why behind each move, we covered the stateless rewrite, the deprecation of Sampling, Roots, and Logging, and the six-SEP auth rewrite as they landed. This piece is the hands-on companion: work top to bottom and your server ships into the new spec on day one.
Do these two before July 28 — they actually break#
1. Change your missing-resource error code. MCP's custom -32002 for a missing resource is gone; the spec now uses the standard JSON-RPC -32602 (Invalid Params) per SEP-2164. One-line edit, but a client validating against the new spec will treat the old code as non-conformant.
- throw new McpError(-32002, "Resource not found: " + uri);
+ throw new McpError(-32602, "Resource not found: " + uri);
2. Make your schemas valid JSON Schema 2020-12. Tool inputSchema and outputSchema are now full JSON Schema 2020-12 (SEP-2106), with composition (oneOf, anyOf, allOf) and conditionals available. If your schemas are plain object/property definitions, you're already fine — but if you hand-rolled anything with an older draft's keywords, validate it now:
# point your schema at the 2020-12 meta-schema and lint every tool
npx ajv-cli validate -s tool.schema.json --spec=draft2020 -d '{}'
Everything below this line is safe to stage after the date.
Go stateless — but keep your state#
Statelessness (SEP-2567) removes the initialize/initialized handshake and the Mcp-Session-Id header. Client metadata — protocol version, capabilities — now rides in _meta on every request, and a server/discover method replaces handshake-time capability exchange. The practical rule: stop leaning on a protocol session, and mint your own explicit handle instead.
// before: state lived in an implicit session tied to Mcp-Session-Id
// after: the handle is a normal return value the model passes back
server.tool("cart_create", {}, async () => {
const cartId = await db.carts.create();
return { content: [{ type: "text", text: JSON.stringify({ cart_id: cartId }) }] };
});
server.tool("cart_add", { cart_id: z.string(), sku: z.string() }, async ({ cart_id, sku }) => {
await db.carts.addItem(cart_id, sku); // any instance can serve this
return { content: [{ type: "text", text: "added" }] };
});
The reward is operational: with no session header and no sticky routing, any request lands on any instance behind a plain round-robin load balancer. If you built a shared session store to scale horizontally, you can delete it.
Replace the three deprecated primitives (you have until 2027)#
SEP-2577 deprecates Sampling, Roots, and Logging — annotation-only, no wire changes, and SEP-2596 guarantees at least 12 months before any of them can be removed. Nothing breaks now. But the direction is set, so wire the replacements in as you touch each tool:
- Sampling → your own provider call. Instead of asking the client to run a completion via
sampling/createMessage, call the model yourself. This moves model choice, cost, and the security boundary back to whoever runs the server — where they belong.
// was: const out = await ctx.sample({ messages });
const out = await anthropic.messages.create({
model: "claude-sonnet-5", max_tokens: 512, messages,
});
- Roots → a tool parameter or config value. The filesystem/URI boundary a client used to hand you becomes an explicit argument (
root_path) or server configuration. More typing, less magic, easier to reason about. - Logging →
stderror OpenTelemetry. For stdio transports, write tostderr; for anything you want to query, emit OpenTelemetry spans. That's where your production telemetry already lives.
Long-running work: Tasks is an extension now#
Tasks graduates out of the core into an independently versioned extension (SEP-2663). Your tools/call returns a task handle; the client drives it with tasks/get, tasks/update, and tasks/cancel. The one thing to unlearn: tasks/list was removed — a stateless protocol has no session to scope a list against — so if you built polling on top of it, switch to handing back handles the client tracks itself.
Auth: start sending iss today#
You don't need the full auth rewrite to prepare for the one change with a scheduled teeth-baring: SEP-2468 makes clients validate the iss (issuer) parameter per RFC 9207, and the spec pre-announces that a future revision will reject any authorization response that omits it. If you run an authorization server, start emitting iss now so nothing breaks later. The rest of the auth changes are about matching how real OAuth/OIDC providers already behave.
The whole migration, in one pass#
Work the list: fix the error code, lint schemas to 2020-12, mint explicit handles and drop the session store, swap Sampling for a direct provider call, move Roots to a parameter and Logging to stderr/OTel, return task handles, and emit iss. Two of those are urgent; the rest you can land on your own calendar. When you're done, re-run your conformance suite — here's how we test an MCP server — and you'll be shipping the July 28 spec before most of the ecosystem has read it.



