v2 backcompat: server legacy package (#2206)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Konstantin Konstantinov 2026-06-01 17:33:32 +03:00 • committed by GitHub
commit e03bca90c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 5797 additions and 71 deletions

View file

@ -9,7 +9,8 @@ const PACKAGE_DIRS: Record<string, string> = {
'@modelcontextprotocol/client': 'client',
'@modelcontextprotocol/server': 'server',
'@modelcontextprotocol/node': 'middleware/node',
'@modelcontextprotocol/express': 'middleware/express'
'@modelcontextprotocol/express': 'middleware/express',
'@modelcontextprotocol/server-legacy': 'server-legacy'
};
const versions: Record<string, string> = {};

View file

@ -3,5 +3,6 @@ export const V2_PACKAGE_VERSIONS: Record<string, string> = {
'@modelcontextprotocol/client': '^2.0.0-alpha.2',
'@modelcontextprotocol/server': '^2.0.0-alpha.2',
'@modelcontextprotocol/node': '^2.0.0-alpha.2',
'@modelcontextprotocol/express': '^2.0.0-alpha.2'
'@modelcontextprotocol/express': '^2.0.0-alpha.2',
'@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2'
};

View file

@ -7,6 +7,8 @@ export interface ImportMapping {
removalMessage?: string;
/** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */
isV2Gap?: boolean;
/** Emitted as an info diagnostic after a successful move, suggesting eventual migration to v2 equivalents. */
migrationHint?: string;
/**
* Subpath suffix appended after `RESOLVE_BY_CONTEXT` resolves the base package (e.g. `/validators/ajv`).
* The final target becomes `@modelcontextprotocol/{client,server}<subpathSuffix>`.
@ -74,9 +76,10 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
status: 'moved'
},
'@modelcontextprotocol/sdk/server/sse.js': {
target: '',
status: 'removed',
removalMessage: 'SSE server transport removed in v2. Migrate to NodeStreamableHTTPServerTransport from @modelcontextprotocol/node.'
target: '@modelcontextprotocol/server-legacy/sse',
status: 'moved',
migrationHint:
'SSEServerTransport is deprecated. Migrate to NodeStreamableHTTPServerTransport from @modelcontextprotocol/node or WebStandardStreamableHTTPServerTransport from @modelcontextprotocol/server.'
},
'@modelcontextprotocol/sdk/server/middleware.js': {
target: '@modelcontextprotocol/express',
@ -90,34 +93,29 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
},
'@modelcontextprotocol/sdk/server/auth/types.js': {
target: '',
status: 'removed',
removalMessage:
'Server auth removed in v2. AuthInfo is now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server.'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy auth types. AuthInfo is also re-exported by @modelcontextprotocol/server.'
},
'@modelcontextprotocol/sdk/server/auth/provider.js': {
target: '',
status: 'removed',
removalMessage:
'Server auth provider removed in v2. For Resource-Server auth (token verification), see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy OAuth AS provider. For RS-only auth, see requireBearerAuth from @modelcontextprotocol/express.'
},
'@modelcontextprotocol/sdk/server/auth/router.js': {
target: '',
status: 'removed',
removalMessage:
'Server auth router removed in v2. For metadata endpoints, see mcpAuthMetadataRouter from @modelcontextprotocol/express. For full OAuth AS router, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy OAuth AS router. For metadata-only endpoints, see mcpAuthMetadataRouter from @modelcontextprotocol/express.'
},
'@modelcontextprotocol/sdk/server/auth/middleware.js': {
target: '',
status: 'removed',
removalMessage:
'Server auth middleware removed in v2. For bearer token validation, see requireBearerAuth from @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy OAuth AS middleware. For bearer-only auth, see requireBearerAuth from @modelcontextprotocol/express.'
},
'@modelcontextprotocol/sdk/server/auth/errors.js': {
target: '',
status: 'removed',
removalMessage:
'Auth error subclasses consolidated in v2. Use OAuthError + OAuthErrorCode from @modelcontextprotocol/server. See also @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy error subclasses. v2 consolidates to OAuthError + OAuthErrorCode in @modelcontextprotocol/server.'
},
'@modelcontextprotocol/sdk/types.js': {

View file

@ -2,7 +2,7 @@ import type { SourceFile } from 'ts-morph';
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
import { renameAllReferences } from '../../../utils/astUtils.js';
import { v2Gap, warning } from '../../../utils/diagnostics.js';
import { info, v2Gap, warning } from '../../../utils/diagnostics.js';
import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js';
import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js';
import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js';
@ -75,10 +75,9 @@ export const importPathsTransform: Transform = {
if (!mapping && isAuthImport(specifier)) {
mapping = {
target: '',
status: 'removed',
removalMessage:
'Server auth removed in v2. For RS auth, see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy auth module. For RS-only auth, see @modelcontextprotocol/express.'
};
}
@ -156,6 +155,9 @@ export const importPathsTransform: Transform = {
}
}
changesCount++;
if (mapping.migrationHint) {
diagnostics.push(info(filePath, line, mapping.migrationHint));
}
for (const [oldName, newName] of symbolsToRenameInFile) {
renameAllReferences(sourceFile, oldName, newName);
}
@ -172,6 +174,9 @@ export const importPathsTransform: Transform = {
}
imp.remove();
changesCount++;
if (mapping.migrationHint) {
diagnostics.push(info(filePath, line, mapping.migrationHint));
}
for (const [oldName, newName] of symbolsToRenameInFile) {
renameAllReferences(sourceFile, oldName, newName);
}
@ -222,10 +227,9 @@ function rewriteExportDeclarations(
if (!mapping && isAuthImport(specifier)) {
mapping = {
target: '',
status: 'removed',
removalMessage:
'Server auth removed in v2. For RS auth, see @modelcontextprotocol/express. For full OAuth AS, see @modelcontextprotocol/server-auth-legacy (PR #1908).'
target: '@modelcontextprotocol/server-legacy/auth',
status: 'moved',
migrationHint: 'Legacy auth module. For RS-only auth, see @modelcontextprotocol/express.'
};
}
@ -288,6 +292,9 @@ function rewriteExportDeclarations(
}
}
changesCount++;
if (mapping.migrationHint) {
diagnostics.push(info(filePath, line, mapping.migrationHint));
}
}
return changesCount;

View file

@ -49,7 +49,7 @@ function resolveTarget(
| { removed: true; isV2Gap?: boolean; removalMessage?: string }
| null {
const mapping = IMPORT_MAP[specifier];
if (!mapping && isAuthImport(specifier)) return { removed: true };
if (!mapping && isAuthImport(specifier)) return { target: '@modelcontextprotocol/server-legacy/auth' };
if (!mapping) return null;
if (mapping.status === 'removed') return { removed: true, isV2Gap: mapping.isV2Gap, removalMessage: mapping.removalMessage };

View file

@ -23,7 +23,7 @@ afterEach(() => {
});
describe('CLI diagnostic behavior', () => {
it('warnings do not produce errors-level diagnostics', () => {
it('info diagnostics do not produce error-level diagnostics', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
@ -36,9 +36,9 @@ describe('CLI diagnostic behavior', () => {
const result = run(migration, { targetDir: dir });
const warnings = result.diagnostics.filter(d => d.level === DiagnosticLevel.Warning);
const infos = result.diagnostics.filter(d => d.level === DiagnosticLevel.Info);
const errors = result.diagnostics.filter(d => d.level === DiagnosticLevel.Error);
expect(warnings.length).toBeGreaterThan(0);
expect(infos.length).toBeGreaterThan(0);
expect(errors.length).toBe(0);
});

View file

@ -573,7 +573,7 @@ describe('integration', () => {
expect(result.packageJsonChanges!.removed).toContain('@modelcontextprotocol/sdk');
});
it('emits diagnostics for removed imports', () => {
it('emits info diagnostics for legacy-moved imports', () => {
const dir = createTempDir();
const input = [
`import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';`,
@ -586,7 +586,7 @@ describe('integration', () => {
const result = run(migration, { targetDir: dir });
expect(result.diagnostics.length).toBeGreaterThan(0);
expect(result.diagnostics.some(d => d.level === DiagnosticLevel.Warning)).toBe(true);
expect(result.diagnostics.some(d => d.level === DiagnosticLevel.Info)).toBe(true);
});
it('transform ordering: critical dependencies are maintained', () => {

View file

@ -59,14 +59,19 @@ describe('import-paths transform', () => {
expect(result.diagnostics[0]!.message).toContain('WebSocketClientTransport');
});
it('removes SSE server import with warning', () => {
it('moves SSE server import to server-legacy/sse with info diagnostic', () => {
const input = `import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\n`;
const ctx: TransformContext = { projectType: 'server' };
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = importPathsTransform.apply(sourceFile, ctx);
const output = sourceFile.getFullText();
expect(output).toContain('@modelcontextprotocol/server-legacy/sse');
expect(output).toContain('SSEServerTransport');
expect(output).not.toContain('@modelcontextprotocol/sdk');
expect(result.changesCount).toBeGreaterThan(0);
expect(result.diagnostics.length).toBeGreaterThan(0);
expect(result.diagnostics[0]!.message).toContain('SSE server transport');
expect(result.diagnostics[0]!.message).toContain('SSEServerTransport is deprecated');
});
it('resolves sdk/types.js based on sibling client imports', () => {
@ -270,13 +275,79 @@ describe('import-paths transform', () => {
expect(result).toContain('NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport');
});
it('removes auth imports with warning', () => {
it('moves auth router import to server-legacy/auth with info diagnostic', () => {
const input = `import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';\n`;
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
const output = sourceFile.getFullText();
expect(output).toContain('@modelcontextprotocol/server-legacy/auth');
expect(output).toContain('mcpAuthRouter');
expect(output).not.toContain('@modelcontextprotocol/sdk');
expect(result.changesCount).toBeGreaterThan(0);
expect(result.diagnostics.length).toBeGreaterThan(0);
expect(result.diagnostics[0]!.message).toContain('auth router removed');
expect(result.diagnostics[0]!.message).toContain('Legacy OAuth AS router');
});
it('moves auth provider import to server-legacy/auth', () => {
const input = `import type { OAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/provider.js';\n`;
const result = applyTransform(input, { projectType: 'server' });
expect(result).toContain('@modelcontextprotocol/server-legacy/auth');
expect(result).toContain('OAuthServerProvider');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('moves auth middleware import to server-legacy/auth', () => {
const input = `import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware.js';\n`;
const result = applyTransform(input, { projectType: 'server' });
expect(result).toContain('@modelcontextprotocol/server-legacy/auth');
expect(result).toContain('requireBearerAuth');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('moves auth errors import to server-legacy/auth', () => {
const input = `import { InvalidTokenError, OAuthError } from '@modelcontextprotocol/sdk/server/auth/errors.js';\n`;
const result = applyTransform(input, { projectType: 'server' });
expect(result).toContain('@modelcontextprotocol/server-legacy/auth');
expect(result).toContain('InvalidTokenError');
expect(result).toContain('OAuthError');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('consolidates multiple auth subpath imports into single server-legacy/auth import', () => {
const input = [
`import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';`,
`import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware.js';`,
''
].join('\n');
const result = applyTransform(input, { projectType: 'server' });
expect(result).toContain('mcpAuthRouter');
expect(result).toContain('requireBearerAuth');
expect(result).toContain('@modelcontextprotocol/server-legacy/auth');
const importLines = result.split('\n').filter((l: string) => l.includes('@modelcontextprotocol/server-legacy/auth'));
expect(importLines.length).toBe(1);
});
it('moves SSE server re-export to server-legacy/sse', () => {
const input = `export { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\n`;
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
const output = sourceFile.getFullText();
expect(output).toContain('@modelcontextprotocol/server-legacy/sse');
expect(output).toContain('SSEServerTransport');
expect(result.diagnostics.some(d => d.message.includes('SSEServerTransport is deprecated'))).toBe(true);
});
it('includes server-legacy in usedPackages for SSE import', () => {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile(
'test.ts',
`import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\n`
);
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
expect(result.usedPackages).toBeDefined();
expect(result.usedPackages!.has('@modelcontextprotocol/server-legacy/sse')).toBe(true);
});
it('handles per-specifier type modifiers', () => {
@ -373,14 +444,17 @@ describe('import-paths transform', () => {
expect(result.diagnostics[0]!.message).toContain('removed');
});
it('handles unknown auth subpath via catch-all', () => {
it('moves unknown auth subpath to server-legacy/auth via catch-all', () => {
const input = `import { SomeType } from '@modelcontextprotocol/sdk/server/auth/handler.js';\n`;
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
expect(result.changesCount).toBe(1);
expect(sourceFile.getFullText()).not.toContain('@modelcontextprotocol/sdk');
expect(result.diagnostics.some(d => d.message.includes('Server auth removed'))).toBe(true);
const output = sourceFile.getFullText();
expect(output).toContain('@modelcontextprotocol/server-legacy/auth');
expect(output).toContain('SomeType');
expect(output).not.toContain('@modelcontextprotocol/sdk');
expect(result.diagnostics.some(d => d.message.includes('Legacy auth module'))).toBe(true);
});
it('rewrites InMemoryTransport to @modelcontextprotocol/server for server projects', () => {

View file

@ -193,13 +193,22 @@ describe('mock-paths transform', () => {
expect(result.diagnostics[0]!.message).toContain('Unknown SDK dynamic import path');
});
it('emits warning for removed SDK dynamic import path', () => {
it('rewrites SSE dynamic import to server-legacy/sse', () => {
const input = [`const m = await import('@modelcontextprotocol/sdk/server/sse.js');`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = mockPathsTransform.apply(sourceFile, ctx);
expect(result.changesCount).toBeGreaterThan(0);
expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/server-legacy/sse');
});
it('emits warning for removed SDK dynamic import path', () => {
const input = [`const m = await import('@modelcontextprotocol/sdk/client/websocket.js');`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = mockPathsTransform.apply(sourceFile, ctx);
expect(result.diagnostics.length).toBeGreaterThan(0);
expect(result.diagnostics[0]!.message).toContain('SSE server transport removed in v2');
expect(result.diagnostics[0]!.message).toContain('WebSocketClientTransport removed in v2');
});
it('emits warning for unknown SDK mock path', () => {