MCP 2026-07-28 Migration: Ship a Dual-Era Server
Migrate an MCP server to 2026-07-28 without breaking legacy clients. Use dual-era routing, state handles, conformance tests, and rollout gates.

Do not switch production MCP traffic to 2026-07-28 yet. Build and test a dual-era adapter now, keep legacy clients on 2025-11-25, and cut over only after the final specification and a production-supported SDK release pass your own conformance and workload gates.
The production decision: prepare now, cut over after final
The safe move is to separate migration work from production activation. The 2026-07-28 release candidate was locked on May 21, 2026, but the final specification is scheduled for July 28, 2026 after a ten-week validation window. It contains breaking changes, so treating the release candidate as a routine dependency bump is the wrong risk model.
The official TypeScript SDK makes that line explicit. Its v2 packages are beta and implement 2026-07-28; v1.x remains the supported production release until stable v2 ships. Stable v2 is expected alongside the final specification on July 28, 2026, and v1.x is scheduled to keep receiving bug fixes and security updates for at least 6 months after v2 ships. Check the live TypeScript SDK status when you execute the cutover, not when you open the migration branch.

For a platform team, that produces a clean environment split:
What changes at the server boundary
The migration belongs in the protocol adapter, not in every tool implementation. 2026-07-28 removes the transport session and handshake, then makes each request carry enough information to stand alone. Your domain services should still receive an authenticated principal, validated tool arguments, and an execution context regardless of which protocol era arrived at the edge.
The draft changelog turns that boundary into a concrete diff:
A conforming modern HTTP request includes the protocol version in both the header and _meta, plus client identity and capabilities. Mcp-Method is required for every request, and Mcp-Name is also required for tools/call, resources/read, and prompts/get. This request is a useful first smoke test against a staging endpoint:
curl -sS "$MCP_URL" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: health/read' \
--data-binary @- <<'JSON'
{
"jsonrpc": "2.0",
"id": "migration-canary",
"method": "tools/call",
"params": {
"name": "health/read",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "migration-canary",
"version": "candidate"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
JSONThe request is deliberately boring. A health tool with no side effects tells you whether routing, metadata validation, authentication, response framing, and tracing work before a business tool complicates the diagnosis. If you are still defining the domain boundary, start with the production MCP server pattern for internal APIs and keep version-specific behavior outside the tool handlers.
Build one dual-era protocol adapter
One endpoint can serve both eras, but the adapter must make the choice deterministically. The official versioning specification calls an implementation that supports both modern and legacy revisions dual-era. A request carrying the required modern _meta selects stateless semantics; an initialize request selects legacy semantics. Both may run concurrently on the same endpoint or process.
The classification layer should be small enough to test exhaustively. This TypeScript module captures the minimum boundary without coupling it to a particular server framework:
type RpcRequest = {
method: string;
params?: Record<string, unknown> & {
name?: string;
uri?: string;
_meta?: Record<string, unknown>;
};
};
const PROTOCOL = "io.modelcontextprotocol/protocolVersion";
const CLIENT = "io.modelcontextprotocol/clientInfo";
const CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
const NAMED_METHODS = new Set(["tools/call", "resources/read", "prompts/get"]);
export function selectMcpEra(
headers: Headers,
rpc: RpcRequest,
): "modern" | "legacy" {
const meta = rpc.params?._meta;
const modern = Boolean(meta?.[PROTOCOL] && meta?.[CLIENT] && meta?.[CAPABILITIES]);
if (modern) {
const headerVersion = headers.get("mcp-protocol-version");
if (headerVersion !== meta?.[PROTOCOL]) throw new Error("HeaderMismatch");
if (headers.get("mcp-method") !== rpc.method) throw new Error("HeaderMismatch");
if (NAMED_METHODS.has(rpc.method)) {
const expectedName = rpc.params?.name ?? rpc.params?.uri;
if (headers.get("mcp-name") !== expectedName) throw new Error("HeaderMismatch");
}
return "modern";
}
if (rpc.method === "initialize") return "legacy";
throw new Error("UnrecognizedProtocolEra");
}This classifier is not the whole compliance layer. The modern adapter must also implement server/discover, return UnsupportedProtocolVersionError with code -32022 and its supported versions, and map malformed required metadata to -32602 with 400 Bad Request. An unsupported method returns 404 Not Found with JSON-RPC code -32601. Keep those errors typed: clients use the body to distinguish a modern rejection from a legacy endpoint.
Separate era detection
Parse enough JSON-RPC to identify the method and
_meta, then select the adapter before creating any protocol session. Do not let a legacy session cookie orMcp-Session-Idoverride a valid modern request.Share domain handlers
Normalize both adapters into one internal call shape: principal, tool name, validated arguments, trace context, approval state, and an abort signal. Tool code should not know whether
initializeran.Keep errors explicit
Return recognized modern errors for version, capability, header, and metadata failures. Do not convert them into a generic
400, because dual-era clients need the JSON-RPC body to decide whether to retry or fall back.Instrument both paths
Log the chosen era, requested version, client identity, method, name, auth issuer, trace ID, result type, and typed failure. Never log access tokens, raw authorization codes, or unrestricted tool arguments.

The dangerous fallback is automatic downgrade after any 400. A dual-era HTTP client should inspect the response body. If it contains a recognized modern JSON-RPC error, correct the request or retry a supported version. Fall back to initialize only when the body is empty or not a recognized modern error. Otherwise a metadata bug, unsupported capability, or header mismatch silently turns into a legacy request and hides the fault you need to fix.
Move hidden session state into explicit handles
Stateless MCP removes protocol state, not application state. The replacement is an explicit server-minted handle returned by one tool and passed back as an ordinary argument on later calls. That handle can identify a browser, deployment, basket, workflow, or other durable domain object.
Consider an illustrative deployment tool. deploy/start authorizes the caller, creates a durable record, queues the work, and returns deployment_id: "dep_alpha". A later deploy/status call must supply that handle:
{
"jsonrpc": "2.0",
"id": "deployment-status",
"method": "tools/call",
"params": {
"name": "deploy/status",
"arguments": {
"deployment_id": "dep_alpha"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "release-console",
"version": "candidate"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}The server resolves dep_alpha only after authenticating the request and proving that the current principal may access it. Any replica can continue the workflow because ownership and progress live in the application store, not in load-balancer affinity or a process-local MCP session.
Treat the handle as an untrusted reference, not as authorization. Make it opaque, bind it to a tenant or principal in storage, apply expiry where the domain requires it, and make repeated reads idempotent. Log the handle type and an internal correlation value, but avoid exposing sensitive state in the handle itself.
This architecture also makes failures easier to recover. A worker restart loses no hidden conversation state. A retry can resolve the same durable object. A support engineer can trace the handle from the MCP edge through the queue and downstream API without reconstructing which server instance minted a session.
Replace callbacks and long connections deliberately
Use input_required for bounded additional input on an active operation, and use the Tasks extension for work that must survive disconnection or process restart. They solve different lifecycle problems and should not share an improvised queue abstraction.
For a destructive tool, the modern server can pause before execution and return an input request. Use input_required, with an underscore. The live changelog, schema, and tools specification use that form; inputRequired appears in the original release-candidate example and should not enter a production contract.
{
"jsonrpc": "2.0",
"id": "delete-request",
"result": {
"resultType": "input_required",
"inputRequests": {
"approval": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Approve the requested deletion?",
"requestedSchema": {
"type": "object",
"properties": {
"approved": { "type": "boolean" }
},
"required": ["approved"]
}
}
}
},
"requestState": "opaque_signed_state"
}
}The client presents the decision, then retries the original method with inputResponses and the echoed requestState. The retry uses a different JSON-RPC ID. Sign or encrypt requestState if the client carries it, bind it to the principal and original operation, reject replays after completion, and do not perform the side effect until the accepted response reaches the server.
The MCP Tasks extension is the durable path. Both client and server opt in with io.modelcontextprotocol/tasks. A server may then return resultType: "task" with a taskId, TTL, and suggested polling interval. It must create the task durably before returning the handle. Clients use tasks/get, tasks/update, and tasks/cancel; cancellation is cooperative and the task may still reach another terminal state.
The production test is not whether the happy path completes. Restart the client, restart the worker, route the next poll to another replica, and verify that the same task can continue without duplicate side effects. Then cancel during each meaningful phase and confirm that the terminal state reflects what actually happened.
Make routing, caching, and traces enforceable
The new metadata is useful only when gateways and observability systems verify it. Treat the body as the source of truth, validate every mirrored header against it, and reject disagreements before routing or rate limiting. MCP-Protocol-Version must match request _meta; Mcp-Method and, where required, Mcp-Name must match the JSON-RPC request.
That gives the gateway safe policy dimensions without parsing tool arguments. You can route read-heavy resource calls separately, apply a stricter policy to mutation tools, and produce method-level saturation metrics. Do not trust mirrored headers on traffic that does not identify a protocol version requiring header/body validation.
Caching also becomes explicit. The draft requires ttlMs and cacheScope on tool, prompt, and resource list/read results. cacheScope is public or private; set it from the authorization semantics of the result, not from endpoint naming. A per-user tool list is private even if every user calls the same URL. A public resource still needs invalidation behavior that respects ttlMs.
Trace propagation uses the reserved _meta keys traceparent, tracestate, and baggage. Preserve them through the adapter, task store, worker, and downstream API so a single trace shows where the tool waited, retried, requested approval, or failed. A minimal structured event can look like this:
{
"protocol_version": "2026-07-28",
"protocol_era": "modern",
"mcp_method": "tools/call",
"mcp_name": "deploy/start",
"auth_issuer": "https://identity.example",
"trace_id": "trace_alpha",
"result_type": "complete",
"outcome": "accepted"
}Add typed events for discovery, unsupported versions, missing capabilities, header mismatches, fallback decisions, input requests, task transitions, cache scope, and downstream side effects. That event model tells you whether a rollout problem is protocol negotiation, authorization, domain behavior, or infrastructure.
Close the authorization and schema breaks
Authorization and schema compatibility deserve separate release gates because both can fail after the transport looks healthy. Reusing legacy session assumptions in either layer creates the most expensive kind of migration bug: a request that reaches the correct tool under the wrong identity or with subtly different validation.
For authorization, the draft says clients must validate a present iss value against the recorded issuer before redeeming the authorization code. Persisted client credentials are issuer-bound: key them by issuer, never reuse them with another authorization server, and re-register when the authorization server changes. Clients must also set an appropriate OpenID Connect application_type during Dynamic Client Registration. The deeper flow and threat boundaries are covered in MCP authorization for production servers.
For schemas, the draft expands tool input and output schemas to JSON Schema 2020-12 and allows structuredContent to be any JSON value. Do not automatically dereference external $ref URIs, and bound schema depth and validation time. Run your golden tool corpus through the new validator and compare accepted inputs, rejected inputs, normalized outputs, and model-visible error messages. The change from resource-not-found code -32002 to -32602 also needs an explicit client regression test if anything branches on the literal code.
Roots, Sampling, and Logging are deprecated, not immediately removed. The documented replacements are tool parameters, resource URIs, or server configuration for Roots; direct provider APIs for Sampling; and stderr on stdio or OpenTelemetry for Logging. The normal feature policy provides at least twelve months before removal eligibility. Keep existing compatibility where clients need it, but do not add new dependencies on deprecated capabilities.
Gate the rollout with conformance and production telemetry
The rollout is ready when the modern adapter passes protocol conformance and produces the same authorized domain outcomes as the legacy path. Either gate alone is insufficient: conformance cannot prove your business semantics, and workload tests can miss a protocol edge that another client will exercise.
Run the draft suite explicitly. The default active server suite excludes draft-spec scenarios, so a green default run does not validate 2026-07-28:
npx @modelcontextprotocol/conformance \
server \
--url "$MCP_URL" \
--suite draftThe official MCP Conformance Test Framework writes server results to checks.json. Unexpected failures exit 1, normal passes exit 0, and a stale expected-failure baseline also exits 1. That last behavior matters: when a previously expected failure starts passing, CI forces you to remove the stale exception instead of letting the baseline grow forever.

Freeze the legacy baseline
Capture a golden corpus of discovery, list, read, prompt, and tool calls with their authorization decisions, normalized results, errors, and side-effect records. This is semantic evidence, not a packet replay full of secrets.
Exercise both eras
Run the corpus through
2025-11-25and2026-07-28adapters against the same domain handlers. Differences must be intentional protocol changes, not drift in permissions or tool behavior.Test failure transitions
Send missing metadata, mismatched headers, unsupported versions, unknown methods, expired handles, rejected approvals, dropped streams, worker restarts, and duplicate retries. Assert the typed protocol error and the absence or idempotence of side effects.
Shadow modern requests
For safe read operations, translate selected legacy traffic into modern requests in a non-authoritative environment. Compare outputs and traces without allowing the shadow path to mutate production state.
Canary known clients
Enable modern semantics only for clients whose SDK version and capability set you can identify. Keep the legacy path live and make rollback a routing change, not a redeploy.
Retire by evidence
Remove legacy behavior only after the client inventory is complete, modern errors are understood, semantic parity holds, tasks survive disruption, and the rollback window has closed under your change policy.
Use a release scorecard that forces an owner to answer each failure mode:
The decision rule is blunt: do not turn on the modern path because the calendar reached the final release date. Turn it on when the supported SDK you actually deploy passes conformance, your client set negotiates correctly, and your production controls make every failure visible and reversible.
Frequently asked questions
How do I migrate an MCP server from 2025-11-25 to 2026-07-28 without breaking clients?
Deploy a dual-era adapter. Select modern semantics from the required per-request _meta, preserve initialize for legacy clients, route both into shared authorized domain handlers, and canary the modern path behind conformance and workload gates.
Is MCP 2026-07-28 final?
No. The release candidate was locked on May 21, 2026, and the final specification is scheduled for July 28, 2026. The current TypeScript SDK v2 line is beta, while v1.x remains the supported production release until stable v2 ships.
Does stateless MCP mean the application cannot keep state?
No. The protocol session disappears, but application state remains in durable domain storage. Return an opaque handle from one tool call, require later calls to pass it back, and authorize every resolution against the current principal.
Should a production TypeScript MCP server use SDK v2 beta now?
Use v2 beta in a migration branch, staging environment, and conformance lane. Keep production on the supported v1.x line until the final specification and stable v2 release pass your workload, authorization, and rollback gates.
Build an MCP Server
Design or migrate a production MCP boundary with dual-era compatibility, authorization, conformance tests, observability, and a controlled rollout.
Jul 11, 2026






