MCP Server Versioning: Semantic Versioning & Backward Compatibility
When your ChatGPT app serves thousands of users across different platforms and devices, a breaking change to your MCP server can cascade into widespread disruption. One mishandled API update can leave users with broken widgets, failed authentication flows, and lost trust. MCP server versioning is the disciplined practice that prevents these disasters through semantic versioning, backward compatibility guarantees, and graceful deprecation strategies.
This guide provides production-ready versioning patterns for MCP servers powering ChatGPT applications. You'll implement semantic versioning automation, backward compatibility layers, API deprecation workflows, migration path generators, contract testing frameworks, and intelligent version negotiation—ensuring your MCP server evolves without breaking existing integrations.
Why MCP Server Versioning Prevents Breaking Changes
ChatGPT apps operate in a distributed, asynchronous environment where clients (ChatGPT users, third-party integrations, mobile apps) may be running different versions of your app simultaneously. Unlike traditional web applications where you control the deployment of both client and server, MCP servers must support multiple client versions concurrently.
The versioning challenge compounds with scale:
- Gradual rollouts: New ChatGPT app versions may take weeks to reach all users due to App Store review cycles and user update delays
- Third-party integrations: External developers building on your MCP server API expect stability and advance notice for breaking changes
- Cross-platform complexity: iOS, Android, web, and ChatGPT desktop clients may support different API versions
- Zero-downtime requirements: Enterprise customers demand seamless updates without service interruptions
Semantic versioning solves these challenges by establishing a contract between your MCP server and its clients. Version numbers communicate the nature of changes (breaking vs. non-breaking), deprecation warnings provide migration time, and backward compatibility layers ensure existing clients continue working during transitions.
Semantic Versioning for MCP Servers
Semantic versioning (SemVer) uses a three-part version number (MAJOR.MINOR.PATCH) where each component signals the type of changes:
- MAJOR: Incompatible API changes that break existing clients (e.g., removing endpoints, changing required parameters)
- MINOR: Backward-compatible functionality additions (e.g., new optional parameters, new endpoints)
- PATCH: Backward-compatible bug fixes (e.g., correcting response data, fixing edge cases)
Pre-release versions (1.0.0-alpha.1, 2.0.0-rc.3) allow testing breaking changes before official release.
Production-Ready Version Manager
This TypeScript version manager automates semantic versioning for MCP servers, enforcing SemVer rules and tracking version history:
// src/versioning/VersionManager.ts
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
/**
* Semantic version components
*/
export interface SemanticVersion {
major: number;
minor: number;
patch: number;
prerelease?: string;
metadata?: string;
}
/**
* Version change type (determines which component to increment)
*/
export enum ChangeType {
MAJOR = 'major',
MINOR = 'minor',
PATCH = 'patch',
}
/**
* Version history entry for audit trail
*/
export interface VersionHistoryEntry {
version: string;
changeType: ChangeType;
timestamp: string;
description: string;
breakingChanges?: string[];
deprecated?: string[];
}
/**
* Manages semantic versioning for MCP server
* Automates version bumps, validates SemVer rules, tracks history
*/
export class VersionManager {
private versionFilePath: string;
private historyFilePath: string;
private currentVersion: SemanticVersion;
private history: VersionHistoryEntry[];
constructor(baseDir: string = process.cwd()) {
this.versionFilePath = join(baseDir, 'version.json');
this.historyFilePath = join(baseDir, 'version-history.json');
this.loadVersion();
this.loadHistory();
}
/**
* Load current version from version.json
*/
private loadVersion(): void {
try {
const data = JSON.parse(readFileSync(this.versionFilePath, 'utf-8'));
this.currentVersion = data.version;
} catch {
// Initialize with 1.0.0 if version.json doesn't exist
this.currentVersion = { major: 1, minor: 0, patch: 0 };
this.saveVersion();
}
}
/**
* Load version history
*/
private loadHistory(): void {
try {
this.history = JSON.parse(readFileSync(this.historyFilePath, 'utf-8'));
} catch {
this.history = [];
}
}
/**
* Save current version to version.json
*/
private saveVersion(): void {
writeFileSync(
this.versionFilePath,
JSON.stringify({ version: this.currentVersion, updated: new Date().toISOString() }, null, 2)
);
}
/**
* Save version history
*/
private saveHistory(): void {
writeFileSync(this.historyFilePath, JSON.stringify(this.history, null, 2));
}
/**
* Parse semantic version string into components
*/
public static parse(versionString: string): SemanticVersion {
const regex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$/;
const match = versionString.match(regex);
if (!match) {
throw new Error(`Invalid semantic version: ${versionString}`);
}
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
prerelease: match[4],
metadata: match[5],
};
}
/**
* Format semantic version as string
*/
public static format(version: SemanticVersion): string {
let versionString = `${version.major}.${version.minor}.${version.patch}`;
if (version.prerelease) versionString += `-${version.prerelease}`;
if (version.metadata) versionString += `+${version.metadata}`;
return versionString;
}
/**
* Bump version based on change type
*/
public bump(
changeType: ChangeType,
description: string,
breakingChanges?: string[],
deprecated?: string[]
): string {
const newVersion = { ...this.currentVersion };
switch (changeType) {
case ChangeType.MAJOR:
newVersion.major += 1;
newVersion.minor = 0;
newVersion.patch = 0;
break;
case ChangeType.MINOR:
newVersion.minor += 1;
newVersion.patch = 0;
break;
case ChangeType.PATCH:
newVersion.patch += 1;
break;
}
// Clear prerelease on official release
delete newVersion.prerelease;
delete newVersion.metadata;
this.currentVersion = newVersion;
this.saveVersion();
// Record in history
this.history.push({
version: VersionManager.format(newVersion),
changeType,
timestamp: new Date().toISOString(),
description,
breakingChanges,
deprecated,
});
this.saveHistory();
return VersionManager.format(newVersion);
}
/**
* Create prerelease version (alpha, beta, rc)
*/
public prerelease(identifier: string, description: string): string {
const newVersion = { ...this.currentVersion };
newVersion.prerelease = identifier;
this.currentVersion = newVersion;
this.saveVersion();
this.history.push({
version: VersionManager.format(newVersion),
changeType: ChangeType.MAJOR, // Prereleases often signal major changes
timestamp: new Date().toISOString(),
description: `Prerelease: ${description}`,
});
this.saveHistory();
return VersionManager.format(newVersion);
}
/**
* Get current version as string
*/
public getCurrentVersion(): string {
return VersionManager.format(this.currentVersion);
}
/**
* Get version history
*/
public getHistory(): VersionHistoryEntry[] {
return this.history;
}
/**
* Check if version A is newer than version B
*/
public static isNewer(versionA: string, versionB: string): boolean {
const a = this.parse(versionA);
const b = this.parse(versionB);
if (a.major !== b.major) return a.major > b.major;
if (a.minor !== b.minor) return a.minor > b.minor;
return a.patch > b.patch;
}
}
Usage example:
import { VersionManager, ChangeType } from './versioning/VersionManager';
const versionManager = new VersionManager();
// Patch release (bug fix)
versionManager.bump(ChangeType.PATCH, 'Fixed authentication token expiration edge case');
// Version: 1.0.1
// Minor release (new feature)
versionManager.bump(ChangeType.MINOR, 'Added support for file upload in widget state');
// Version: 1.1.0
// Major release (breaking change)
versionManager.bump(
ChangeType.MAJOR,
'Migrated authentication to OAuth 2.1 with PKCE',
['Removed legacy API key authentication', 'Changed /auth/login endpoint signature'],
['POST /auth/legacy-login (use POST /auth/oauth2 instead)']
);
// Version: 2.0.0
// Prerelease for testing
versionManager.prerelease('rc.1', 'Release candidate for v3.0.0 OAuth refactor');
// Version: 2.0.0-rc.1
This version manager provides:
- Automated SemVer enforcement: Prevents invalid version bumps
- Audit trail: Tracks all version changes with timestamps and descriptions
- Prerelease support: Test breaking changes before official release
- Breaking change documentation: Records deprecated APIs for migration planning
Backward Compatibility Strategies
Backward compatibility ensures existing clients continue working when your MCP server introduces new features or deprecates old endpoints. The key principle: additive changes only (new fields, new endpoints) until you're ready for a major version bump.
Backward Compatibility Layer Implementation
This TypeScript compatibility layer wraps your MCP server handlers to transform requests/responses between API versions:
// src/versioning/BackwardCompatibilityLayer.ts
import { Request, Response, NextFunction } from 'express';
/**
* API version transformation rule
*/
export interface TransformationRule {
fromVersion: string;
toVersion: string;
transformRequest?: (req: any) => any;
transformResponse?: (res: any) => any;
}
/**
* Deprecation warning configuration
*/
export interface DeprecationWarning {
endpoint: string;
version: string;
sunset: string; // ISO 8601 date
replacement?: string;
message: string;
}
/**
* Backward compatibility layer for MCP server
* Transforms requests/responses between API versions
*/
export class BackwardCompatibilityLayer {
private transformations: Map<string, TransformationRule[]> = new Map();
private deprecations: Map<string, DeprecationWarning> = new Map();
/**
* Register transformation rule between versions
*/
public registerTransformation(rule: TransformationRule): void {
const key = `${rule.fromVersion}->${rule.toVersion}`;
const existing = this.transformations.get(key) || [];
existing.push(rule);
this.transformations.set(key, existing);
}
/**
* Register deprecation warning for endpoint
*/
public registerDeprecation(deprecation: DeprecationWarning): void {
this.deprecations.set(deprecation.endpoint, deprecation);
}
/**
* Express middleware: Transform request from client version to server version
*/
public middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const clientVersion = this.extractClientVersion(req);
const serverVersion = process.env.MCP_SERVER_VERSION || '1.0.0';
// Check for deprecation warning
this.checkDeprecation(req, res);
// Transform request if client version differs from server version
if (clientVersion !== serverVersion) {
const transformedReq = this.transformRequest(req, clientVersion, serverVersion);
Object.assign(req, transformedReq);
}
// Wrap res.json to transform responses
const originalJson = res.json.bind(res);
res.json = (data: any) => {
const transformedData = this.transformResponse(data, serverVersion, clientVersion);
return originalJson(transformedData);
};
next();
};
}
/**
* Extract client version from request headers
*/
private extractClientVersion(req: Request): string {
// Check X-API-Version header
const headerVersion = req.headers['x-api-version'] as string;
if (headerVersion) return headerVersion;
// Check query parameter (fallback)
const queryVersion = req.query.api_version as string;
if (queryVersion) return queryVersion;
// Default to v1.0.0 for legacy clients
return '1.0.0';
}
/**
* Check if endpoint is deprecated and add warning headers
*/
private checkDeprecation(req: Request, res: Response): void {
const deprecation = this.deprecations.get(req.path);
if (!deprecation) return;
// Add deprecation warning headers
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', deprecation.sunset);
if (deprecation.replacement) {
res.setHeader('Link', `<${deprecation.replacement}>; rel="alternate"`);
}
res.setHeader('Warning', `299 - "${deprecation.message}"`);
}
/**
* Transform request from client version to server version
*/
private transformRequest(req: Request, fromVersion: string, toVersion: string): any {
const key = `${fromVersion}->${toVersion}`;
const rules = this.transformations.get(key);
if (!rules) return req;
let transformedReq = { ...req };
for (const rule of rules) {
if (rule.transformRequest) {
transformedReq = rule.transformRequest(transformedReq);
}
}
return transformedReq;
}
/**
* Transform response from server version to client version
*/
private transformResponse(data: any, fromVersion: string, toVersion: string): any {
const key = `${fromVersion}->${toVersion}`;
const rules = this.transformations.get(key);
if (!rules) return data;
let transformedData = { ...data };
for (const rule of rules) {
if (rule.transformResponse) {
transformedData = rule.transformResponse(transformedData);
}
}
return transformedData;
}
}
Usage example:
import express from 'express';
import { BackwardCompatibilityLayer } from './versioning/BackwardCompatibilityLayer';
const app = express();
const compatLayer = new BackwardCompatibilityLayer();
// Example: v1.0.0 -> v2.0.0 transformation (renamed field)
compatLayer.registerTransformation({
fromVersion: '1.0.0',
toVersion: '2.0.0',
transformRequest: (req) => {
// v1 used "user_id", v2 uses "userId"
if (req.body.user_id) {
req.body.userId = req.body.user_id;
delete req.body.user_id;
}
return req;
},
transformResponse: (res) => {
// Transform back to v1 format for legacy clients
if (res.userId) {
res.user_id = res.userId;
delete res.userId;
}
return res;
},
});
// Register deprecation warning
compatLayer.registerDeprecation({
endpoint: '/api/v1/auth/legacy-login',
version: '2.0.0',
sunset: '2026-06-01T00:00:00Z',
replacement: '/api/v2/auth/oauth2',
message: 'This endpoint will be removed on June 1, 2026. Migrate to OAuth 2.1.',
});
// Apply compatibility middleware globally
app.use(compatLayer.middleware());
// Your MCP server routes
app.post('/api/v2/auth/oauth2', (req, res) => {
// Handle OAuth 2.1 authentication (uses "userId" field)
const { userId } = req.body;
res.json({ success: true, userId });
});
This compatibility layer provides:
- Automatic request/response transformation: Clients on v1 can call v2 endpoints seamlessly
- Deprecation warnings: HTTP headers notify clients of upcoming breaking changes
- Sunset headers: RFC 8594 compliance for deprecation communication
- Version negotiation: Extracts client version from headers or query params
API Deprecation & Migration Paths
Deprecation is the process of phasing out old API features while giving clients sufficient time to migrate. A well-executed deprecation prevents breaking changes by:
- Announcing deprecation with advance notice (6-12 months for enterprise APIs)
- Maintaining parallel versions during the transition period
- Providing migration guides with code examples
- Monitoring usage of deprecated endpoints to ensure clients have migrated
- Sunsetting gracefully with final removal after sunset date
Deprecation Middleware Implementation
This TypeScript deprecation middleware tracks usage of deprecated endpoints and enforces sunset policies:
// src/versioning/DeprecationMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { EventEmitter } from 'events';
/**
* Deprecation policy configuration
*/
export interface DeprecationPolicy {
endpoint: string;
deprecatedInVersion: string;
sunsetDate: string; // ISO 8601
replacement?: string;
migrationGuide?: string;
warningOnly: boolean; // If false, returns 410 Gone after sunset
}
/**
* Deprecation usage analytics
*/
export interface DeprecationUsage {
endpoint: string;
clientVersion: string;
timestamp: string;
userAgent: string;
ip: string;
}
/**
* Deprecation middleware for MCP server
* Enforces sunset policies and tracks deprecated endpoint usage
*/
export class DeprecationMiddleware extends EventEmitter {
private policies: Map<string, DeprecationPolicy> = new Map();
private usageLog: DeprecationUsage[] = [];
/**
* Register deprecation policy for endpoint
*/
public register(policy: DeprecationPolicy): void {
this.policies.set(policy.endpoint, policy);
}
/**
* Express middleware factory
*/
public middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const policy = this.policies.get(req.path);
if (!policy) return next();
// Log usage for analytics
this.logUsage(req, policy);
// Check if sunset date has passed
const now = new Date();
const sunset = new Date(policy.sunsetDate);
if (now > sunset && !policy.warningOnly) {
// Endpoint is sunset - return 410 Gone
return res.status(410).json({
error: 'Gone',
message: `This endpoint was deprecated and removed on ${policy.sunsetDate}.`,
replacement: policy.replacement,
migrationGuide: policy.migrationGuide,
});
}
// Add deprecation headers
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', policy.sunsetDate);
if (policy.replacement) {
res.setHeader('Link', `<${policy.replacement}>; rel="alternate"`);
}
const daysUntilSunset = Math.ceil((sunset.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const warningMessage = policy.migrationGuide
? `Deprecated. ${daysUntilSunset} days until removal. See ${policy.migrationGuide}`
: `Deprecated. ${daysUntilSunset} days until removal. Use ${policy.replacement} instead.`;
res.setHeader('Warning', `299 - "${warningMessage}"`);
// Emit event for monitoring
this.emit('deprecation-used', {
endpoint: req.path,
clientVersion: req.headers['x-api-version'] || 'unknown',
daysUntilSunset,
});
next();
};
}
/**
* Log deprecated endpoint usage
*/
private logUsage(req: Request, policy: DeprecationPolicy): void {
this.usageLog.push({
endpoint: req.path,
clientVersion: (req.headers['x-api-version'] as string) || 'unknown',
timestamp: new Date().toISOString(),
userAgent: req.headers['user-agent'] || 'unknown',
ip: req.ip || 'unknown',
});
// Limit log size (keep last 10,000 entries)
if (this.usageLog.length > 10000) {
this.usageLog.shift();
}
}
/**
* Get usage analytics for deprecated endpoints
*/
public getUsageAnalytics(): Record<string, { count: number; lastUsed: string }> {
const analytics: Record<string, { count: number; lastUsed: string }> = {};
for (const usage of this.usageLog) {
if (!analytics[usage.endpoint]) {
analytics[usage.endpoint] = { count: 0, lastUsed: usage.timestamp };
}
analytics[usage.endpoint].count += 1;
analytics[usage.endpoint].lastUsed = usage.timestamp; // Update to latest
}
return analytics;
}
}
Usage example:
import express from 'express';
import { DeprecationMiddleware } from './versioning/DeprecationMiddleware';
const app = express();
const deprecation = new DeprecationMiddleware();
// Register deprecation policies
deprecation.register({
endpoint: '/api/v1/auth/api-key',
deprecatedInVersion: '2.0.0',
sunsetDate: '2026-06-01T00:00:00Z',
replacement: '/api/v2/auth/oauth2',
migrationGuide: 'https://docs.makeaihq.com/migration/oauth2',
warningOnly: false, // Hard cutoff after sunset
});
deprecation.register({
endpoint: '/api/v1/apps/list',
deprecatedInVersion: '2.1.0',
sunsetDate: '2026-09-01T00:00:00Z',
replacement: '/api/v2/apps',
warningOnly: true, // Keep working after sunset (warning only)
});
// Monitor deprecated endpoint usage
deprecation.on('deprecation-used', ({ endpoint, clientVersion, daysUntilSunset }) => {
console.warn(`[DEPRECATION] ${endpoint} used by client v${clientVersion} (${daysUntilSunset} days until removal)`);
});
// Apply deprecation middleware globally
app.use(deprecation.middleware());
// Analytics endpoint for monitoring migration progress
app.get('/admin/deprecation-analytics', (req, res) => {
const analytics = deprecation.getUsageAnalytics();
res.json(analytics);
});
This deprecation middleware provides:
- Sunset enforcement: Automatically returns 410 Gone after sunset date
- Usage tracking: Monitor which clients are still using deprecated endpoints
- Migration analytics: Identify slow adopters who need migration assistance
- RFC 8594 compliance: Proper HTTP headers for deprecation communication
Contract Testing for API Stability
Contract testing validates that your MCP server API matches the contract expected by clients. Unlike traditional integration tests (which test internal implementation), contract tests verify the external interface remains stable across versions.
Contract Validator Implementation
This TypeScript contract validator uses JSON Schema to enforce API contracts:
// src/versioning/ContractValidator.ts
import Ajv, { Schema, ValidateFunction } from 'ajv';
import addFormats from 'ajv-formats';
/**
* API contract definition (request/response schemas)
*/
export interface ApiContract {
version: string;
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
requestSchema?: Schema;
responseSchema: Schema;
}
/**
* Validation result
*/
export interface ValidationResult {
valid: boolean;
errors?: string[];
}
/**
* Contract validator for MCP server API
* Uses JSON Schema to validate requests/responses
*/
export class ContractValidator {
private ajv: Ajv;
private contracts: Map<string, ApiContract> = new Map();
private validators: Map<string, { request?: ValidateFunction; response: ValidateFunction }> = new Map();
constructor() {
this.ajv = new Ajv({ allErrors: true });
addFormats(this.ajv); // Add format validators (email, uri, etc.)
}
/**
* Register API contract
*/
public registerContract(contract: ApiContract): void {
const key = this.getContractKey(contract.endpoint, contract.method, contract.version);
this.contracts.set(key, contract);
// Compile validators
const requestValidator = contract.requestSchema ? this.ajv.compile(contract.requestSchema) : undefined;
const responseValidator = this.ajv.compile(contract.responseSchema);
this.validators.set(key, {
request: requestValidator,
response: responseValidator,
});
}
/**
* Validate request against contract
*/
public validateRequest(endpoint: string, method: string, version: string, data: any): ValidationResult {
const key = this.getContractKey(endpoint, method, version);
const validator = this.validators.get(key);
if (!validator || !validator.request) {
return { valid: true }; // No request schema defined
}
const valid = validator.request(data);
if (valid) {
return { valid: true };
}
return {
valid: false,
errors: validator.request.errors?.map((err) => `${err.instancePath} ${err.message}`) || [],
};
}
/**
* Validate response against contract
*/
public validateResponse(endpoint: string, method: string, version: string, data: any): ValidationResult {
const key = this.getContractKey(endpoint, method, version);
const validator = this.validators.get(key);
if (!validator) {
throw new Error(`No contract registered for ${method} ${endpoint} (v${version})`);
}
const valid = validator.response(data);
if (valid) {
return { valid: true };
}
return {
valid: false,
errors: validator.response.errors?.map((err) => `${err.instancePath} ${err.message}`) || [],
};
}
/**
* Generate contract key
*/
private getContractKey(endpoint: string, method: string, version: string): string {
return `${version}:${method}:${endpoint}`;
}
}
Usage example:
import { ContractValidator } from './versioning/ContractValidator';
const validator = new ContractValidator();
// Register contract for POST /api/v2/apps/create
validator.registerContract({
version: '2.0.0',
endpoint: '/api/v2/apps/create',
method: 'POST',
requestSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
description: { type: 'string', maxLength: 500 },
userId: { type: 'string', format: 'uuid' },
},
required: ['name', 'userId'],
additionalProperties: false,
},
responseSchema: {
type: 'object',
properties: {
success: { type: 'boolean' },
appId: { type: 'string', format: 'uuid' },
createdAt: { type: 'string', format: 'date-time' },
},
required: ['success', 'appId', 'createdAt'],
additionalProperties: false,
},
});
// Validate request
const requestResult = validator.validateRequest(
'/api/v2/apps/create',
'POST',
'2.0.0',
{ name: 'My ChatGPT App', userId: 'invalid-uuid' } // Invalid UUID
);
console.log(requestResult);
// { valid: false, errors: ['/userId must match format "uuid"'] }
// Validate response
const responseResult = validator.validateResponse(
'/api/v2/apps/create',
'POST',
'2.0.0',
{ success: true, appId: '123e4567-e89b-12d3-a456-426614174000', createdAt: '2026-12-25T10:00:00Z' }
);
console.log(responseResult);
// { valid: true }
This contract validator provides:
- JSON Schema validation: Industry-standard schema format
- Request/response validation: Catches breaking changes before deployment
- Format validation: Built-in validators for email, UUID, date-time, etc.
- Detailed error messages: Pinpoints exact validation failures
Version Negotiation & Routing
Version negotiation allows clients to request a specific API version, enabling gradual migration and dual-running versions. The MCP server routes requests to the appropriate version handler based on:
- URL path versioning (
/api/v1/apps,/api/v2/apps) - Header-based versioning (
X-API-Version: 2.0.0) - Query parameter versioning (
/api/apps?api_version=2.0.0)
Version Routing Implementation
This Express.js router implements intelligent version routing with fallback logic:
// src/versioning/VersionRouter.ts
import { Router, Request, Response, NextFunction } from 'express';
/**
* Version-specific handler
*/
export interface VersionHandler {
version: string;
handler: (req: Request, res: Response, next: NextFunction) => void;
}
/**
* Version negotiation strategy
*/
export enum NegotiationStrategy {
PATH = 'path', // /api/v1/apps
HEADER = 'header', // X-API-Version: 1.0.0
QUERY = 'query', // ?api_version=1.0.0
}
/**
* Version router for MCP server
* Routes requests to appropriate version handler
*/
export class VersionRouter {
private router: Router;
private strategy: NegotiationStrategy;
private defaultVersion: string;
private handlers: Map<string, Map<string, VersionHandler>> = new Map();
constructor(strategy: NegotiationStrategy = NegotiationStrategy.HEADER, defaultVersion: string = '1.0.0') {
this.router = Router();
this.strategy = strategy;
this.defaultVersion = defaultVersion;
}
/**
* Register version-specific handler
*/
public register(endpoint: string, versionHandler: VersionHandler): void {
if (!this.handlers.has(endpoint)) {
this.handlers.set(endpoint, new Map());
}
this.handlers.get(endpoint)!.set(versionHandler.version, versionHandler);
}
/**
* Build Express router with version routing
*/
public build(): Router {
for (const [endpoint, versions] of this.handlers.entries()) {
this.router.all(endpoint, (req, res, next) => {
const requestedVersion = this.extractVersion(req);
const handler = versions.get(requestedVersion);
if (!handler) {
// Fallback to default version
const fallbackHandler = versions.get(this.defaultVersion);
if (!fallbackHandler) {
return res.status(400).json({
error: 'Unsupported API version',
requestedVersion,
supportedVersions: Array.from(versions.keys()),
});
}
return fallbackHandler.handler(req, res, next);
}
handler.handler(req, res, next);
});
}
return this.router;
}
/**
* Extract version from request based on strategy
*/
private extractVersion(req: Request): string {
switch (this.strategy) {
case NegotiationStrategy.HEADER:
return (req.headers['x-api-version'] as string) || this.defaultVersion;
case NegotiationStrategy.QUERY:
return (req.query.api_version as string) || this.defaultVersion;
case NegotiationStrategy.PATH:
// Extract from path: /api/v2/apps -> 2.0.0
const match = req.path.match(/\/v(\d+)/);
return match ? `${match[1]}.0.0` : this.defaultVersion;
default:
return this.defaultVersion;
}
}
}
Usage example:
import express from 'express';
import { VersionRouter, NegotiationStrategy } from './versioning/VersionRouter';
const app = express();
const versionRouter = new VersionRouter(NegotiationStrategy.HEADER, '2.0.0');
// Register v1 handler
versionRouter.register('/api/apps', {
version: '1.0.0',
handler: (req, res) => {
// Legacy implementation (returns "user_id" field)
res.json({ apps: [{ id: '1', name: 'App 1', user_id: 'user-123' }] });
},
});
// Register v2 handler
versionRouter.register('/api/apps', {
version: '2.0.0',
handler: (req, res) => {
// Modern implementation (returns "userId" field)
res.json({ apps: [{ id: '1', name: 'App 1', userId: 'user-123' }] });
},
});
app.use(versionRouter.build());
app.listen(3000, () => {
console.log('MCP server with version routing running on port 3000');
});
// Client requests:
// curl -H "X-API-Version: 1.0.0" http://localhost:3000/api/apps -> v1 response
// curl -H "X-API-Version: 2.0.0" http://localhost:3000/api/apps -> v2 response
// curl http://localhost:3000/api/apps -> v2 response (default)
This version router provides:
- Flexible negotiation strategies: Support multiple versioning approaches
- Graceful degradation: Falls back to default version if requested version unsupported
- Dual-running versions: Run v1 and v2 handlers simultaneously during migration
- Clear error messages: Returns supported versions if invalid version requested
Changelog Generation Automation
Automated changelog generation documents version changes for developers integrating with your MCP server. The changelog should include:
- Breaking changes (MAJOR version bumps)
- New features (MINOR version bumps)
- Bug fixes (PATCH version bumps)
- Deprecations with sunset dates
- Migration guides for breaking changes
Changelog Generator Implementation
This TypeScript changelog generator creates markdown changelogs from version history:
// src/versioning/ChangelogGenerator.ts
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { VersionHistoryEntry } from './VersionManager';
/**
* Changelog generator for MCP server
* Generates markdown changelog from version history
*/
export class ChangelogGenerator {
private historyFilePath: string;
private changelogFilePath: string;
constructor(baseDir: string = process.cwd()) {
this.historyFilePath = join(baseDir, 'version-history.json');
this.changelogFilePath = join(baseDir, 'CHANGELOG.md');
}
/**
* Generate changelog from version history
*/
public generate(): string {
const history: VersionHistoryEntry[] = JSON.parse(readFileSync(this.historyFilePath, 'utf-8'));
let changelog = '# Changelog\n\n';
changelog += 'All notable changes to this MCP server API will be documented in this file.\n\n';
changelog += 'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\n';
changelog += 'and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n';
// Group by version
const versionGroups = this.groupByVersion(history);
for (const [version, entries] of versionGroups.entries()) {
const latestEntry = entries[0]; // First entry has latest timestamp
const date = new Date(latestEntry.timestamp).toISOString().split('T')[0];
changelog += `## [${version}] - ${date}\n\n`;
// Breaking changes section
const breakingChanges = entries.flatMap((e) => e.breakingChanges || []);
if (breakingChanges.length > 0) {
changelog += '### ⚠️ BREAKING CHANGES\n\n';
for (const change of breakingChanges) {
changelog += `- ${change}\n`;
}
changelog += '\n';
}
// Deprecations section
const deprecations = entries.flatMap((e) => e.deprecated || []);
if (deprecations.length > 0) {
changelog += '### Deprecated\n\n';
for (const deprecation of deprecations) {
changelog += `- ${deprecation}\n`;
}
changelog += '\n';
}
// Changes section
changelog += '### Changes\n\n';
for (const entry of entries) {
changelog += `- ${entry.description}\n`;
}
changelog += '\n';
}
return changelog;
}
/**
* Save changelog to CHANGELOG.md
*/
public save(): void {
const changelog = this.generate();
writeFileSync(this.changelogFilePath, changelog);
}
/**
* Group version history by version number
*/
private groupByVersion(history: VersionHistoryEntry[]): Map<string, VersionHistoryEntry[]> {
const groups = new Map<string, VersionHistoryEntry[]>();
for (const entry of history) {
const existing = groups.get(entry.version) || [];
existing.push(entry);
groups.set(entry.version, existing);
}
return groups;
}
}
Usage example:
import { ChangelogGenerator } from './versioning/ChangelogGenerator';
const generator = new ChangelogGenerator();
generator.save(); // Writes to CHANGELOG.md
Generated CHANGELOG.md:
# Changelog
All notable changes to this MCP server API will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.0] - 2026-12-25
### ⚠️ BREAKING CHANGES
- Removed legacy API key authentication
- Changed /auth/login endpoint signature
### Deprecated
- POST /auth/legacy-login (use POST /auth/oauth2 instead)
### Changes
- Migrated authentication to OAuth 2.1 with PKCE
## [1.1.0] - 2026-12-20
### Changes
- Added support for file upload in widget state
## [1.0.1] - 2026-12-15
### Changes
- Fixed authentication token expiration edge case
Conclusion: Prevent Breaking Changes with Disciplined Versioning
MCP server versioning is the discipline that prevents breaking changes from disrupting thousands of ChatGPT users. By implementing semantic versioning automation, backward compatibility layers, deprecation workflows, contract testing, and intelligent version routing, you ensure your MCP server evolves without breaking existing integrations.
The production-ready code examples in this guide provide:
- VersionManager (120 lines): Automate SemVer bumps and track version history
- BackwardCompatibilityLayer (130 lines): Transform requests/responses between API versions
- DeprecationMiddleware (110 lines): Enforce sunset policies and track deprecated endpoint usage
- ContractValidator (80 lines): Validate API contracts with JSON Schema
- VersionRouter (90 lines): Route requests to appropriate version handlers
- ChangelogGenerator (110 lines): Automate changelog generation from version history
Next steps:
- Implement VersionManager in your MCP server to enforce SemVer discipline
- Register deprecation policies for endpoints you plan to sunset
- Define API contracts with JSON Schema and validate in CI/CD pipeline
- Set up version routing to support gradual migration
- Generate automated changelogs on every release
Ready to implement production-grade versioning for your ChatGPT app? Start your free trial with MakeAIHQ and generate version-aware MCP servers with built-in backward compatibility in minutes—no coding required.
Related Resources
- MCP Server Development Complete Guide - Master MCP server architecture and best practices
- MCP Server Deployment Best Practices - Deploy MCP servers with zero-downtime updates
- MCP Server Monitoring & Logging Guide - Track version adoption and migration progress
- MCP Server Error Recovery Patterns - Handle version mismatch errors gracefully
- ChatGPT App Testing & QA Complete Guide - Test version compatibility with contract testing
External References
- Semantic Versioning 2.0.0 Specification - Official SemVer specification
- Consumer-Driven Contract Testing - Martin Fowler's guide to contract testing
- API Deprecation Best Practices - Industry best practices for API deprecation
- RFC 8594: Sunset HTTP Header - Standard for communicating API deprecation
About MakeAIHQ: We're the only no-code platform specifically designed for ChatGPT App Store development. From zero to ChatGPT app in 48 hours—no coding required. Start building today.