On this page
- First: identify which revisions you actually support
- Do not infer protocol behavior from endpoint shape
- Separate conversation logic from protocol-version logic
- Build a feature matrix before changing code
- Migrate transport state separately from application state
- Remove the assumption that initialization is mandatory
- Keep a compatibility path for older servers if needed
- Modern requests must serialize required routing headers
- Validate header/body agreement in tests
- Move interactive workflows to MRTR
- Do not map requestState into old session state
- Migrate elicitation UI independently
- Migrate Tasks as a new extension, not a renamed core API
- Persist task IDs during migration
- Replace unsolicited notification assumptions
- Do not assume notifications are durable
- Account for deprecations
- OAuth migration needs its own checklist
- Avoid destructive credential migrations
- Re-key caches when identity semantics change
- Preserve tool permissions carefully
- Tool-schema changes may require permission review
- Persist old and new protocol metadata separately
- Fallback should be explicit and bounded
- Never treat 401 as protocol mismatch
- Keep user-visible errors precise
- Create migration fixtures before touching production code
- Test mixed fleets
- Test app upgrade with persisted legacy state
- Test app downgrade if you support it
- Make protocol logs version-aware
- Avoid leaking raw protocol objects into UI code
- Prefer capability checks over version-number branching when possible
- Do not trust self-reported client/server metadata for security
- Roll out modern behavior incrementally
- Define a deprecation policy for your client too
- A migration checklist
- Where BYOKchat fits
- Further reading
MCP migrations are not ordinary dependency upgrades.
A new SDK can compile while your application still implements the wrong protocol lifecycle.
The 2026-07-28 revision is a good example because it changes several foundational assumptions at once:
- modern requests are stateless;
- the protocol-level
initialize/initializedexchange is removed from the modern wire format; Mcp-Session-Idis removed for modern requests;server/discoveris available but optional;- MRTR replaces the old server-to-client request pattern for interactive input;
- Tasks move into an extension with a redesigned lifecycle;
- change notifications move to
subscriptions/listen; - OAuth behavior is hardened;
- several older features/transports are deprecated.
A safe migration therefore starts with architecture, not search-and-replace.
First: identify which revisions you actually support
Do not say:
supports MCP
internally.
Track something closer to:
supports legacy initialize/session revisions
supports 2026-07-28 stateless revision
supports MRTR
supports Tasks extension
supports subscriptions/listen
A protocol profile makes version-sensitive behavior explicit.
Do not infer protocol behavior from endpoint shape
This is fragile:
/mcp -> modern
/sse -> legacy
Endpoint naming is not a reliable protocol contract.
Use protocol version/capability information and your configured transport strategy.
Separate conversation logic from protocol-version logic
The conversation orchestrator should ask for operations such as:
list tools
call tool
listen for changes
continue input-required operation
poll task
It should not know whether the transport underneath uses:
legacy initialize + session
or:
modern stateless request
A clean boundary looks like:
This makes migrations local instead of invasive.
Build a feature matrix before changing code
For each supported revision, document behavior.
Example:
| Area | Older lifecycle | 2026-07-28 modern lifecycle |
|---|---|---|
| Startup | initialize / initialized | no required handshake |
| Session header | may use Mcp-Session-Id | removed |
| Discovery | tied to initialized capabilities | server/discover optional |
| Interactive input | server-to-client request channel | MRTR / input_required |
| Notifications | session/connection-oriented | subscriptions/listen opt-in |
| Tasks | older experimental design | io.modelcontextprotocol/tasks extension |
This table becomes your migration checklist.
Migrate transport state separately from application state
A common legacy client has an object like:
MCPConnection {
sessionId
initialized
capabilities
socket/stream
}
That mixes configuration, protocol session, and transport connection.
Refactor toward:
MCPServerConfiguration -> durable
ProtocolProfile -> durable/cached
HTTP request -> transient
Subscription -> reconnectable
Task record -> durable
Tool operation -> durable host state
Then removing modern sessions becomes much easier.
Remove the assumption that initialization is mandatory
Legacy flow:
connect
-> initialize
-> initialized
-> list tools
Modern flow:
configured endpoint
-> optionally server/discover
-> list tools / call tool directly
If every request in your host waits on isInitialized == true, that abstraction must change.
Keep a compatibility path for older servers if needed
Supporting 2026-07-28 does not require abandoning older servers immediately.
A migration layer can choose:
modern path when supported
legacy path when configured/negotiated
But do not mix states.
For example, a modern request should not accidentally carry a stale legacy session ID.
Modern requests must serialize required routing headers
The 2026-07-28 Streamable HTTP binding requires modern routing/version headers such as:
MCP-Protocol-Version
Mcp-Method
Mcp-Name where applicable
During migration, make header generation part of the modern request serializer.
Do not bolt headers on in UI/network middleware that does not understand the JSON-RPC operation.
See MCP Streamable HTTP Explained.
Validate header/body agreement in tests
Migration bugs often look like:
body method changed
header builder still uses legacy code
Add fixtures for:
wrong Mcp-Method
missing Mcp-Name
wrong protocol version
mirrored parameter mismatch
The new adapter should catch these deterministically.
Move interactive workflows to MRTR
If old client code waits for server-initiated elicitation requests on a persistent connection, that design does not map directly to the modern stateless core.
Modern lifecycle:
send request
<- input_required
persist workflow state
collect input
retry original request with inputResponses + requestState
Refactor around a logical operation that spans multiple wire requests.
See MCP Multi-Round-Trip Requests Explained.
Do not map requestState into old session state
requestState is continuation data for an MRTR operation.
It is not:
replacement Mcp-Session-Id
Do not use it to key every server request or store global connection state.
Bind it only to the logical operation that received it.
Migrate elicitation UI independently
Old code may assume:
server sends request
client immediately responds on same channel
Modern code should support:
input_required persisted
UI can wait
app can background/restart
response validated
new retry sent later
This is a lifecycle improvement, not just a protocol rewrite.
Migrate Tasks as a new extension, not a renamed core API
The 2026-07-28 Tasks design differs from the earlier experimental lifecycle.
Important modern expectations include:
- extension opt-in;
- server-directed task creation;
tasks/get;tasks/update;- cancellation support according to the extension;
- no broad
tasks/listassumption; - optional notifications through subscriptions.
Treat this as a new task adapter.
See MCP Tasks Explained.
Persist task IDs during migration
If the old client depended on session-scoped task discovery/listing, move to app-owned persistence of task handles.
Migration step:
CreateTaskResult received
-> write local task record immediately
On restart, reconcile saved non-terminal tasks with tasks/get.
Replace unsolicited notification assumptions
Modern MCP uses subscriptions/listen for opt-in notification streams.
If old code assumes:
connect once
server may push any notification
refactor to:
open explicit listen stream
request supported notification categories
handle acknowledgment
reconnect when dropped
reconcile authoritative state
See How MCP Tool Discovery Works for list-change handling.
Do not assume notifications are durable
During migration, avoid recreating session semantics on top of subscriptions.
A disconnected listener may miss changes.
Use notifications to trigger refresh/reconciliation rather than treating them as the sole data store.
Account for deprecations
The 2026-07-28 generation deprecates several older capabilities/patterns, including legacy HTTP+SSE and older core features such as Roots, Sampling, and Logging.
If your product currently depends on them:
- identify which user-facing feature uses them;
- keep the legacy path while required;
- do not build new product features on deprecated behavior;
- plan an explicit replacement/removal path.
A deprecation warning in release notes is not enough if your architecture still depends on the feature.
OAuth migration needs its own checklist
The current MCP authorization model includes hardening around issuer validation and resource binding, and moves client registration toward Client ID Metadata Documents while deprecating DCR over time.
Review:
expected issuer
resource indicator/audience
client credential issuer binding
authorization response issuer validation
PKCE
redirect URI handling
stored credential migration
DCR fallback vs CIMD path
Do not reuse old OAuth client registrations across issuers automatically.
See OAuth for MCP Explained and How to Secure Remote MCP Servers.
Avoid destructive credential migrations
If existing users have working legacy OAuth credentials, migrate carefully.
Possible strategy:
keep old credential profile
mark registration method/version
use it for legacy-compatible flow
create new modern profile only when reauthorization is needed
Do not silently delete refresh tokens and force every user to reconnect unless required for security/correctness.
Re-key caches when identity semantics change
A legacy discovery cache may be keyed by:
endpoint URL
Modern multi-account/version-aware behavior may need:
connection ID
credential profile
protocol revision
cache scope
Migration should invalidate old entries that cannot be mapped safely.
Stale capability data is less dangerous than leaking one account’s capabilities into another.
Preserve tool permissions carefully
A tool permission record from an old version might be:
create_issue -> Always Allow
If modern identity is now:
(server connection, account, tool name, contract fingerprint)
migrate only when the old record can be mapped unambiguously.
Otherwise default to Ask and let the user reauthorize behavior.
Security-sensitive migration should prefer losing convenience over widening permission accidentally.
Tool-schema changes may require permission review
Protocol migration can expose richer/different schemas.
If a tool materially changes from read-only to mutable behavior, invalidate broad Always Allow assumptions.
A local contract fingerprint can help detect material changes.
Persist old and new protocol metadata separately
During a transition, a connection record might include:
interface ProtocolState {
preferredRevision?: string
lastSuccessfulRevision?: string
compatibilityMode: "auto" | "modern" | "legacy"
discoveredCapabilities?: CapabilitySnapshot
}
Avoid one boolean such as isModern if you need to reason about fallback and last-known behavior.
Fallback should be explicit and bounded
A migration client may try modern behavior and fall back to a supported legacy revision.
But avoid indefinite guessing:
try revision A
fail
try B
fail
try C
...
Define a supported set and deterministic fallback rules.
Do not downgrade on authentication or application errors that have nothing to do with protocol compatibility.
Never treat 401 as protocol mismatch
This is a classic migration bug.
If modern request returns:
401 Unauthorized
that usually means auth needs attention, not that the client should automatically retry using an older MCP revision.
Likewise:
429 -> rate limit
500 -> server failure
Only protocol-specific incompatibility signals should trigger version fallback.
Keep user-visible errors precise
Good:
This MCP server uses a protocol revision this app does not support.
Better with diagnostics:
Requested: 2026-07-28
Server/compatibility response: ...
Bad:
Connection failed
when the real problem is version mismatch.
Create migration fixtures before touching production code
Capture representative old behavior:
legacy initialize success
legacy session tool call
legacy notification
legacy elicitation
legacy task behavior if supported
Then add modern fixtures:
stateless tools/list
stateless tools/call
input_required MRTR
Tasks extension
subscriptions/listen
modern OAuth cases
Your migration is complete only when both suites behave intentionally.
Test mixed fleets
A real user may configure:
Server A -> modern
Server B -> legacy
Server C -> modern with Tasks
Server D -> modern without Tasks
The application should handle them concurrently.
Do not store global protocol state such as:
currentMCPVersion = ...
Version/capability state belongs per connection/request.
Test app upgrade with persisted legacy state
Install previous app version, create:
server configs
permissions
cached tools
active tasks if applicable
OAuth credentials
Then upgrade to the new client build.
Verify migrations of persisted schemas, not only fresh-install behavior.
Test app downgrade if you support it
If older app versions may read the same local database, adding new enum values/state can break downgrade.
Use versioned persistence or migration guards.
At minimum, ensure the app fails safely rather than corrupting user data.
Make protocol logs version-aware
Useful diagnostics:
connection ID
chosen protocol revision
transport adapter
operation method/name
fallback attempted? why?
MRTR round
Tasks extension enabled?
subscription status
This turns field reports into actionable information.
Avoid leaking raw protocol objects into UI code
If UI views depend directly on SDK-specific response types, every SDK migration becomes a UI migration.
Normalize into app models:
ToolDefinition
ToolExecutionState
InputRequest
TaskState
SubscriptionState
Then map SDK/protocol types underneath.
Prefer capability checks over version-number branching when possible
Some behavior should be decided by actual advertised support.
For example:
supports Tasks extension?
is better than:
version >= X therefore Tasks
But version numbers still matter for wire semantics.
Use both appropriately:
revision -> protocol framing/lifecycle
capabilities/extensions -> optional features
Do not trust self-reported client/server metadata for security
Modern metadata may include client/server names and versions for diagnostics/display.
Do not use them for:
permission bypass
TLS trust
OAuth issuer choice
feature security policy
They are self-reported metadata, not authenticated security identities.
Roll out modern behavior incrementally
A safe product rollout can be:
- add modern adapter behind internal flag;
- run compatibility tests;
- enable for new known-modern endpoints;
- collect local/sanitized failure diagnostics;
- enable auto-selection;
- retain legacy mode for configured older servers;
- remove deprecated paths only when support policy allows.
Avoid one release that rewrites transport, auth, permissions, and persistence with no staged testing.
Define a deprecation policy for your client too
Your app should document:
which MCP revisions are supported
which are deprecated
when older paths may be removed
This prevents legacy compatibility code from becoming permanent by accident.
A migration checklist
Before shipping:
[ ] modern requests do not require initialize/session
[ ] modern headers/body match
[ ] legacy and modern adapters are isolated
[ ] discovery cache is version/account aware
[ ] MRTR persists logical operation state
[ ] requestState is opaque and bounded
[ ] Tasks extension is explicit and durable
[ ] subscriptions/listen reconnects and reconciles
[ ] OAuth issuer/resource validation is current
[ ] permissions do not widen during migration
[ ] persisted legacy connections upgrade safely
[ ] protocol fallback ignores unrelated 401/429/500 errors
[ ] fake-server integration suite covers both generations
Where BYOKchat fits
A provider-neutral local-first client benefits from keeping MCP protocol behavior behind a dedicated client layer.
Conversations, projects, tool permissions, and persisted tool rounds can remain stable while the MCP adapter evolves from older lifecycle assumptions to the current stateless revision.
That is the goal of a good protocol migration:
change the wire behavior without changing the user’s mental model or weakening security.