Claude Code
MCP 2026-07-28 Migration Guide: Stateless Protocol, No Sessions, server/discover, MRTR, and SDK v2 Upgrade
How do I migrate my MCP server or client to the 2026-07-28 Model Context Protocol specification?
MCP 2026-07-28 makes the protocol stateless. It removes the initialize handshake and the Mcp-Session-Id header, so every request carries its protocol version and client capabilities in _meta. Servers must implement server/discover and return server-to-client requests (elicitation, sampling, roots) as input_required results instead of sending their own requests. Upgrade to the v2 TypeScript packages (2.0.0) or Python mcp 2.0.0. Both can serve modern and 2025-era clients from one server.
What changed versus 2025-11-25
The official changelog lists these major changes since revision 2025-11-25.
- Protocol-level sessions and the Mcp-Session-Id header are removed from Streamable HTTP. Servers that need state across calls mint explicit handles and pass them as ordinary tool arguments (SEP-2567).
- The initialize / notifications/initialized handshake is removed. Every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in _meta. Clients SHOULD send io.modelcontextprotocol/clientInfo, and servers SHOULD return io.modelcontextprotocol/serverInfo in each result's _meta (SEP-2575).
- New server/discover RPC. Servers MUST implement it to advertise supported versions, capabilities and identity.
- The HTTP GET endpoint and resources/subscribe / resources/unsubscribe are replaced by subscriptions/listen, one long-lived POST response stream. Clients opt in to toolsListChanged, promptsListChanged, resourcesListChanged or resourceSubscriptions.
- ping, logging/setLevel and notifications/roots/list_changed are removed. Log level is set per request with io.modelcontextprotocol/logLevel in _meta.
- Tasks move out of core into the io.modelcontextprotocol/tasks extension. It polls with tasks/get, adds tasks/update, and removes tasks/result and tasks/list (SEP-2663).
- Multi Round-Trip Requests (MRTR) replace server-initiated roots/list, sampling/createMessage and elicitation/create. Servers return resultType "input_required" with inputRequests, and clients retry with inputResponses (SEP-2322).
- Every result has a required resultType field: "complete" or "input_required".
- SSE resumability (Last-Event-ID and SSE event IDs) is removed. If a stream breaks, the client MUST re-issue the request with a new request ID.
Minor changes include: required Mcp-Method and Mcp-Name headers on Streamable HTTP POSTs, plus x-mcp-header for mirroring tool parameters into headers. Results of tools/list, prompts/list, resources/list, resources/read and resources/templates/list must carry ttlMs and cacheScope. tools/list SHOULD return tools in a deterministic order. Resource not found moves from -32002 to -32602. The error codes are renumbered to HeaderMismatch -32020, MissingRequiredClientCapability -32021 and UnsupportedProtocolVersion -32022. inputSchema and outputSchema may use any JSON Schema 2020-12 keyword. notifications/elicitation/complete and the URL mode elicitationId are removed. Auth changes add RFC 9207 iss validation, a required application_type in Dynamic Client Registration, and credentials bound to the issuing authorization server.
Removed and deprecated features
Removed in 2026-07-28:
- Sessions and the Mcp-Session-Id header
- The initialize handshake
- The standalone HTTP GET stream, plus resources/subscribe and resources/unsubscribe
- ping, logging/setLevel and notifications/roots/list_changed
- SSE stream resumability via Last-Event-ID
- notifications/elicitation/complete and the elicitationId field
Deprecated (still works, scheduled for removal, minimum twelve-month window):
- Roots, Sampling and Logging (SEP-2577). Pass paths through tool parameters, resource URIs or config instead of Roots. Call LLM provider APIs directly instead of Sampling. Log to stderr or use OpenTelemetry instead of Logging.
- The HTTP+SSE transport from 2024-11-05. Migrate to Streamable HTTP.
- The includeContext values "thisServer" and "allServers". Omit the field or use "none".
- OAuth Dynamic Client Registration (RFC 7591), deprecated in favor of Client ID Metadata Documents.
What server authors must change
- Implement server/discover and return supportedVersions, capabilities, and serverInfo in _meta.
- Read the protocol version from each request's _meta. If you do not support it, return UnsupportedProtocolVersionError (-32022) that lists the supported versions. On HTTP, send it with 400 Bad Request.
- Drop session-keyed state. Use explicit handles in tool arguments, or requestState for MRTR flows. Treat requestState as attacker-controlled and protect it with HMAC or AEAD when it affects auth or business logic.
- Replace server-initiated elicitation, sampling and roots requests with an InputRequiredResult on tools/call, prompts/get or resources/read only.
- Add resultType to every result, and ttlMs plus cacheScope to list and read results.
- On Streamable HTTP, validate that MCP-Protocol-Version, Mcp-Method and Mcp-Name match the body. Reject mismatches with 400 and -32020. Answer GET or DELETE with 405, and ignore Mcp-Session-Id and Last-Event-ID.
- Emit notifications/message only for requests that include io.modelcontextprotocol/logLevel. Deliver list-changed notifications only on subscriptions/listen.
- A modern-only server SHOULD name its supported versions in the error it returns to a legacy initialize request.
InputRequiredResult (trimmed from the spec)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}What client authors must change
- Stop sending initialize. Put protocolVersion, clientCapabilities and, ideally, clientInfo in the _meta of every request.
- On HTTP, send MCP-Protocol-Version, Mcp-Method and, for tools/call, resources/read and prompts/get, Mcp-Name on every POST. Mirror any x-mcp-header tool parameters as Mcp-Param-{Name}, using the =?base64?...?= encoding for unsafe values. Leave out of tools/list any tool whose x-mcp-header annotations are invalid.
- Handle resultType "input_required". Fulfill inputRequests, then retry the original request with a new JSON-RPC id, the inputResponses, and the exact requestState. Treat results without resultType from older servers as "complete".
- Open subscriptions/listen for change notifications instead of the GET stream or resources/subscribe.
- If a response stream breaks, re-issue the request with a new id. There is no resumption.
- Use ttlMs and cacheScope to cache list results.
- For OAuth, validate a present iss before redeeming the code, set application_type in Dynamic Client Registration, and key stored credentials by issuer.
Streamable HTTP request (from the spec)
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}How to negotiate protocolVersion
There is no handshake. Every request declares its version, and the server accepts or rejects each request on its own. A client may call server/discover first to learn supportedVersions. It may also send any request inline and handle UnsupportedProtocolVersionError, then retry with a version from the supported list.
UnsupportedProtocolVersionError
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}Talking to legacy (2025-11-25 and earlier) servers:
- stdio: send server/discover first. If you get any error that is not a recognized modern error, or a timeout, fall back to initialize.
- Streamable HTTP: try a modern request. On 400, check the body. A recognized modern JSON-RPC error means the server is modern, so retry or fix the request. An empty or unrecognized body means you should fall back to initialize.
- Cache the result per server process (stdio) or per origin (HTTP).
- A dual-era server picks its behavior from the first request. Modern _meta gets stateless handling. initialize gets legacy semantics.
SDK versions that support 2026-07-28
The official blog says the four Tier 1 SDKs (TypeScript, Python, Go and C#) support the spec as of release, and Rust supports it in beta.
- Python: mcp v2.0.0 (released 28 Jul 2026) supports 2026-07-28 and still serves earlier revisions. Client(target) negotiates the version automatically. FastMCP is renamed MCPServer. v2.1.0 followed on 24 Aug 2026, and v2.1.1 is the latest release. To stay on v1, pin mcp>=1.28,<2. v1.x now gets only security fixes.
- TypeScript: the v2 split packages @modelcontextprotocol/server, @modelcontextprotocol/server-legacy, @modelcontextprotocol/client, @modelcontextprotocol/core, @modelcontextprotocol/node, @modelcontextprotocol/express, @modelcontextprotocol/hono, @modelcontextprotocol/fastify and @modelcontextprotocol/codemod are at 2.0.0. The v1 line (1.30.0) does not mention 2026-07-28.
- TypeScript codemod for upgrading v1 code (from the beta announcement): npx @modelcontextprotocol/codemod@beta v1-to-v2 .
- Go and C#: the beta announcement listed go-sdk v1.7.0-pre.1 and ModelContextProtocol 2.0.0-preview.1. Check their release pages for stable versions.
TypeScript v2: opt in on the client, serve both eras on the server
// Client: probes with server/discover, falls back to initialize
const client = new Client({ name: 'my-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
// client.getProtocolEra() returns 'modern' or 'legacy'
// Server: createMcpHandler serves 2026-07-28 and 2025-era by default (legacy: 'stateless')
const modern = createMcpHandler(factory, { legacy: 'reject' }); // modern only
// stdio: serveStdio(() => buildServer())Note: the TypeScript v2 client keeps the 2025 handshake by default. Set versionNegotiation to mode 'auto', or pin with mode { pin: '2026-07-28' }, to send modern traffic.
Get Claude Code settings changes by email
When a setting, flag or permission rule in this guide changes, it goes in the weekly AgentAtlas update, checked against the vendor docs. One email a week.
Sources
More on Claude Code
More Claude Code guides
- Claude Code: allow commands without prompts (permission modes and allow rules)
- Claude Code permissions: allow, ask and deny rules, rule syntax and allowed tools
- Claude Code telemetry and data retention: what is sent and how to turn it off
- CLAUDE.md: where it goes, how it loads, and how it works with AGENTS.md
- Claude Code hooks: format on save, block risky edits, get notified
- Claude Code subagents: create one, limit its tools, and call it
- Add MCP servers to Claude Code (claude mcp add, .mcp.json, scopes)
- Claude Code custom slash commands and skills (SKILL.md)
- Claude Code settings.json: file locations, precedence, and key settings
- Claude Code plugins and marketplaces: install, create, and share
- Claude Code headless mode: claude -p, --output-format json, --bare and CI flags
- Claude Code statusline: custom status bar in settings.json with context %, cost and git branch
- Claude Code /model: switch models, aliases (opus, sonnet, haiku, opusplan), ANTHROPIC_MODEL and effort
- Claude Code /cost and /usage: check token spend, usage limits and reduce costs
- Claude Code VS Code extension: install, shortcuts, settings and /ide (works in Cursor too)
- Subagents and Custom Agents Compared: Claude Code vs Codex vs Cursor vs Gemini CLI vs GitHub Copilot (2026)
Other guides
Reviewed Oct 13, 2026. Settings change often; the linked vendor docs are the source of truth.