The MCP specification evolves. New capabilities are added; some older mechanisms are deprecated; breaking changes occasionally ship. Building MCP servers that handle protocol version negotiation correctly means your clients and servers can interoperate across version boundaries without hard dependencies on a single spec revision. This lesson covers how MCP versioning works, how to negotiate capabilities with older clients, how to write migration guides when your own server schema changes, and the stability guarantees you can rely on from Anthropic.

How MCP Protocol Versioning Works
MCP uses date-stamped version strings like 2024-11-05 or 2025-03-26. During initialization, the client sends the version it wants, and the server responds with the version it will use (typically the same or an older compatible one).
// Initialization exchange (JSON-RPC)
// Client sends:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": { "name": "my-client", "version": "2.0.0" },
"capabilities": { "sampling": {}, "elicitation": {} }
}
}
// Server responds with the version it accepts:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"serverInfo": { "name": "my-server", "version": "1.5.0" },
"capabilities": { "tools": {}, "resources": {}, "prompts": {} }
}
}
// The @modelcontextprotocol/sdk handles version negotiation automatically
// You do not need to implement it manually
// To check the negotiated version in your server:
server.server.oninitialized = () => {
const version = server.server.negotiatedProtocolVersion;
console.log(`MCP session initialized with protocol version: ${version}`);
};
Feature Detection (Capability Negotiation)
// Check if the connected client supports a specific capability
// before using it in your server code
server.server.oninitialized = () => {
const clientCaps = server.server.getClientCapabilities();
const supportsElicitation = !!clientCaps?.elicitation;
const supportsSampling = !!clientCaps?.sampling;
const supportsRoots = !!clientCaps?.roots;
console.log(`Client capabilities: elicitation=${supportsElicitation} sampling=${supportsSampling} roots=${supportsRoots}`);
if (!supportsElicitation) {
// Fall back to returning instructions in tool result instead of interactive elicitation
console.warn('Client does not support elicitation - using text fallback');
}
};

Migrating Your Tool Schema
When you change a tool’s input schema, existing clients that have cached the old schema will break. Follow a compatibility-first migration process:
// Backwards-compatible schema evolution: add optional fields, never remove required ones
// Version 1 schema (existing clients use this)
// search_products: { query: z.string(), limit: z.number().optional().default(10) }
// Version 2: add optional 'category' filter without breaking v1 clients
server.tool('search_products', {
query: z.string(),
limit: z.number().optional().default(10),
category: z.string().optional(), // New optional field - backwards compatible
// NEVER remove or rename 'query' or 'limit' - that breaks v1 clients
// NEVER make an optional field required - that also breaks v1 clients
}, handler);
// Breaking change strategy: add a versioned tool name during transition
// Phase 1: add new tool alongside old one
server.tool('search_products_v2', {
query: z.string(),
limit: z.number().optional().default(10),
filters: z.object({ // New required field - would break v1 if added to original
category: z.string().optional(),
priceMax: z.number().optional(),
inStock: z.boolean().optional().default(true),
}),
}, handler);
// Phase 2: deprecate old tool via description
// server.tool('search_products', ...
// description: 'DEPRECATED: use search_products_v2 instead'
// Phase 3 (after client migration window): remove old tool
Version Compatibility Matrix
| MCP Spec Version | SDK Version | Key Features Added |
|---|---|---|
| 2024-11-05 | 0.x | Initial release: tools, resources, prompts, sampling |
| 2025-03-26 | 1.x | Elicitation, streamable HTTP transport, tasks API |
Stability Guarantees
- JSON-RPC 2.0 wire format: Stable. Will not change between spec versions.
- Core methods (initialize, tools/call, resources/read, prompts/get): Stable across all versions.
- New capabilities: Always added as optional, never required for a functional server.
- Deprecated features: Maintained for at least 2 spec revisions before removal.
- SDK APIs: The TypeScript/JavaScript SDK minor versions maintain backwards compatibility; only major versions may include breaking changes.
What to Build Next
- Add a
server://versionresource to your MCP server that returns the current protocol version, SDK version, and your tool schema versions. Update it on every release. - Review your most-used tools for any fields that are currently optional but should be made required. Use the v2 naming strategy to transition safely.
nJoy 😉
