How to Migrate an MCP Server to v2: A Step-by-Step Checklist
Purpose
OP on r/mcp framed v2 as roughly a week away and asked “how’s migration prep going on your end?” I had been treating the v2 upgrade as a version bump, but the two themes OP highlighted — stateless transport and sampling deprecation — are both breaking changes, not feature flags. So I wrote myself a checklist before touching the server, and this post is that checklist.
The goal is a focused, ordered migration plan, not a rewrite.
Environment
- MCP v1 server, multiple instances behind nginx
- Per-session state currently in process memory
- Sampling-based tool flow that delegates model calls to the host
- Target: MCP v2 (stateless transport, sampling removed)
The two hotspots
Everything in this checklist comes from two breaking changes. Naming them up front keeps the steps from feeling random:
1. Session state -> must leave the process (shared store or client)2. Sampling -> removed; server must call its own model or expose a normal toolIf a step does not touch one of these, it is regular plumbing and probably already works.
How to solve it?
I run through six steps in order. Each one is small enough to verify on its own.
Step 1: Audit in-process session state
Before changing anything, I list every place my server keeps per-session state in memory.
# Look for patterns like: self.session[...], _ctx[...], session_cache = {}grep -RnE "session\[|_ctx\[|session_cache|self\._[a-z_]+_cache" src/I write the matches down. Anything I find has to move before v2 will work for me.
Step 2: Externalize state
For each item from step 1, I decide where it lives now.
- Durable state (history, account lookups, long-lived context) -> Redis or a database.
- Ephemeral state (positions in a stream, transient hints) -> the client, sent back on each request.
The rule of thumb I use: if losing the instance would corrupt a user-visible flow, the state belongs in a shared store; if it would only cause a minor re-fetch, it can live on the client.
Step 3: Drop sticky-session routing
Once state is externalized, the load balancer no longer needs affinity. I switch nginx from sticky to a plain round-robin or least-connections upstream.
upstream mcp_v1 { sticky cookie=mcp_affinity; server 10.0.0.1:8080; server 10.0.0.2:8080;}upstream mcp_v2 { least_conn; server 10.0.0.1:8080; server 10.0.0.2:8080;}The :8080 in the server lines is the port my server listens on; the important change is removing the sticky cookie= line.
Step 4: Remove sampling
I have a separate post on sampling deprecation, so I keep this short. The move is to take every requestSampling(...) call and either bring the model call into my server or refactor the flow so the client passes a model choice as a normal tool argument.
Whatever I do, the sampling code path is a dead path under v2. I delete it rather than leaving it to throw at runtime.
Step 5: Test init-then-failover
This is the step that catches the mistake nobody notices in development: silent re-pinning. I run a literal failover test.
1. Start two v2 instances: A and B.2. Client sends init -> say it lands on A.3. Kill A.4. Client sends a tool call (the same session).5. Expect: B serves it successfully, because state is in the shared store.6. If it fails with "unknown session", step 2 was not actually stateless.I treat step 5 as the only proof that migration worked. Console logs saying “handled request” on B are not enough — B has to serve the continuing session without an error.
Step 6: Keep a rollback window
During the cutover I keep the v1 image warmed up. The migration touches both code and deployment, so if anything breaks under real load, I want to flip the upstream back without rebuilding.
The reason
I think the key reason a checklist matters here is that the two failure modes — sticky affinity and dead sampling — are silent until production. In dev, a single instance hides the first one, and a host with a model configured hides the second. The checklist forces me to touch both before they can hide.
Two common mistakes I want to avoid:
- Forgetting background subscriptions/resources. They held state in v1 and also need to be externalized or accepted as non-survivable. They are easy to miss because they are not in the main request path.
- Leaving sampling code as dead paths. It compiles, so it looks harmless, but under v2 it throws at runtime. Deleting it is faster than debugging a runtime error later.
Summary
In this post, I showed a six-step checklist for migrating an MCP server to v2: audit session state, externalize it, drop sticky routing, remove sampling, run a failover test, and keep a rollback window. The key point is that session state and sampling are the two hotspots, and both need explicit handling rather than a version bump.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments