HomeJournalThis post

MCP 2026 Transport Migration, Work Intact

Move from legacy sessions and resumable streams to stateless POST and request-scoped SSE with explicit version negotiation and durable work handles.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

MCP 2026 transport migration asks how to remove protocol sessions and resumable GET streams without abandoning long-running application work. This guide builds a revision gate, stateless POST handler, request-scoped SSE response, and explicit durable work ledger.

MCP 2026 transport migration removes session rails

Start with the dated specification, not with an undated recollection of Streamable HTTP. The 2026-07-28 revision defines an HTTP transport around independent POST requests and optional server-sent events scoped to the response. The prior 2025-11-25 revision included protocol-session and resumption mechanisms that a compatibility server may still encounter. A current implementation must not depend on those older rails for correctness.

The current Streamable HTTP section is normative for the new path, while the older transport is evidence for a deliberately supported adapter. Keep both documents dated in code comments and tests. MCP 2026 transport migration should delete accidental coupling, not erase an explicit compatibility promise without notice.

The committed compatibility matrix contains frozen request and response objects for both revisions. It classifies protocol mechanisms and application work fields without opening sockets or claiming observed client behavior. Its exact pass receipt comes from local assertions, making the migration argument reproducible even when server deployments differ.

Inventory every client revision, method, header, and connection assumption before editing code. That inventory becomes the retirement checklist and prevents an undocumented proxy dependency from masquerading as protocol behavior.

MCP transport migration mapLegacy session rails and resumable event tracks give way to stateless POST stops and request-scoped SSE branches.legacy session raillegacy resumable GET2026 stateless POSTrequest SSE branchapplication work ID continues
  • Dashed rails exist only in the compatibility adapter.
  • The current route accepts independent POST requests.
  • Durable work resumes through an application handle, not a transport session.
MCP transport migration map reading key
SignalInterpretation
MCP transport migration mapLegacy session rails and resumable event tracks give way to stateless POST stops and request-scoped SSE branches.
Figure 1: The new transport removes rails while application work keeps an explicit route.

Separate durable work from transport continuity

A tool can outlive the request that started it, but that does not require the transport protocol to preserve a session. Return an application-defined work handle from the accepted tool result, persist the work state under that handle, and expose a normal tool or resource operation that retrieves or cancels it. Authorization must be checked again on every operation.

The MCP tasks for long-running tools article covers the product-level state machine. A durable handle needs created, running, completed, failed, cancelled, and expired outcomes, plus an idempotency relationship to the initiating request. MCP 2026 transport migration is successful when killing a connection changes delivery, not whether the underlying authorized work exists.

Never substitute a legacy MCP session token for the work identifier. Session lifetime and business lifetime are different, and a session header may disappear in the current revision. The teaching fixture assigns an independent work_42 field in JSON-RPC result data; this name belongs to the local example, not to the protocol specification.

A work lookup should return an immutable status receipt or a versioned projection, never grant broader tool authority merely because the caller knows the handle string.

Accept independent POST requests

Design each current-revision POST so it can be authenticated, parsed, validated, routed, and completed without hidden state from a previous HTTP exchange. Request identity comes from JSON-RPC semantics and application authentication, not from connection affinity. If a load balancer sends the next request elsewhere, any required durable state must already be in a shared authorized store.

A stateless MCP transport still has stateful applications. Tool definitions, authorization grants, work handles, and server resources can persist; the transport simply does not create the older protocol-session contract. Use bounded request bodies, content-type validation, origin protections where appropriate, cancellation tied to request lifetime, and structured errors. MCP 2026 transport migration should make those ordinary HTTP boundaries more visible.

The compatibility matrix sends two current POST fixtures with no session header and expects both to route. A third fixture incorrectly requires a remembered session and must fail its local policy assertion. These cases demonstrate server logic, not benchmark throughput or availability.

Make shared state access explicit in dependency injection. A hidden map that happens to survive local tests will fail as soon as current requests land on different instances.

Use SSE only inside the response scope

A current request may receive a direct JSON response or an SSE stream according to the specification and negotiated content types. Treat that stream as a delivery shape for the request, not as a durable event bus. Close it on completion, cancellation, timeout, or transport failure, and let application work continue only if the tool contract explicitly accepted detached work.

Request-scoped SSE needs monotonically understandable JSON-RPC messages, proxy-safe headers, keepalive policy where documented, and backpressure handling. The SSE vs NDJSON comparison helps with framing trade-offs, but MCP fixes its own protocol rules. MCP 2026 transport migration must follow the dated spec instead of importing resume semantics from a generic SSE implementation.

Do not send Last-Event-ID as though it were current MCP resumption. A legacy adapter may interpret it only for the revision that defines it. The local matrix marks the header legacy_only and proves the current handler ignores it for work recovery, which instead uses the explicit application handle.

If a response stream breaks, record only delivery uncertainty. Do not relabel accepted detached work as failed until its application state machine reaches a terminal result.

Protocol negotiation state machineA request branches by advertised protocol revision into modern handling, legacy adapter, or explicit version error.requestversiongate2026 handlerlegacy or error
  1. Parse the revision before applying revision-specific headers.
  2. Route an explicitly supported old revision to a bounded adapter.
  3. Return a useful version error instead of guessing from missing headers.
Protocol negotiation state machine reading key
SignalInterpretation
Protocol negotiation state machineA request branches by advertised protocol revision into modern handling, legacy adapter, or explicit version error.
Figure 2: Negotiation is a finite protocol decision, not a chain of silent fallbacks.

The dated transport matrix pairs requests with responses and makes every current-versus-legacy routing decision inspectable.

Runnable artifact — mcp-transport-compat-matrix.mjs

import assert from "node:assert/strict";
const CURRENT = "2026-07-28", LEGACY = "2025-11-25", effects = [];
const cases = [
  { name: "current-a", request: { jsonrpc: "2.0", id: 1, version: CURRENT, method: "POST", session: null, lastEventId: null, work: "work_42" }, expected: { route: "current", delivery: "json", lastEventPolicy: "ignored" } },
  { name: "current-b", request: { jsonrpc: "2.0", id: 2, version: CURRENT, method: "POST", session: null, lastEventId: null, work: "work_42" }, expected: { route: "current", delivery: "json", lastEventPolicy: "ignored" } },
  { name: "current-last-event", request: { jsonrpc: "2.0", id: 3, version: CURRENT, method: "POST", session: null, lastEventId: "7", work: "work_42" }, expected: { route: "current", delivery: "json", lastEventPolicy: "ignored" } },
  { name: "current-session-dependency", request: { jsonrpc: "2.0", id: 4, version: CURRENT, method: "POST", session: "legacy-session", requiresSession: true, work: "work_42" }, expected: { route: "error", code: "current_session_dependency" } },
  { name: "legacy-resume", request: { jsonrpc: "2.0", id: 5, version: LEGACY, method: "GET", session: "s1", lastEventId: "7", work: "work_42" }, expected: { route: "legacy", delivery: "sse", lastEventPolicy: "legacy_only" } },
];
const handle = (fixture) => {
  const request = fixture.request;
  if (request.version === CURRENT && request.requiresSession) return { jsonrpc: "2.0", id: request.id, error: { code: "current_session_dependency" }, route: "error" };
  if (request.version === CURRENT && request.method === "POST") { effects.push(request.id); return { jsonrpc: "2.0", id: request.id, result: { work: request.work, delivery: "json", lastEventPolicy: "ignored" }, route: "current" }; }
  if (request.version === LEGACY && request.session) { effects.push(request.id); return { jsonrpc: "2.0", id: request.id, result: { work: request.work, delivery: "sse", lastEventPolicy: "legacy_only" }, route: "legacy" }; }
  return { jsonrpc: "2.0", id: request.id, error: { code: "unsupported_revision" }, route: "error" };
};
const pairs = cases.map((fixture) => ({ ...fixture, response: handle(fixture) }));
for (const pair of pairs) { assert.equal(pair.response.id, pair.request.id); assert.equal(pair.response.jsonrpc, pair.request.jsonrpc); assert.equal(pair.response.route, pair.expected.route); if (pair.expected.delivery) assert.equal(pair.response.result.delivery, pair.expected.delivery); if (pair.expected.lastEventPolicy) assert.equal(pair.response.result.lastEventPolicy, pair.expected.lastEventPolicy); if (pair.expected.code) assert.equal(pair.response.error.code, pair.expected.code); }
assert.equal(effects.includes(4), false);
assert.equal(pairs.find((pair) => pair.name === "current-last-event").response.result.lastEventPolicy, "ignored");
assert.equal(pairs.find((pair) => pair.name === "legacy-resume").response.result.lastEventPolicy, "legacy_only");
assert.equal(new Set(pairs.filter((pair) => pair.response.result).map((pair) => pair.response.result.work)).size, 1);
console.log(JSON.stringify(pairs.map(({ name, response }) => ({ name, route: response.route, result: response.result || response.error }))));
console.log("PASS: revision routing keeps work handles transport-independent");

Run node mcp-transport-compat-matrix.mjs. Expected receipt: PASS: revision routing keeps work handles transport-independent.

Negotiate the MCP protocol version explicitly

Parse the protocol-version signal required by the specification before choosing revision-specific behavior. Maintain an allowlist of revisions the server implements. Route the current value to the stateless handler, a supported older value to a compatibility adapter, and every other value to a documented error. Missing or malformed version data should follow the specification rather than a home-grown heuristic.

An MCP protocol version is a contract switch. Once selected, headers, methods, response forms, and error expectations must all come from that revision. Do not use a current handler until it fails and then silently retry legacy logic; work may already have been accepted. MCP 2026 transport migration needs a decision before side effects.

The MCP sampling deprecation is a reminder that protocol features evolve independently. Pin conformance fixtures to revision strings and revisit them when the specification changes. The finite-state figure makes every fallthrough visible so an unsupported client receives a useful migration path rather than ambiguous transport errors.

Include the selected revision in every structured error and trace span. That single field turns many apparent JSON-RPC bugs into explainable routing or compatibility failures.

Build a narrow legacy adapter

Compatibility code should translate one older wire contract into the same internal request model used by the new handler. Keep legacy session lookup, GET stream handling, and event resumption inside that adapter. Do not leak session assumptions into tool implementations or durable work storage. Add telemetry for revision use so removal is based on actual supported-client policy.

Define an end condition: client versions covered, notice period, error message after sunset, and the release that deletes the adapter. A legacy MCP session can remain functional during migration without becoming the architecture's permanent center. The current transport stays the default and receives all new conformance work.

MCP 2026 transport migration also requires hostile compatibility tests: unknown session, expired session, duplicate request, resumed stream with an unavailable event, and a work handle created before the client disconnects. The local artifact covers only protocol routing and header parity; deployment-specific stream storage still needs its own committed tests.

Keep adapter limits symmetric where possible: authentication, body size, tool authorization, and work idempotency should not weaken merely because a client speaks the older revision.

Header and body parity consoleFour paired gauges compare protocol version, session header, last event identifier, and work handle rules.protocol version: matchsession: current rejects dependencyLast-Event-ID: legacy onlywork-id: application body fieldPASS / EXPLAIN
MechanismCurrent revision
Session headernot a durable-work primitive
Last-Event-IDnot current resumption
Work handleapplication-defined
Header and body parity console reading key
SignalInterpretation
Header and body parity consoleFour paired gauges compare protocol version, session header, last event identifier, and work handle rules.
Figure 3: The console reports a precise mismatch before any work is accepted.

Prove header and body parity before cutover

Create a table for every mechanism that changed: revision advertisement, HTTP method, accepted content types, session header, Last-Event-ID, response-scoped stream, JSON-RPC identifier, and application work handle. For each row, name current behavior, legacy behavior, and the rejection or translation rule. This table is more reliable than a prose promise that the server remains backward compatible.

Run the same domain request through both adapters and normalize the accepted tool name, arguments, work ID, terminal result, and error category. A mismatch should stop cutover until it is explained. Do not require wire equality where the standards intentionally differ. MCP 2026 transport migration preserves domain meaning while changing transport mechanics.

Add the structured concurrency guide at the request executor boundary so a disconnected current request cancels only work that was not explicitly accepted as durable. Record whether a tool was inline or detached; otherwise operators cannot tell if continued activity after disconnect is a leak or the intended contract.

A parity failure needs a named disposition—intentional protocol difference, adapter defect, or unsupported legacy behavior—before the cutover report can be considered complete.

Cut over with dated evidence and rollback

Deploy the current handler behind revision routing, observe compatible clients, and keep legacy traffic isolated. Alert on version errors, unsupported methods, missing content types, handler ambiguity, duplicate work claims, and requests that depend on connection affinity. Preserve a rollback that changes routing, not application work records, so accepted durable tasks remain inspectable across releases.

Document the exact specification dates, server release, conformance suite, compatibility sunset, and work-handle schema. MCP 2026 transport migration should be revisited when a new dated revision appears or when the compatibility population reaches its planned threshold. Do not call a living or dated standard permanently settled.

The end state is straightforward: current POST requests stand alone, SSE belongs to one response, protocol version selects one handler before effects, and durable work continues through authorized application IDs. That is enough to remove lost-work fear without preserving transport machinery the current revision no longer defines.

Practice rollback after current requests have created durable handles. Routing can revert to the old server, but the new work records must remain readable and cancellable.