Semantic Versioning & Backward-Compatible Tool Evolution

13 min read Module 6 of 10 Topic 16 of 30

What you'll learn

  • Classify a tool schema change as backward-compatible or breaking
  • Version tool names explicitly and run multiple versions side by side during a migration window
  • Deprecate an old tool version safely once all callers have migrated
  • Recognize and handle silent behavior changes that break callers without any schema change at all
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

An MCP server that changes tool contracts without a versioning discipline will eventually break some agent that was built against the old shape, often silently, since the failure shows up as a confusing tool-call error rather than an obvious deployment issue. Treat tool schemas with the same rigor as a public API, and be aware of the specific gap this discipline does not close on its own: changes to behavior that leave the schema itself untouched.

Classifying Changes: Backward-Compatible vs. Breaking

flowchart TD
    Change["Proposed schema change"] --> Q1{"Does it remove a\nfield, change a type,\nor add a required param?"}
    Q1 -->|yes| Breaking["Breaking change:\nrequires new tool version"]
    Q1 -->|no| Q2{"Is the change additive\nand optional, with a\ndefault preserving old behavior?"}
    Q2 -->|yes| Compatible["Backward-compatible:\nsame tool version, no migration needed"]
    Q2 -->|no| Breaking

    style Change fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style Q1 fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style Q2 fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style Breaking fill:#fef2f2,stroke:#dc2626,color:#0F172A
    style Compatible fill:#f0fdf9,stroke:#0D9488,color:#0F172A

Backward-compatible: adding an optional field with a sensible default, adding a new tool entirely, widening an enum’s allowed values, improving a description without changing behavior. Breaking: removing or renaming a field, changing a field’s type, adding a new required parameter, or changing what the tool’s output structure looks like.

Versioning Tool Names Explicitly

MCP has no built-in version negotiation for individual tools, so the established convention is encoding the version directly in the tool name, so old and new clients can coexist against the same server during a migration window.

class KBQueryInputV1(BaseModel):
    query: str
    top_k: int = 5

class KBQueryInputV2(BaseModel):
    query: str
    top_k: int = 5
    doc_type: str | None = None      # New: filter by document type
    min_relevance: float = 0.0        # New: minimum relevance threshold

@mcp.tool(name="kb_query/v1", description="[DEPRECATED, use kb_query/v2] Search the knowledge base")
async def kb_query_v1(input: KBQueryInputV1) -> list[dict]:
    """Kept running unchanged for agents still on the old contract."""
    return await _kb_search(input.query, input.top_k)

@mcp.tool(name="kb_query/v2", description="Search the knowledge base with document-type filtering")
async def kb_query_v2(input: KBQueryInputV2) -> list[dict]:
    """New callers get filtering and relevance thresholds."""
    return await _kb_search(input.query, input.top_k, doc_type=input.doc_type, min_relevance=input.min_relevance)

The Expand-Contract Migration Sequence

sequenceDiagram
    participant OldAgent as Agent (v1 client)
    participant NewAgent as Agent (v2 client)
    participant Server as MCP Server

    Note over Server: Phase 1: Expand, register both versions
    OldAgent->>Server: tools/call kb_query/v1
    Server-->>OldAgent: Result (unchanged behavior)
    NewAgent->>Server: tools/call kb_query/v2
    Server-->>NewAgent: Result (new fields)

    Note over Server: Phase 2: Monitor v1 call volume,\nmigrate remaining agents

    Note over Server: Phase 3: Contract, remove v1\nonce volume reaches zero
    Server-->>Server: kb_query/v1 removed from registration
# Monitoring: log which version is actually being called, drives the
# decision of when it is safe to remove kb_query/v1 entirely.
import logging

@mcp.middleware()
async def log_tool_version(request, call_next):
    if request.tool_name.startswith("kb_query/"):
        version = request.tool_name.split("/")[-1]
        tool_version_metrics.increment(f"kb_query.{version}.calls")
    return await call_next(request)

Only remove kb_query/v1 once its call volume has been at or near zero for a defined period, or after a communicated sunset date, whichever governance process your organization uses.

The Gap Schema Versioning Does Not Close: Silent Behavior Changes

Everything above versions changes to a tool’s shape, its parameters and its return structure. It says nothing about changes to a tool’s behavior that leave the shape completely untouched: a ranking algorithm tweak that reorders results, a default filter that quietly becomes stricter, a downstream data source swap that returns subtly different values for the same query. These changes produce a zero-line diff in the schema, meaning the CI schema-diff gate built in Lesson 29 will not flag them at all, and yet they can meaningfully change what a caller receives for identical input.

# Schema-identical, behaviorally different: no schema diff will catch this
@mcp.tool(name="kb_query/v2")
async def kb_query_v2(input: KBQueryInputV2) -> list[dict]:
    """
    Same input/output schema as last week. But the ranking algorithm
    underneath was swapped from BM25 to a hybrid dense+sparse reranker,
    changing result order and occasionally which documents appear at all.
    """
    return await _kb_search_v2_hybrid(input.query, input.top_k, input.doc_type, input.min_relevance)

There is no purely mechanical fix for this, since a diff tool can only compare what is structurally represented, behavior lives in code the diff tool does not execute. Three practical mitigations, used together rather than any one alone: a changelog convention requiring any behavior-affecting change (even schema-identical ones) to be called out explicitly in the tool’s description or a linked changelog entry, so a human reviewer is prompted to consider whether it warrants a version bump anyway; a model-in-the-loop eval suite (Lesson 26) run against a fixed set of representative queries before and after the change, to catch meaningful output drift empirically rather than relying on someone remembering to flag it; and, for changes expected to be large enough to matter, simply choosing to version-bump anyway even though the schema alone did not require it, treating “meaningfully different results for the same input” as a breaking change in spirit even when it is not one in the JSON Schema sense.

The next lesson scales this versioning discipline across many servers with tool discovery, namespacing, and federation.

Knowledge Check

3 questions to test your understanding

1 A tool `kb_query` adds a new optional parameter `doc_type` with a default value, existing callers that omit it get identical behavior to before. Is this a breaking change?

2 A tool needs a new required parameter that changes its semantics meaningfully. What is the correct way to roll this out?

3 A team changes `kb_query`'s underlying ranking algorithm so that, for the exact same query and schema, results now come back in a meaningfully different order and sometimes include different documents entirely. No parameter or return field was added, removed, or retyped. Does semantic versioning by schema shape alone catch this change?

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan