Version your MCP server API using semantic versioning to evolve tools without breaking existing clients. Use the server's version field for compatibility signaling, implement capability negotiation during the MCP handshake, and follow deprecation patterns that give clients time to migrate. This tutorial covers adding, renaming, and removing tools safely across versions.
Versioning MCP Server APIs for Backward Compatibility
As your MCP server evolves, you will add new tools, change parameters, and eventually remove old tools. Without versioning, these changes break clients that depend on the old API. This tutorial shows how to apply semantic versioning to MCP servers, use the protocol's built-in capability negotiation, implement deprecation warnings, and run multiple tool versions side by side during migration periods.
Prerequisites
- A working MCP server with tools that need versioning
- Understanding of semantic versioning (major.minor.patch)
- Node.js 18+ and npm installed
- Basic familiarity with the MCP protocol handshake
Step-by-step guide
Define your versioning strategy with semantic versioning
Define your versioning strategy with semantic versioning
Apply semantic versioning to your MCP server: MAJOR version for breaking changes (removed tools, changed parameter types), MINOR for additions (new tools, optional parameters), PATCH for bug fixes. Set the version in McpServer configuration. Clients can read this version during connection and adjust their behavior. Document your versioning policy so users know what to expect across releases.
1// src/version.ts2export const SERVER_VERSION = "2.1.0";34// Version changelog:5// 2.1.0 - Added search_files tool, added optional 'recursive' param to list_files6// 2.0.0 - BREAKING: Renamed get_file to read_file, removed legacy_search tool7// 1.3.0 - Added write_file tool8// 1.2.0 - Added list_files tool9// 1.1.0 - Added get_stats tool10// 1.0.0 - Initial release with get_file tool1112// src/index.ts13import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";14import { SERVER_VERSION } from "./version.js";1516const server = new McpServer({17 name: "versioned-file-server",18 version: SERVER_VERSION, // Communicated to clients during handshake19});Expected result: Server version set and communicated to clients during the MCP handshake.
Implement capability negotiation for feature detection
Implement capability negotiation for feature detection
MCP clients and servers exchange capabilities during the initial handshake. Use custom capabilities to signal which tool groups or features are available. Clients that understand your capabilities can enable advanced features, while older clients gracefully fall back. This is more flexible than pure version number comparison because clients can check for specific features.
1// src/capabilities.ts2export const SERVER_CAPABILITIES = {3 tools: {4 supported: true,5 features: {6 fileOperations: true,7 searchOperations: true,8 writeOperations: true,9 metricsReporting: true,10 },11 },12 versioning: {13 currentVersion: "2.1.0",14 minimumClientVersion: "1.0.0",15 deprecatedTools: ["legacy_search"],16 removedInNext: ["get_file_v1"],17 },18};1920// Register a version info tool21server.tool(22 "get_server_info",23 "Get server version, capabilities, and deprecation notices",24 {},25 async () => {26 return {27 content: [{28 type: "text",29 text: JSON.stringify(SERVER_CAPABILITIES, null, 2),30 }],31 };32 }33);Expected result: Server capabilities include feature flags and version metadata that clients can query.
Deprecate tools with warnings before removal
Deprecate tools with warnings before removal
When a tool is being phased out, do not remove it immediately. Mark it as deprecated by adding a warning to its response and updating its description. Run the deprecated tool for at least one major version cycle so clients have time to migrate. Log deprecation usage to track how many clients still use the old tool.
1// src/deprecated-tool.ts2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { z } from "zod";4import { logger } from "./logger.js";56export function registerDeprecatedTool(7 server: McpServer,8 oldName: string,9 newName: string,10 description: string,11 schema: Record<string, z.ZodTypeAny>,12 handler: (params: any) => Promise<any>13) {14 server.tool(15 oldName,16 `[DEPRECATED - Use ${newName} instead] ${description}`,17 schema,18 async (params) => {19 logger.warn({ tool: oldName, replacement: newName }, "Deprecated tool called");2021 const result = await handler(params);2223 // Prepend deprecation warning to response24 const warning = `⚠ DEPRECATION WARNING: "${oldName}" will be removed in the next major version. Use "${newName}" instead.\n\n`;25 const content = result.content.map((c: any) => 26 c.type === "text" ? { ...c, text: warning + c.text } : c27 );2829 return { ...result, content };30 }31 );32}3334// Usage:35// registerDeprecatedTool(server, "get_file", "read_file", "Read file contents", 36// { path: z.string() }, readFileHandler);Expected result: Deprecated tools still work but return warnings and log usage for migration tracking.
Add new parameters safely with backward compatibility
Add new parameters safely with backward compatibility
When adding new parameters to existing tools, always make them optional with sensible defaults. This is a MINOR version change that does not break existing clients. Clients that do not send the new parameter get the default behavior. Document the new parameter in the tool description so AI clients know it is available.
1// Before (v2.0.0):2server.tool("list_files", "List files in a directory", {3 directory: z.string().default("."),4}, listFilesHandler);56// After (v2.1.0) — added optional 'recursive' parameter:7server.tool("list_files", "List files in a directory. Set recursive=true to include subdirectories.", {8 directory: z.string().default("."),9 recursive: z.boolean().default(false).describe("Include files from subdirectories"),10 fileTypes: z.array(z.string()).optional().describe("Filter by file extensions, e.g. ['.ts', '.js']"),11}, async ({ directory, recursive, fileTypes }) => {12 // Existing behavior preserved when recursive=false and fileTypes=undefined13 const fs = await import("fs/promises");14 const entries = await fs.readdir(directory, { withFileTypes: true, recursive });15 let files = entries.filter(e => e.isFile());16 if (fileTypes?.length) {17 files = files.filter(e => fileTypes.some(ext => e.name.endsWith(ext)));18 }19 return { content: [{ type: "text", text: JSON.stringify(files.map(f => f.name), null, 2) }] };20});Expected result: New optional parameters added without breaking clients that omit them.
Run versioned tools side by side during migration
Run versioned tools side by side during migration
For major breaking changes, run both old and new versions of a tool simultaneously. Name the old version with a _v1 suffix and register the new version under the original name. This gives clients a migration window where both versions are available. Remove the v1 tool in the next major release after confirming no clients use it.
1// src/versioned-tools.ts2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { z } from "zod";45export function registerVersionedTools(server: McpServer) {6 // Current version (v2) — new interface7 server.tool(8 "search_files",9 "Search files by content pattern. Returns matched lines with context.",10 {11 pattern: z.string().describe("Regex pattern to search for"),12 directory: z.string().default("."),13 contextLines: z.number().default(2).describe("Lines of context around matches"),14 },15 searchFilesV2Handler16 );1718 // Legacy version (v1) — deprecated, will be removed in v3.0.019 server.tool(20 "search_files_v1",21 "[DEPRECATED in v3.0.0 - Use search_files instead] Search files by name pattern.",22 {23 query: z.string().describe("File name pattern"),24 },25 async ({ query }) => {26 console.error(`[deprecation] search_files_v1 called — migrate to search_files`);27 // Delegate to v2 with compatible parameters28 return searchFilesV2Handler({ pattern: query, directory: ".", contextLines: 0 });29 }30 );31}Expected result: Both tool versions work simultaneously, with deprecated versions logging migration warnings.
Complete working example
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";2import { z } from "zod";34export const SERVER_VERSION = "2.1.0";56export function registerVersionedServer(server: McpServer) {7 // Version info tool8 server.tool("get_server_info", "Get server version and capabilities", {}, async () => ({9 content: [{ type: "text" as const, text: JSON.stringify({10 name: "versioned-server",11 version: SERVER_VERSION,12 deprecated: ["search_files_v1"],13 removedInNext: [],14 }, null, 2) }],15 }));1617 // Current tool (v2)18 server.tool("search_files", "Search files by content pattern with context lines", {19 pattern: z.string(),20 directory: z.string().default("."),21 contextLines: z.number().default(2),22 }, async ({ pattern, directory, contextLines }) => {23 return { content: [{ type: "text" as const, text: `Searching ${directory} for ${pattern} with ${contextLines} context lines` }] };24 });2526 // Deprecated v1 adapter27 server.tool("search_files_v1",28 "[DEPRECATED - Use search_files] Search by file name",29 { query: z.string() },30 async ({ query }) => {31 console.error(`[deprecation] search_files_v1 called`);32 return { content: [{ type: "text" as const,33 text: `WARNING: search_files_v1 is deprecated. Use search_files instead.\n\nResults for: ${query}` }] };34 }35 );36}3738// Deprecation wrapper factory39export function deprecated<T>(40 oldName: string,41 newName: string,42 handler: (params: T) => Promise<any>43): (params: T) => Promise<any> {44 return async (params: T) => {45 console.error(`[deprecated] ${oldName} -> use ${newName}`);46 const result = await handler(params);47 const warn = `[DEPRECATED: ${oldName} will be removed. Use ${newName}]\n`;48 return {49 ...result,50 content: result.content.map((c: any) =>51 c.type === "text" ? { ...c, text: warn + c.text } : c52 ),53 };54 };55}Common mistakes when versioning your MCP server API
Why it's a problem: Removing tools without a deprecation period, breaking all existing clients immediately
How to avoid: Deprecate tools for at least one major version cycle before removal. Mark them in the description and log usage.
Why it's a problem: Adding required parameters to existing tools, which is a breaking change
How to avoid: Always make new parameters optional with sensible defaults. Required parameter additions require a new major version.
Why it's a problem: Changing tool return formats without versioning, causing client-side parse errors
How to avoid: Document return format changes in the changelog and treat format changes as breaking changes requiring a major version bump.
Why it's a problem: Not tracking deprecated tool usage, making it impossible to know when it is safe to remove them
How to avoid: Log every call to deprecated tools and monitor the count. Remove only when usage drops to zero.
Best practices
- Follow semantic versioning: MAJOR for breaking changes, MINOR for additions, PATCH for fixes
- Set the version in McpServer config so clients see it during the handshake
- Deprecate tools for at least one major version cycle before removal
- Make all new parameters optional with defaults to avoid breaking changes
- Expose a get_server_info tool for programmatic version and capability discovery
- Log deprecated tool usage to track migration progress
- Run old and new tool versions side by side during migration periods
- Document changes in a changelog that clients can reference
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to version an MCP server API. I need semantic versioning, a get_server_info tool for capability discovery, deprecation wrappers that add warnings to old tools, and a pattern for running v1 and v2 of a tool side by side during migration.
Implement API versioning for my MCP server. Add SERVER_VERSION constant, a get_server_info tool, deprecated tool wrappers with logging, and backward-compatible parameter additions. Use TypeScript with Zod schemas.
Frequently asked questions
When should I bump the major version of my MCP server?
Bump the major version when you remove a tool, rename a tool, change a parameter from optional to required, or change the return format in a way that would break existing clients.
How long should I keep deprecated tools available?
At least one major version cycle. If you deprecate a tool in v2.x, keep it available through all v2.x releases and remove it in v3.0.0. Track usage to confirm no clients still depend on it.
Can MCP clients automatically detect breaking changes?
Clients can read the server version during the handshake and compare it to their expected version. The get_server_info tool provides more detailed capability information for programmatic compatibility checks.
Should I version individual tools or the entire server?
Version the entire server. Individual tool versioning (search_v1, search_v2) should be a temporary migration pattern, not a permanent architecture. Keep the server version as the single source of truth.
Can RapidDev help plan MCP server versioning strategy?
Yes. RapidDev helps teams design versioning strategies that balance backward compatibility with the ability to evolve. They can audit existing tool APIs and recommend a migration plan.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation