Build Stack Review

MCP Server Versioning and Environment Compatibility

Protocol and server versions must move as one locked unit in production.

Senior Writer · · 11 min read
Cover illustration for “MCP Server Versioning and Environment Compatibility”
AI Agent Development Environments · September 13, 2026 · 11 min read · 2,364 words

MCP's version string looks like a date because it is one: the YYYY-MM-DD identifier marks the last time the protocol made a backward-incompatible change, not a sequential build number. Since November 2024, the protocol has moved through five published revisions, and at least two of them broke compatibility in ways a casual reading of the changelog would miss entirely. Most teams running MCP servers in production are still treating protocol version, server version, SDK version, and runtime as four dials that drift independently until something breaks. That's backwards. They need to move as one locked unit, and the rest of this piece is about why.

The breaking-change pattern across five revisions

Start with 2024-11-05. That revision stabilized the core schema: tools, resources, prompts, and an HTTP transport built on SSE, a streaming event-based protocol for delivering data. Everyone built on top of it.

Then came 2025-03-26. It introduced OAuth 2.1-style authorization, replaced SSE with Streamable HTTP, and added tool annotations. A reasonable engineer reading that changelog would assume 2025-06-18 continues the same trajectory. It doesn't. The 2025-06-18 revision removed JSON-RPC batching, a capability 2025-03-26 had added only months earlier. The spec's reasoning was blunt: batching lacked a compelling use case. Anyone who spent the spring writing batch-request logic against 2025-03-26 watched that logic get invalidated by summer. That same revision also reclassified MCP servers as OAuth Resource Servers, required Resource Indicators, added structured tool output, and mandated an MCP-Protocol-Version header on every HTTP request.

The 2025-11-25 revision continued the established session-based model before the next major structural change arrived. Then 2026-07-28 threw that model out. It made the protocol fully stateless: the initialize handshake is gone, protocol version and client capabilities now travel per-request rather than being established once at session start, and servers can expose their version and capabilities before a client commits to a full interaction. Updated SDKs for TypeScript, Python, Go, and C# shipped alongside it. Swapping per-session versioning for per-request versioning is a substantive change with real consequences. It's a structural change in how client and server agree on what dialect they're speaking. Backward compatibility with older servers is described in the protocol text, but that compatibility only works if someone has actually tested it against the servers running in a given environment. Nothing guarantees it by default.

Look at the transport layer alone across five revisions: SSE, then Streamable HTTP, then a stateless model with no handshake at all. That's not application code changing underneath a team. That's infrastructure assumptions getting invalidated across single revision gaps, the kind of thing a load balancer config or a proxy timeout setting was quietly built around and nobody remembered to revisit.

Four distinct version numbers that must stay in sync

Every MCP server carries four separate version identifiers, and nothing in the specification or in any SDK forces them to move together. Treating any one of them as a stand-in for the others is the mistake.

There's the protocol version, the YYYY-MM-DD revision the server actually implements. There's the server version, the string reported in serverInfo.version during initialization, the one a host like Claude Desktop or a debugging tool like Inspector shows to a human. There's the SDK version, the major version of whatever language SDK the server runs on; SDK minor versions are supposed to hold backward compatibility, and only major bumps are allowed to break things. And there's the runtime itself: the Node.js or Python interpreter, the system libraries, every resolved dependency the process leans on just to start.

These four numbers drift apart constantly. A GitHub discussion opened in May 2025 (modelcontextprotocol/modelcontextprotocol #1176) documented a published package reporting "version":"1.0.0" in its protocol response while its own package.json sat at 0.6.2, and the actual published package had already moved to a CalVer scheme reading 2025.4.28. Three numbers, one package, zero agreement between them. The fix proposed in that discussion was simple: serverInfo.version should match the semantic version of the associated package whenever one exists. A separate issue opened in November 2025 (#1915) asked for language-agnostic guidance on combining tool name and tool version, which tells you the versioning mess inside a single server hasn't been solved either, let alone the mess between servers and clients.

How tool-level versioning adds a fifth variable agents depend on silently

Inside a server, individual tools carry their own versioning problem. A tool name is supposed to work as a stable identifier, a tool version carries SemVer metadata, and a tool schema is the parameter fingerprint an agent actually reasons against when it decides how to call the thing.

Issue #1915 spells out exactly what practitioners are stuck on. Should the version live in the name itself, something like get_info_v1, or should the name stay fixed with version traveling as separate metadata? How many versions of a tool should a server keep alive at once? What happens by default when a client sends no tool_requirements at all? Issue #1915 references SEP-986 and SEP-1575 in the context of these questions, with ongoing discussion pointing toward keeping names stable and carrying version as separate metadata, so that clients aren't forced to chase name changes and the tool list in host interfaces doesn't bloat.

Here's the part that actually bites in production. On 4 September 2026, drift got documented across public MCP servers where tool parameter fingerprints changed with no corresponding change to the human-readable description. The schema moved. The words describing it stayed put. That matters because LLM clients cache tool definitions, and an agent that hasn't re-fetched the tool list keeps sending arguments shaped for the old schema. A stale cached schema doesn't throw a clean, reasoned error, it produces a malformed request or a silent mismatch that looks like a model failure when the actual fault sits in the environment layer underneath it.

The fix that's held up is a schema registry: store the approved fingerprint for each tool, keyed by tool name and server endpoint, and have CI compute the current fingerprint on every deployment and check it against the register. If they don't match, the pipeline halts for human review instead of shipping the drift silently. Making new fields optional, or running a versioned tool name during the transition window, keeps stale cached schemas from causing a hard failure before agents get a chance to re-register.

What environment drift looks like when MCP servers run in production

Cloudflare's MCP server for its own API, released in February 2026, has scaled to thousands of requests per second and served billions of tool calls. At that volume, a schema mismatch or a protocol version mismatch stops being a rare edge case anyone can shrug off. It becomes a recurring operational event. Community MCP servers now exist for a wide range of tools and platforms, and each one runs its own independent combination of protocol version, server version, SDK version, and runtime.

The failure modes repeat across all of them, and they repeat in the same four shapes. Protocol mismatch happens when client and server land on different revisions and nobody has actually tested the fallback between them. SDK drift happens when a server bumps its SDK's major version on a developer's laptop without doing the same in CI, staging, or production, so behavior quietly splits across the pipeline. Runtime drift shows up when the Node.js or Python version differs between a developer machine and a CI runner, changing transport or TLS behavior in ways that affect connection stability. And there's tool schema drift, already covered, the one that fails silently instead of loudly.

The /mcp endpoint under 2026-07-28 is built to accept both the new stateless protocol and older 2025-era Streamable HTTP clients. That backward compatibility is real, but only if the server is actually running the build that implements it. An environment pinned to an older release doesn't inherit that compatibility just because the spec document says it should be there. Backward compatibility is a feature of the protocol text. It is not a guarantee about any particular running environment, and treating it as one is how teams get surprised.

Why ad-hoc upgrades make MCP compatibility harder, not easier

When an MCP server starts misbehaving, the instinct is to upgrade something: bump the SDK, pull the latest server image, update the runtime. That instinct is wrong more often than it's right. Each of those moves changes one of the four layers in isolation, with no guarantee the other three stay compatible with whatever just changed.

The batching example is worth returning to, because it shows the trap in miniature. A team moving from 2025-03-26 to 2025-06-18 to pick up any of its genuine improvements was also forced to strip out every bit of batch-request logic it had built, because that capability had just been removed. Those two changes arrived bundled together, with no way to take the feature without absorbing the removal.

Without a locked, version-controlled definition of the environment, the word "upgrade" means something different on every machine it runs on. A developer who upgraded their SDK locally and a CI runner that never got the same update are now, functionally, running two different pieces of software that happen to share a name. The EOL MCP server, a tool that queries endoflife.date for software lifecycle status, is a small but concrete case of this: it hard-requires Node.js v18 or higher, and if the runtime drifts below that floor, the server breaks regardless of what its own version string claims. Ad-hoc upgrades also collide with agent caching in an ugly way. An upgrade that changes a tool schema invalidates every cached definition an agent is holding, but the person doing the upgrade has no lever to force that agent to re-fetch. The server moves on. The agent doesn't know it yet, and it won't find out until a request fails.

Treating protocol version, server version, SDK version, and runtime as a single lockable unit

The broader principle of codifying environments as standardized, version-controlled artifacts, cutting onboarding time and improving consistency across a team, is well established in developer workflow practice. That principle applies here directly, and it argues for one conclusion: stop managing these four numbers by hand.

A lockable MCP environment means the protocol version gets declared explicitly in server configuration rather than inferred from whatever the SDK happens to default to. It means the server version gets pulled programmatically from package metadata, package.json or pyproject.toml, instead of hardcoded by hand, consistent with the direction raised in discussion #1176. It means the SDK version sits pinned in a lock file, package-lock.json, uv.lock, go.sum, and stays in version control where it can't quietly drift. And it means the runtime itself, the interpreter, the system libraries, gets declared in the environment definition instead of inherited from whatever's already sitting on the machine.

The tool schema registry described earlier belongs in this same lockable unit as a fifth element: the approved fingerprint for each tool is itself a versioned artifact, and CI fails the build the moment that fingerprint drifts from what's on record. The 2026-07-28 stateless model, where a server can run in a single Worker with no stateful infrastructure behind it, cuts down on some runtime complexity, but the environment that produces that Worker still has to be reproducible on demand. The target worth aiming for is a git clone and one command that reliably produces an environment where all five pieces, protocol, server, SDK, runtime, and tool schemas, are pinned and checkable. Not a sixteen-step README full of caveats about which runtime version actually works this week. Teams that applied Nix-based environment management to comparable dependency problems have reported cutting new developer onboarding from two days down to twenty minutes. The same logic holds here.

Reproducible environment tooling that covers the MCP dependency stack

Four tools cover this ground in 2026, and picking between them comes down to how much of the MCP dependency stack actually needs to be pinned. Nix is the right default for anyone who needs the whole stack locked, not the easiest place to start.

Nix, and Nix Flakes specifically, cover every layer: runtime, SDK dependencies, and the system libraries underneath all of it. The environments it produces are bit-reproducible in the literal sense, and a single devenv.nix file under version control replaces the pile of README paragraphs, Brewfiles, and apt install scripts that usually accumulate instead. The tradeoff is a real learning curve; simple tasks can feel heavier than they should until the underlying concepts click. Nix fits teams that need identical environments across developer machines, CI, and deployment all at once, which is the full requirement MCP reliability actually demands.

Devbox sits on top of the Nix package manager and gives a gentler way into that same reproducibility guarantee. It provides per-project environments that stay isolated and reproducible without asking a team to learn Nix Flakes syntax on day one, and it kills the configuration drift that creeps in once everyone's local setup diverges slightly from everyone else's.

Dev Containers offer the lowest onboarding barrier of the four, native to VS Code and to cloud development environments. The tradeoff is Docker overhead, roughly 1.5 to 2 GB for a running container, a real cost on lower-spec hardware, and the abstraction can bury environment details in ways that make an MCP-specific bug harder to trace back to its source. Dev Containers make the most sense for Codespaces access or for onboarding contributors on locked-down corporate machines where installing anything else isn't an option.

Mise installs in about 30 seconds, works across platforms, and handles roughly 90% of runtime version management on its own. What it can't do is manage system libraries or guarantee binary reproducibility below the language runtime, a gap that matters for any MCP server carrying native dependencies.

One pattern a 12-developer team has reported success with combines all three: mise locally for fast day-to-day version switching, Nix Flakes running in CI for the full reproducibility guarantee, and a Dev Container for Codespaces access. Each layer produces the same pinned combination of protocol version, server version, SDK version, and runtime, so that no matter where the MCP server actually runs, it's running the environment it was built and tested against, not a nearby approximation of it.

Sources

  1. MCP release notes
  2. MCP Server Versioning Guidelines Proposal · modelcontextprotocol modelcontextprotocol · Discussion #1176
  3. Document recommended tool versioning and naming patterns for MCP servers (post SEP-986 / SEP-1575) · Issue #1915 · modelcontextprotocol/modelcontextprotocol
  4. EOL MCP Server | Awesome MCP Servers
  5. The next generation of MCP
  6. medium.com
  7. MCP’s biggest update removes the machinery many servers were built around

More in AI Agent Development Environments