You update the MCP SDK, TypeScript compiles again, and the test suite passes.
Production goes live and the client still performs initialize. The server still reads Mcp-Session-Id. The load balancer remains sticky. Some tools still depend on server-to-client requests, and an older Tasks wire path is still running.
This migration failure is easy to produce because the SDK version and the protocol era actually running on the wire can diverge.
MCP 2026-07-28 is not an upgrade you complete by adopting a few new APIs. It changes protocol sessions, per-request metadata, server-to-client interaction, Tasks, HTTP transport behaviour, authorisation, and the status of several older features.[1]
The useful measurement is how much production traffic still depends on the 2025-era wire contract, session assumptions, and feature semantics. Package version alone cannot answer that.
Part 01 followed state after the protocol session disappeared. Part 02 looked at how multi-round interaction, durable work, routing, caching, and tracing continue without recreating one hidden session.
Part 03 closes the series with the operational question: how does a production MCP client or server cross a protocol era without cutting off every older counterpart at once?
Reading assumption: if the state model from Part 01 is still unfamiliar, read that first; if MRTR, Tasks, and routing headers are new, Part 02 will make this migration guide much easier to follow. Part 03 assumes those foundations and is aimed at people maintaining an MCP client, server, gateway, or production integration.
Version scope: this article targets MCP
2026-07-28and the current specification, Deprecated Features registry, and Tier-1 SDK documentation available on 18 August 2026. MCP is still moving quickly, so the sources should be checked again before publication. SEP-2596 is now Final. The current Deprecated Features registry still expresses HTTP+SSE earliest removal as “three months after SEP-2596 reaches Final”; this article follows the registry’s current wording rather than inventing a calendar date.[5][7]
Start by checking which protocol era is actually on the wire
The official TypeScript SDK v2 migration guide makes this unusually concrete. Code can already be on the v2 packages without putting 2026-07-28 bytes on the wire. The client has to select the modern era through versionNegotiation, and the server has to use the relevant modern serving entry.[3]
That makes a good first migration test.
If the migration plan is only:
npm update
npm test
a green CI run does not prove that:
initialize/initializedhave left the production pathMcp-Session-Idno longer carries application assumptions- session-local state has moved into explicit application state
- server-initiated requests have moved to MRTR or another current lifecycle
- old change notifications have moved to
subscriptions/listen - experimental Tasks have moved to the current Tasks Extension
- HTTP+SSE or 2025-era Streamable HTTP compatibility has an exit plan
- the OAuth flow satisfies the current issuer and resource-binding requirements
- DCR usage has fallen far enough to make a CIMD-first path safe
- Roots, Sampling, and Logging dependencies have been inventoried
I would therefore begin with a protocol migration inventory, not a package upgrade.
Inventory the legacy dependency before changing code
A practical inventory can be divided into seven surfaces:
| Area | What to look for | 2026-07-28 direction |
|---|---|---|
| Protocol lifecycle | initialize, initialized, Mcp-Session-Id | per-request metadata, server/discover |
| State | session-local memory, sticky affinity, shared protocol-session store | explicit application state / durable backend |
| Server-to-client interaction | elicitation, sampling, roots request flows | MRTR / current input lifecycle; migrate deprecated features separately |
| Notifications | long-lived unsolicited list/resource notifications | subscriptions/listen |
| Long-running work | experimental 2025 Tasks, long synchronous requests | current Tasks Extension |
| HTTP transport | GET/DELETE session semantics, Last-Event-ID, legacy HTTP+SSE | 2026-07-28 Streamable HTTP |
| Authorisation / governance | old issuer assumptions, DCR, Roots/Sampling/Logging | current Authorisation, CIMD, documented migration paths |
The value of the inventory is not the checklist itself.
It forces a harder question:
Which assumptions live only inside the SDK, and which have leaked into the load balancer, Redis, OAuth server, browser host, observability pipeline, and business application?
Once a session has shaped the surrounding architecture, the migration is no longer a search-and-replace exercise.
Modern-first does not mean “fall back on every 400”
The 2026-07-28 Streamable HTTP specification defines an important backward-compatibility rule.
A client that supports both a modern per-request-metadata era and a legacy initialize era may send a modern request first. If the server answers 400 Bad Request, the client must not assume that the server is legacy.[2]
A modern server also uses 400 for cases such as:
- unsupported protocol version
- missing required client capability
- header-validation failure
The decision is closer to:
Send modern request
↓
Success
↓
Stay modern
400 Bad Request
↓
Inspect response body
├─ Recognised modern JSON-RPC error
│ ↓
│ Correct / renegotiate modern request
│
└─ Empty or not a recognised modern error
↓
Fall back to initialize-era flow
That distinction matters because blind fallback can hide a broken modern request.
A missing header can quietly turn into “the server must be old”, and the client spends another year on the legacy path without producing an obvious incident.
The official TypeScript SDK packages this into versionNegotiation. mode: "auto" probes with server/discover and can fall back to a 2025-only server, while a pin: "2026-07-28" configuration refuses to downgrade.[3]
That is an SDK implementation pattern, not a protocol requirement that every language expose the same API or default.
The migration runbook should therefore record the negotiated protocol era, not merely the package version.

Figure 3-1 | The migration unit is the protocol era. Detect modern and legacy traffic, keep compatibility paths observable, move the underlying semantics, then drain legacy traffic. Dual-era serving is a migration strategy, not an MCP-mandated deployment topology.
Dual-era serving is useful only when it has an exit
The same principle applies on the server.
The official TypeScript SDK v2 createMcpHandler() can serve 2026-07-28 on one endpoint and, by default, also handle stateless 2025-era traffic through its legacy mode.[3]
An existing sessionful v1 deployment may need a different transition:
Incoming request
↓
Legacy classifier
├─ legacy → existing legacy handler
└─ modern → strict modern handler
That is useful because a team can:
- add a modern lane
- keep the legacy lane temporarily
- observe which traffic uses each lane
- remove the remaining legacy dependencies
- retire the old handler only after the evidence says it is safe
One boundary matters:
Dual-era support is transition architecture, not a protocol requirement.
If the production stack still contains an indefinite:
if legacy:
old_stack()
else:
new_stack()
with no telemetry, drain target, or removal criterion, compatibility has become permanent architecture.
That is not a completed migration.
Routing is only the first step; the hard work is in the old semantics
Correctly classifying modern and legacy requests only moves traffic to the right code path.
The difficult work is removing the old assumptions behind that path.
Do not move session state into another hidden corner
Suppose an older server uses:
Mcp-Session-Id
↓
in-memory session object
↓
browser context / basket / workspace / workflow progress
The modern answer is not:
someSingletonMap[clientId]
followed by a declaration that the server is stateless because the header disappeared.
That leaves the application coupled to one process.
The Part 01 boundary has to become real architecture:
- protocol context travels with the request
- application state is located through explicit handles
- durable state lives in the application backend
- authorisation remains an independent per-call decision
Server-to-client requests are not migrated by renaming a method
The 2026-07-28 era removes the old server-to-client JSON-RPC request channel.[3]
If a tool needs client input during execution, the modern path can return input_required and let the client retry the original request through MRTR.
If durable background work needs input halfway through, the Task lifecycle uses tasks/get and tasks/update.
Both can expose an input-required state. They do not share state ownership or continuation semantics.
Change notifications have a different lifetime too
A 2025-era connection can rely on long-lived unsolicited list_changed or resources/updated behaviour.
The modern model moves change notifications to subscriptions/listen.[3]
If a client UI assumes:
Once connected, the server will push every change over the same ongoing connection.
that assumption belongs in the migration inventory.
“Streamable HTTP” is not precise enough
Protocol revisions from 2025-03-26 through 2025-11-25 also used the Streamable HTTP name, but with a different contract. That era could still:
- mint
Mcp-Session-Id - open a standalone SSE stream with HTTP GET
- terminate a session with HTTP DELETE
- resume streams with
Last-Event-ID - send server-initiated JSON-RPC requests on SSE
None of those mechanisms are part of the 2026-07-28 revision.[2]
A migration document that only says:
Transport: Streamable HTTP
has not actually identified the protocol era.
Authorisation belongs in the migration plan too
For a protected remote MCP server, the transport migration should not be separated from OAuth assumptions.
The current Authorisation specification makes authorisation OPTIONAL overall. When an HTTP-based MCP implementation supports it, it SHOULD conform to the current Authorisation specification.[4]
Several checks belong in the migration plan.
Validate the authorisation response issuer
Before redirecting the user-agent, the client records the expected issuer from validated authorisation-server metadata.
On the response:
- a present
issmust be compared with that recorded issuer - if metadata advertises
authorization_response_iss_parameter_supported=trueandissis absent, the response is rejected - the comparison must not invent URI normalisation such as host case folding or default-port removal[4]
This is part of the trust boundary against OAuth mix-up problems.
Bind the token request to the intended MCP resource
Clients must use RFC 8707 Resource Indicators and include the intended MCP server as the resource in both authorisation and token requests.[4]
The MCP server must then validate that the access token was issued for itself as the intended audience.
The migration question is therefore not only:
Can we decode this token?
It is:
Was this token actually issued for this MCP resource?
Prefer CIMD without pretending DCR has already disappeared
The direction of the current Authorisation specification is clear:
- authorisation servers and MCP clients SHOULD support Client ID Metadata Documents (CIMD)
- Dynamic Client Registration MAY remain for backward compatibility
- the current Deprecated Features registry lists DCR as Deprecated, with CIMD as its migration path[4][5]
That still does not justify switching off DCR before the fleet is ready.
If an authorisation server, client population, or enterprise identity layer does not yet support CIMD correctly, disabling DCR creates an availability failure, not a clean migration.
A safer rollout is:
Add CIMD support
↓
Measure registration path
↓
Prefer CIMD where available
↓
Keep DCR fallback for legacy counterparts
↓
Drain DCR usage
↓
Remove when compatibility permits
Once again, the important word is telemetry.
Deprecated is not Removed, and the clocks are not all the same
The easiest way to get Part 03 wrong is to compress every deprecated feature into:
MCP gives you a year and then deletes it.
The current Deprecated Features registry is more specific.[5]
Features deprecated in 2026-07-28
The registry currently lists:
- Roots
- Sampling
- Logging
- Dynamic Client Registration
Their earliest removal is:
the first revision released on or after 2027-07-28
That means eligible for removal, not automatically removed at midnight on 28 July 2027.
The registry explicitly says that actual removal is a Core Maintainer decision during release preparation and may occur later.[5]
Grandfathered deprecations
Legacy HTTP+SSE has a different history.
It has been described as deprecated since 2025-03-26, before the feature-lifecycle policy. The current registry treats it as a grandfathered case, points migration to Streamable HTTP, and expresses earliest removal as:
Three months after SEP-2596 reaches Final[5]
SEP-2596 is now Final.[7] The current Deprecated Features registry still records HTTP+SSE earliest removal as “Three months after SEP-2596 reaches Final”.[5]
Until the registry itself provides a more specific date, a production runbook should reference that current condition directly rather than manufacture a calendar date.
It also means the concise “year-long offramp” wording in the release blog is not the source I would use for an exact removal schedule.[1]
For operational planning, use the current registry for current feature status and earliest-removal conditions.

Figure 3-2 | Roots, Sampling, Logging, and DCR entered Deprecated in 2026-07-28; HTTP+SSE is a grandfathered case. Earliest removal is an eligibility condition, not an automatic removal date. Status shown is checked as of 18 August 2026.
Where should each deprecated feature move?
MRTR is not a universal replacement.
The current registry and SEP-2577 point in different directions for different responsibilities:[5][6]
| Deprecated feature | Migration direction |
|---|---|
| Roots | tool parameters, resource URIs, server configuration |
| Sampling | direct integration with LLM provider APIs |
| Logging | stderr for stdio; OpenTelemetry for structured observability |
| DCR | Client ID Metadata Documents (CIMD) |
| HTTP+SSE | Streamable HTTP |
That table exposes a broader design shift.
MCP is not trying to make every application or infrastructure responsibility a core protocol primitive.
Some responsibilities return to tool contracts.
Some use standard OAuth.
Some use OpenTelemetry.
Some move into extensions.
The replacement often lives at a different abstraction layer, so an old-method-to-new-method lookup table is not enough.
A production migration can move through five observable gates
For a remote MCP server with real users, I would avoid a single cutover.
Gate 1: establish the baseline
Measure the old world first.
At minimum:
% of requests using initialize-era flow
% of requests carrying Mcp-Session-Id
GET / DELETE / Last-Event-ID traffic on the MCP endpoint
legacy HTTP+SSE connections
server-initiated elicitation / sampling / roots calls
experimental 2025 Tasks usage
DCR vs CIMD registration path
Roots / Sampling / Logging negotiation or invocation
modern protocol errors and fallback reasons
Without those numbers, “we can probably remove the old path” is intuition, not a migration decision.
Gate 2: ship the modern lane without removing legacy
Clients can probe modern first.
Servers can add a modern handler.
Gateways can start recognising MCP-Protocol-Version and modern request metadata.
The objective is not immediate fleet conversion.
It is to prove that the modern path completes real workloads.
Gate 3: migrate semantics and harden authorisation
This is where most of the work normally lives:
- session-local state → explicit handle + durable backend
- server-to-client interactive flow → MRTR
- long-running work → current Tasks Extension
- change notifications →
subscriptions/listen - 2025 transport assumptions → 2026 per-request model
- hard-coded wire/error assumptions → current protocol semantics
Routing a request to the modern handler without moving these semantics usually exposes the remaining coupling quickly.
Harden authorisation in the same migration gate
Where OAuth is present, test:
- issuer validation
resourceparameter- token audience validation
- CIMD support
- DCR fallback telemetry
- scope / step-up behaviour
Do this before the end of the rollout rather than discovering that the modern client reaches the server but cannot complete authorisation.
Gate 4: drain legacy
Set explicit exit criteria, for example:
legacy traffic < internal threshold
no critical customer depends on a legacy-only path
no unexplained modern→legacy fallback
deprecated-feature usage is understood
DCR fallback fits the accepted compatibility budget
rollback path has been tested
There is deliberately no universal 1% or 0.1% threshold here.
A public MCP server, an internal agent platform, and an enterprise gateway have different compatibility costs.
The threshold is a product and risk decision, not a protocol constant.
Gate 5: remove
Only now remove:
- the legacy handler
- sticky-routing rules
- the protocol-session store
- old transport endpoints that are no longer required
- deprecated feature implementations
- fallback-only tests and dashboards
- obsolete incident and migration runbooks
Removal is the last migration action, not the first one.
Where migrations quietly get stuck
The SDK changed, therefore the protocol changed
The TypeScript SDK migration guide is enough to disprove this assumption.[3]
Observe negotiated protocol behaviour.
Every 400 means legacy
Modern servers also return 400.
Blind fallback hides modern configuration errors.[2]
Dual-era works well, therefore keep it forever
A compatibility path without a drain plan becomes the next legacy path.
Mcp-Session-Id is gone, but state still lives in instance RAM
That is a stateless façade over an instance-bound application.
DCR says Deprecated, therefore disable DCR now
The current specification still permits DCR for backward compatibility.[4]
Build CIMD coverage first, then drain DCR.
Deprecated means Removed
The current registry explicitly says that no features have yet been removed under this policy.[5]
A launch-blog sentence becomes the removal calendar
Release posts are useful for direction.
Exact operational status and earliest-removal conditions belong in the current specification and registry.[1][5]
HTTP+SSE is the clearest example of why that distinction matters.
A final checklist for the exit decision
| Area | Ready to migrate? |
|---|---|
| Protocol era | modern / legacy can be distinguished without blind fallback |
| Client | modern negotiation is enabled and observable |
| Server | modern handler completes real workloads independently |
| State | no dependency on protocol session or hidden instance-local state |
| Interaction | MRTR / Tasks / subscriptions lifecycles are separated |
| Transport | 2025 Streamable HTTP and HTTP+SSE dependencies are inventoried |
| Authorisation | issuer, resource/audience, CIMD/DCR paths are tested |
| Deprecations | Roots / Sampling / Logging / DCR / HTTP+SSE each have an owner |
| Observability | era, fallback reason, and deprecated usage are measurable |
| Rollback | modern rollout can be reversed without improvising data recovery |
| Removal | legacy traffic meets the organisation’s exit criteria before deletion |
The useful part of the checklist is not that every cell eventually says yes.
It is that each cell has an owner, measurement, and exit criterion.
Three parts, three questions
Part 01 asked:
Where does state go after the session disappears?
The answer was that protocol-session state disappears, while application state, domain data, and authorisation responsibilities remain and become more explicit.
Part 02 asked:
How do interaction and long-running work continue without a session?
MRTR, Tasks, headers, caching, and Trace Context each take a narrower lifecycle rather than rebuilding one universal session.
Part 03 asks the final production question:
How does an existing system cross that boundary safely?
The job is to make the protocol era observable, migrate the old semantics one by one, and remove compatibility only after legacy traffic no longer has a reason to exist.
The migration is not complete when package.json contains a newer version.
It is complete when the architectural assumptions that only the old protocol era required have stopped receiving production traffic.
References
- Model Context Protocol, The 2026-07-28 Specification, 28 July 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/
- Model Context Protocol, Streamable HTTP: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- MCP TypeScript SDK, Supporting protocol revision 2026-07-28. https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28
- Model Context Protocol, Authorization: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- Model Context Protocol, Deprecated Features: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- Model Context Protocol, SEP-2577: Deprecate Roots, Sampling, and Logging. https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging
- Model Context Protocol, SEP-2596: Specification Feature Lifecycle and Deprecation Policy. https://modelcontextprotocol.io/seps/2596-spec-feature-lifecycle-and-deprecation