Upgrading to MCP TypeScript SDK v2 Does Not Select the New Protocol
The MCP TypeScript SDK keeps the 2025 handshake by default, even on v2 packages. This guide explains legacy, auto, and pinned negotiation for the 2026-07-28 protocol revision, plus what to verify before calling the migration complete.
You can upgrade an MCP client to the TypeScript SDK v2 packages and still send the same 2025-era initialization handshake as before.
The package version and the wire protocol revision are separate choices. The official MCP TypeScript SDK migration guide says a hand-constructed Client, Server, or McpServer keeps speaking the 2025-era protocol by default. The 2026-07-28 revision requires an explicit opt-in.
I have not captured these handshakes on a test server yet, so this is a close reading of the official guide rather than a firsthand benchmark. The migration rule is still clear enough to act on: checking the installed SDK version does not tell you which protocol era reached the wire.
What the default client does
This client keeps the legacy negotiation behavior:
const client = new Client({
name: 'my-client',
version: '1.0.0'
});
await client.connect(transport);
Client.connect() sends the same 2025 initialize handshake used by v1.x. Upgrading the dependency can change the API available to your application while leaving the connection on the old protocol era.
That distinction matters during a staged migration. A deployment can look current in package.json and still behave like a 2025 client at runtime. If the application or the server expects features that only exist in the 2026-07-28 revision, the package upgrade alone has not completed the work.
Three negotiation choices
The client-side switch is ClientOptions.versionNegotiation. The guide documents three modes.
Keep the 2025 behavior
Leaving the option absent, or selecting legacy, skips the discovery probe and preserves the current handshake.
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'legacy' } }
);
This is useful when byte-stable behavior matters or when a tool must keep working with a known 2025-only server.
Probe, then fall back
auto asks the server whether it supports the modern era. When the server is 2025-only and the client’s supported-version list still includes a legacy revision, the SDK falls back to the old handshake.
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }
);
await client.connect(transport);
client.getProtocolEra(); // 'modern' | 'legacy'
This is the compatibility choice. It lets one client connect to modern and legacy servers, with one extra discovery round trip when negotiation runs.
The fallback is deliberately limited. Authentication failures and server errors are not treated as proof that the server is old. The SDK rejects those failures instead of silently switching eras. A modern-only supported-version list also removes the legacy fallback.
Require the 2026 revision
A pin removes the fallback:
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{
versionNegotiation: {
mode: { pin: '2026-07-28' }
}
}
);
The connection succeeds only when the peer supports the pinned modern revision. A 2025-only server causes connect() to reject with SdkError(EraNegotiationFailed).
This is the enforcement choice. It is appropriate when using the old era would be a configuration error and a failed deployment is safer than an unnoticed fallback.
Why auto is not a universal default
The guide calls out spawn-per-invocation command-line tools and debugging tools as an exception.
Some legacy stdio servers do not answer an unknown request before initialize. An automatic probe can then wait for its timeout before falling back. The probe also changes raw transcripts, which is a real problem for tools built to observe the wire byte for byte.
Those tools may be better off preserving legacy as the default and exposing auto or a pinned revision as an explicit flag. Compatibility has a cost when startup time or stable protocol captures are part of the product.
The stdio implementation has another useful detail. With the SDK’s own StdioClientTransport, the modern probe runs in a short-lived sibling process. The actual session process starts after the SDK knows the protocol era. This avoids spending the caller’s only child process on a probe that may make an older server exit.
The server side also needs an explicit path
The same rule applies to servers. Connecting a hand-constructed McpServer directly to StdioServerTransport continues to serve the 2025 era.
For HTTP, the v2 entry point is createMcpHandler(factory). Its default setup can serve the 2026-07-28 protocol per request while retaining a stateless legacy path. For stdio and other long-lived connections, the documented migration entry is serveStdio(() => buildServer()).
This makes client and server upgrades symmetrical in one important way: installing v2 gives you the new implementation, but you still have to choose the wire behavior.
A migration check that proves the right thing
Do not stop at npm ls or a lockfile diff. Verify the negotiated era at runtime.
- Choose the intended policy:
legacy,auto, or a pinned revision. - Connect to every server class your deployment supports.
- Read
client.getProtocolEra()after the connection. - Test the expected fallback or rejection against a 2025-only server.
- Keep one captured handshake for the default path and one for the modern path.
For auto, test both branches. A successful connection only proves that one of them worked. It does not tell you whether the SDK selected the modern era or quietly used the allowed legacy fallback.
For a pin, the failure case is part of the contract. A 2025-only server should be rejected. If it connects, the configuration is not enforcing the revision you intended.
Verdict
Treat the SDK version as deployment metadata and the negotiated protocol era as runtime evidence.
Use auto when one client must remain compatible with 2025-only servers. Pin 2026-07-28 when fallback would hide a migration error. Keep legacy when a probing round trip or a changed wire transcript would damage the tool’s purpose.
The migration is complete when the runtime selects the era you intended, not when the new package appears in the lockfile.
Documentation reading, not firsthand protocol testing. Source: the official MCP TypeScript SDK guide, Supporting protocol revision 2026-07-28.