How do you version and evolve an MCP server's tool/resource API without breaking existing clients?

5 minadvancedmcpversioningapi-designbackward-compatibility

Quick Answer

Treat your server's tool/resource/prompt surface as a public API with the same discipline you'd apply to a REST API: adding a new optional argument or a new tool is safe and backward-compatible, but renaming a tool, removing a required argument's old meaning, or changing what a tool returns in an incompatible way will break any client (or, more likely, any model) that learned the old shape from earlier interactions or cached tool descriptions. Prefer additive changes (new tools, new optional parameters with sensible defaults) over breaking ones; when a genuine breaking change is unavoidable, consider versioning the tool name itself (create_issue_v2) alongside the old one for a deprecation window, and always send notifications/tools/list_changed (if you declared that capability) so connected clients refresh their view instead of operating on a stale, cached tool list.

Detailed Answer

A running MCP server's tool catalog is effectively a public API surface. There's no formal versioning scheme baked into the protocol for individual tools, though, the way there is for the protocol itself.

Safe, additive changes

# Before
@mcp.tool()
def create_issue(repo: str, title: str) -> str: ...

# After: adding an optional parameter with a sensible default is backward-compatible
@mcp.tool()
def create_issue(repo: str, title: str, labels: list[str] = []) -> str: ...

Existing callers (and any model that already learned the old shape from a prior session) keep working exactly as before, since the new field is optional.

Breaking changes to avoid, or handle carefully

# Breaking: renamed the tool entirely
# create_issue -> open_issue   (any client expecting create_issue now fails outright)

# Breaking: changed the meaning of an existing field without renaming it
# "priority" used to be 1-5, now it's "low"/"medium"/"high" — same field name, different semantics

A rename or a required-field removal isn't just a code change. It invalidates whatever the model, and any cached tool-list UI in a host, already believes about the tool.

Managing an unavoidable breaking change

  • Ship the new behavior under a new tool name (create_issue_v2) alongside the old one for a deprecation window, rather than mutating create_issue's contract in place.
  • Update the old tool's description to mention it's deprecated and point at the replacement, so a model reading the current tool list is nudged toward the new one.
  • Send notifications/tools/list_changed the moment the catalog changes, so already-connected clients refetch rather than continuing to operate on a stale list they cached at session start.

Why this discipline matters more here than in a typical API

Unlike a human developer reading a changelog, an LLM calling your tool has no changelog to read. Its only knowledge of your API is whatever's in the current tools/list response, or what it happens to remember from earlier in the conversation. Breaking changes surface as confusing, silent failures rather than a clear compile-time or documentation-driven signal. That makes backward compatibility even more valuable here than for a typical human-consumed API.

Related Resources