MCPmatic

How to migrate an MCP server to spec 2026-07-28

Published · Updated · Written against MCP spec 2026-07-28

A practical upgrade path for MCP server authors moving to spec 2026-07-28. Most tools-only servers need a handful of changes, and an up-to-date SDK does much of the work. For background on each change, see what changed in 2026-07-28.

Upgrade the SDK#

Move to an SDK release that supports 2026-07-28. The MCP blog lists TypeScript, Python, Go and C# as Tier 1 SDKs with support, and Rust in beta. In TypeScript, use serveStdio with a factory that returns a fresh server each time; it negotiates per client, so you can serve both new and 2025-era clients during the transition. The Python quickstart requires SDK 2.0.0 or newer.

Remove session state#

There's no initialize handshake and no Mcp-Session-Id. Each request carries io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities and client info in _meta.

  • Remove anything keyed on the session id: caches, auth state, "current project" variables.
  • If tools need shared state, return an explicit handle from one tool and accept it as an argument on the next. Make handles random and bound to the user. See security best practices.
  • Behind a load balancer, any request can now reach any instance. Sticky sessions are no longer needed for MCP.

Answer server/discover#

Servers MUST implement server/discover, returning supported protocol versions, capabilities and server identity. SDKs that support 2026-07-28 should handle this; check that yours does by calling it once.

Return resultType and cache hints#

  • Every result carries resultType. Normal results use "complete".
  • List results such as tools/list carry ttlMs and cacheScope so clients can cache them.
  • Return tools in a deterministic order (the spec says SHOULD), so caches and diffs stay stable.

Replace server-to-client requests#

Servers no longer send requests to the client mid-call. When a tool needs more input, return a result with resultType: "input_required", an inputRequests object and, if you need it, an opaque requestState. The client gathers the input and sends the original request again, with a new JSON-RPC id, adding inputResponses and the requestState.

// First response: need confirmation
{
  "resultType": "input_required",
  "inputRequests": { "confirm": { "...": "elicitation request" } },
  "requestState": "opaque-state-from-server"
}

Treat requestState like any other client-supplied input: validate it, and don't trust it for authorization.

Clean up removed features#

  • ping and logging/setLevel are removed. Log level now comes per request in _meta.
  • SSE resumability is removed. A broken stream loses that request and the client resends it.
  • Tasks moved to the io.modelcontextprotocol/tasks extension.
  • Roots, Sampling and Logging are deprecated. Don't add new uses.

Check tools and test both eras#

  1. Paste your tools/list output into the MCP tool linter. It checks the 2026-07-28 rules, including x-mcp-header.
  2. Test with MCP Inspector using --protocol-era modern, then again with --protocol-era legacy. See how to test an MCP server.
  3. Watch for the new error codes in client logs: -32020 header mismatch, -32021 missing client capability, -32022 unsupported protocol version.

Sources#