# How to version your MCP server API

- Tool: MCP
- Difficulty: Advanced
- Time required: 20-30 min
- Compatibility: MCP TypeScript SDK v1.x, Node.js 18+
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
// src/version.ts
export const SERVER_VERSION = "2.1.0";

// Version changelog:
// 2.1.0 - Added search_files tool, added optional 'recursive' param to list_files
// 2.0.0 - BREAKING: Renamed get_file to read_file, removed legacy_search tool
// 1.3.0 - Added write_file tool
// 1.2.0 - Added list_files tool
// 1.1.0 - Added get_stats tool
// 1.0.0 - Initial release with get_file tool

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SERVER_VERSION } from "./version.js";

const server = new McpServer({
  name: "versioned-file-server",
  version: SERVER_VERSION,  // Communicated to clients during handshake
});
```

**Expected result:** Server version set and communicated to clients during the MCP handshake.

### 2. 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.

```
// src/capabilities.ts
export const SERVER_CAPABILITIES = {
  tools: {
    supported: true,
    features: {
      fileOperations: true,
      searchOperations: true,
      writeOperations: true,
      metricsReporting: true,
    },
  },
  versioning: {
    currentVersion: "2.1.0",
    minimumClientVersion: "1.0.0",
    deprecatedTools: ["legacy_search"],
    removedInNext: ["get_file_v1"],
  },
};

// Register a version info tool
server.tool(
  "get_server_info",
  "Get server version, capabilities, and deprecation notices",
  {},
  async () => {
    return {
      content: [{
        type: "text",
        text: JSON.stringify(SERVER_CAPABILITIES, null, 2),
      }],
    };
  }
);
```

**Expected result:** Server capabilities include feature flags and version metadata that clients can query.

### 3. 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.

```
// src/deprecated-tool.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { logger } from "./logger.js";

export function registerDeprecatedTool(
  server: McpServer,
  oldName: string,
  newName: string,
  description: string,
  schema: Record<string, z.ZodTypeAny>,
  handler: (params: any) => Promise<any>
) {
  server.tool(
    oldName,
    `[DEPRECATED - Use ${newName} instead] ${description}`,
    schema,
    async (params) => {
      logger.warn({ tool: oldName, replacement: newName }, "Deprecated tool called");

      const result = await handler(params);

      // Prepend deprecation warning to response
      const warning = `⚠ DEPRECATION WARNING: "${oldName}" will be removed in the next major version. Use "${newName}" instead.\n\n`;
      const content = result.content.map((c: any) => 
        c.type === "text" ? { ...c, text: warning + c.text } : c
      );

      return { ...result, content };
    }
  );
}

// Usage:
// registerDeprecatedTool(server, "get_file", "read_file", "Read file contents", 
//   { path: z.string() }, readFileHandler);
```

**Expected result:** Deprecated tools still work but return warnings and log usage for migration tracking.

### 4. 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.

```
// Before (v2.0.0):
server.tool("list_files", "List files in a directory", {
  directory: z.string().default("."),
}, listFilesHandler);

// After (v2.1.0) — added optional 'recursive' parameter:
server.tool("list_files", "List files in a directory. Set recursive=true to include subdirectories.", {
  directory: z.string().default("."),
  recursive: z.boolean().default(false).describe("Include files from subdirectories"),
  fileTypes: z.array(z.string()).optional().describe("Filter by file extensions, e.g. ['.ts', '.js']"),
}, async ({ directory, recursive, fileTypes }) => {
  // Existing behavior preserved when recursive=false and fileTypes=undefined
  const fs = await import("fs/promises");
  const entries = await fs.readdir(directory, { withFileTypes: true, recursive });
  let files = entries.filter(e => e.isFile());
  if (fileTypes?.length) {
    files = files.filter(e => fileTypes.some(ext => e.name.endsWith(ext)));
  }
  return { content: [{ type: "text", text: JSON.stringify(files.map(f => f.name), null, 2) }] };
});
```

**Expected result:** New optional parameters added without breaking clients that omit them.

### 5. 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.

```
// src/versioned-tools.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerVersionedTools(server: McpServer) {
  // Current version (v2) — new interface
  server.tool(
    "search_files",
    "Search files by content pattern. Returns matched lines with context.",
    {
      pattern: z.string().describe("Regex pattern to search for"),
      directory: z.string().default("."),
      contextLines: z.number().default(2).describe("Lines of context around matches"),
    },
    searchFilesV2Handler
  );

  // Legacy version (v1) — deprecated, will be removed in v3.0.0
  server.tool(
    "search_files_v1",
    "[DEPRECATED in v3.0.0 - Use search_files instead] Search files by name pattern.",
    {
      query: z.string().describe("File name pattern"),
    },
    async ({ query }) => {
      console.error(`[deprecation] search_files_v1 called — migrate to search_files`);
      // Delegate to v2 with compatible parameters
      return searchFilesV2Handler({ pattern: query, directory: ".", contextLines: 0 });
    }
  );
}
```

**Expected result:** Both tool versions work simultaneously, with deprecated versions logging migration warnings.

## Complete code example

File: `src/versioning.ts`

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export const SERVER_VERSION = "2.1.0";

export function registerVersionedServer(server: McpServer) {
  // Version info tool
  server.tool("get_server_info", "Get server version and capabilities", {}, async () => ({
    content: [{ type: "text" as const, text: JSON.stringify({
      name: "versioned-server",
      version: SERVER_VERSION,
      deprecated: ["search_files_v1"],
      removedInNext: [],
    }, null, 2) }],
  }));

  // Current tool (v2)
  server.tool("search_files", "Search files by content pattern with context lines", {
    pattern: z.string(),
    directory: z.string().default("."),
    contextLines: z.number().default(2),
  }, async ({ pattern, directory, contextLines }) => {
    return { content: [{ type: "text" as const, text: `Searching ${directory} for ${pattern} with ${contextLines} context lines` }] };
  });

  // Deprecated v1 adapter
  server.tool("search_files_v1",
    "[DEPRECATED - Use search_files] Search by file name",
    { query: z.string() },
    async ({ query }) => {
      console.error(`[deprecation] search_files_v1 called`);
      return { content: [{ type: "text" as const,
        text: `WARNING: search_files_v1 is deprecated. Use search_files instead.\n\nResults for: ${query}` }] };
    }
  );
}

// Deprecation wrapper factory
export function deprecated<T>(
  oldName: string,
  newName: string,
  handler: (params: T) => Promise<any>
): (params: T) => Promise<any> {
  return async (params: T) => {
    console.error(`[deprecated] ${oldName} -> use ${newName}`);
    const result = await handler(params);
    const warn = `[DEPRECATED: ${oldName} will be removed. Use ${newName}]\n`;
    return {
      ...result,
      content: result.content.map((c: any) =>
        c.type === "text" ? { ...c, text: warn + c.text } : c
      ),
    };
  };
}
```

## Common mistakes

- **Removing tools without a deprecation period, breaking all existing clients immediately** — undefined Fix: Deprecate tools for at least one major version cycle before removal. Mark them in the description and log usage.
- **Adding required parameters to existing tools, which is a breaking change** — undefined Fix: Always make new parameters optional with sensible defaults. Required parameter additions require a new major version.
- **Changing tool return formats without versioning, causing client-side parse errors** — undefined Fix: Document return format changes in the changelog and treat format changes as breaking changes requiring a major version bump.
- **Not tracking deprecated tool usage, making it impossible to know when it is safe to remove them** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-version-mcp-server-api
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-version-mcp-server-api
