Guide · Versioning
MCP server versioning
MCP server versioning has two distinct layers: the protocol version, which is negotiated in the initialize handshake and is controlled by the MCP specification, and the tool schema version, which is your responsibility and controls what tool inputs your server accepts and what it returns. Protocol version negotiation is automatic when you use the official SDK. Tool schema versioning requires discipline in how you evolve your tool definitions — a breaking change to a tool schema breaks every client that has cached the old schema from a prior tools/list call.
TL;DR
The MCP SDK handles protocol version negotiation automatically in the initialize handshake — you do not need to write negotiation code. For tool schemas: adding optional parameters, adding new tools, and improving descriptions are non-breaking. Removing tools, removing required parameters, and changing parameter types are breaking changes that require a migration window. Use the schema snapshot test in CI to catch accidental breaking changes before deploy. Set up AliveMCP to verify that your server's initialize response and tools/list remain stable after every deployment.
Protocol version negotiation
The MCP initialize handshake is where protocol version negotiation happens. The client sends the highest protocol version it supports; the server responds with the version it will use for the session:
// Client sends:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26", // client's highest supported version
"capabilities": { "tools": {} },
"clientInfo": { "name": "claude-desktop", "version": "1.0.0" }
}
}
// Server responds:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26", // version the server will use
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "my-mcp-server", "version": "1.2.0" }
}
}
The official MCP SDK handles this negotiation automatically — the SDK version you install supports the protocol versions defined at the time of release. When a new protocol spec version is published, the SDK is updated to support it. You do not need to write version negotiation code. However, you must be aware that upgrading the SDK may change the protocolVersion field in the initialize response — this can break clients that check for a specific protocol version string. Run your protocol compliance tests after every SDK upgrade to catch this.
The serverInfo.version field in the initialize response is your application version — change it with every deployment. This field appears in AliveMCP's probe history, giving you a deployment log from uptime monitoring: each time the version string changes in probe results, it marks a deployment. Use semantic versioning: 1.2.3 where patch bumps are non-breaking, minor bumps add new tools, and major bumps indicate breaking tool schema changes.
Breaking vs non-breaking tool schema changes
AI clients cache the tools/list response. A client that has cached your old tool schema will send tool calls using the old parameter names and types. Breaking changes break those calls:
| Change type | Breaking? | Why |
|---|---|---|
| Add a new tool | No | Clients that cached the old list simply do not know about the new tool |
| Add an optional parameter to an existing tool | No | Old clients omit the new param; the server uses the default |
| Improve a tool or parameter description | No | Description is informational; does not affect call structure |
Add a default to an existing required parameter | Potentially | Makes a previously required param optional — clients that relied on server-side validation of presence now see different behavior |
| Remove a tool | Yes | Clients calling the removed tool receive a -32601 method-not-found error |
| Rename a tool | Yes | Equivalent to remove + add; old clients call the old name |
| Remove a parameter | Yes | Clients passing the removed parameter may get a validation error |
| Change a parameter type (e.g. string → number) | Yes | Old clients pass the old type; server returns -32602 invalid params |
| Make an optional parameter required | Yes | Old clients that omit the param now get a -32602 error |
Schema snapshot test to catch breaking changes
The schema snapshot test catches breaking changes before they reach production. It stores a SHA-256 hash of the sorted tools/list response as a committed baseline. Any change to the tool schema fails the snapshot test — forcing an intentional review before the change deploys:
// test/schema-snapshot.test.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
const SNAPSHOT_PATH = 'test/schema-snapshot.json';
test('tools/list schema matches committed snapshot', async () => {
const client = new Client({ name: 'snapshot-test', version: '1.0.0' }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3001/mcp'));
await client.connect(transport);
const { tools } = await client.listTools();
const sorted = tools.sort((a, b) => a.name.localeCompare(b.name));
const schema = JSON.stringify(sorted, null, 2);
const hash = createHash('sha256').update(schema).digest('hex');
if (!existsSync(SNAPSHOT_PATH)) {
// First run — write baseline
writeFileSync(SNAPSHOT_PATH, JSON.stringify({ hash, tools: sorted }, null, 2));
console.log('Schema snapshot written. Commit this file.');
return;
}
const snapshot = JSON.parse(readFileSync(SNAPSHOT_PATH, 'utf-8'));
expect(hash).toBe(snapshot.hash);
// If this fails: review the schema change, update the snapshot, and document why.
});
await client.close();
When a tool schema change is intentional, update the snapshot file and commit it alongside the code change. The snapshot file in version control creates a permanent record of every tool schema change: when it happened, what changed, and who approved it. This is the structural defence against accidental breaking changes slipping through code review.
Multi-version servers for breaking changes
When you must make a breaking change, run both the old and new tool schema simultaneously during a migration window so existing clients have time to upgrade:
// Dual-version tool: keep the old name, add the new one, deprecate the old
server.tool(
'search', // OLD name — still works
'[DEPRECATED: use search_v2] Search docs by query',
{ query: z.string() }, // OLD schema — required string only
async (args) => {
// Forward to the new implementation with defaults
return searchImpl({ query: args.query, limit: 10, format: 'text' });
}
);
server.tool(
'search_v2', // NEW name — new schema
'Search docs by query with format and limit options',
{
query: z.string().min(1),
limit: z.number().int().min(1).max(50).default(10),
format: z.enum(['text', 'json', 'markdown']).default('text'),
},
async (args) => searchImpl(args)
);
Announce the deprecation in the tool description. After a migration window (typically 30-90 days for external clients, shorter for internal clients you control), remove the deprecated tool. The schema snapshot test will require a snapshot update when you add search_v2 and again when you remove search — both are intentional and documented changes.
Rolling deploys and versioning
During a rolling deploy, multiple instances of your server run simultaneously — some on the old version, some on the new. If a client session starts on the old version, subsequent requests in that session may route to a new-version instance. This creates a session-version mismatch problem.
The mitigation: use session affinity (sticky routing) during deploys. Most reverse proxies support session affinity via a cookie or the mcp-session-id header. Configure Caddy, nginx, or your load balancer to route all requests with the same mcp-session-id to the same instance for the duration of the session. AliveMCP's probe starts a new session on each probe cycle — if the probe consistently routes to a new-version instance before old-version instances are drained, the probe's tools/list hash will change at deploy time. This is expected behavior, not an incident. Monitor the probe's serverInfo.version field to confirm the deployment completed: when all instances return the new version, the rolling deploy is done.
Related questions
How should I communicate deprecations to clients that have cached my tool schema?
Update the tool description to include a deprecation notice (e.g., "[DEPRECATED: use search_v2]"). AI clients re-read the tool description at the start of each session — the deprecation notice appears in the next session after you deploy. For external clients you cannot contact directly, the tool description is the only communication channel available to you within the MCP protocol. If you have a developer relations channel (email newsletter, docs site, Discord), announce breaking changes there too.
Should I version my server's URL (e.g. /mcp/v2)?
Only if you need to run two completely different server implementations simultaneously for different clients. For most use cases, a single /mcp endpoint that supports both old and new tool schemas during the migration window is simpler. URL versioning forces clients to know which URL to use, which requires out-of-band communication. Schema versioning (keeping old and new tools simultaneously at the same URL) is self-describing — clients discover what's available from tools/list.
How do I detect schema drift in production?
AliveMCP monitors the tools/list response and can alert when the tool list changes between probe cycles. This catches schema drift caused by partial deploys, feature flags that affect tool registration, or configuration errors that silently remove tools. A production monitoring check on your tool schema is the safety net below your CI schema snapshot test — the snapshot test catches planned changes; the monitoring check catches unplanned ones. See schema drift in MCP tool definitions for common drift patterns.
What happens if a client sends an initialize request with an unsupported protocol version?
The official MCP SDK returns the server's protocol version in the initialize response regardless of what the client requested, because MCP version negotiation is not a strict rejection mechanism — the server reports the version it supports and the client decides whether to proceed. If the client requires a minimum version and the server's version is lower, the client should close the connection. The SDK does not expose a mechanism to reject clients based on their requested version — this is by design in the current protocol spec.
Further reading
- MCP server SDK — initialize handshake and serverInfo.version field
- MCP server testing — schema snapshot test for catching breaking changes in CI
- MCP server CI/CD — schema snapshot gate in the deploy pipeline
- MCP server deployment — rolling deploys and session affinity
- Schema drift in MCP tool definitions — production patterns and prevention
- MCP server health check — monitoring the initialize response across versions
- AliveMCP — uptime monitoring with tool schema change detection