Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-dodos-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1-to-v2 codemod now points migrated legacy server transport entries at `serveStdio` or `createMcpHandler` when adopting protocol revision 2026-07-28.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { handlerRegistrationTransform } from './handlerRegistration';
import { importPathsTransform } from './importPaths';
import { mcpServerApiTransform } from './mcpServerApi';
import { mockPathsTransform } from './mockPaths';
import { modernServerEntryNoticeTransform } from './modernServerEntryNotice';
import { removedApisTransform } from './removedApis';
import { schemaParamRemovalTransform } from './schemaParamRemoval';
import { symbolRenamesTransform } from './symbolRenames';
Expand Down Expand Up @@ -37,6 +38,7 @@ import { symbolRenamesTransform } from './symbolRenames';
// independent of the other transforms.
export const v1ToV2Transforms: Transform[] = [
importPathsTransform,
modernServerEntryNoticeTransform,
symbolRenamesTransform,
removedApisTransform,
mcpServerApiTransform,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { SyntaxKind } from 'ts-morph';

import type { Diagnostic, Transform, TransformResult } from '../../../types';
import { info } from '../../../utils/diagnostics';

const MODERN_PROTOCOL_GUIDE = 'https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28#serving-the-2026-07-28-revision';

const LEGACY_SERVER_TRANSPORTS = new Map([
[
'@modelcontextprotocol/server/stdio:StdioServerTransport',
'This StdioServerTransport entry serves 2025-era clients only. To also serve protocol revision 2026-07-28, use serveStdio(() => buildServer()).'
],
[
'@modelcontextprotocol/node:NodeStreamableHTTPServerTransport',
'This NodeStreamableHTTPServerTransport entry serves 2025-era clients only. To also serve protocol revision 2026-07-28, see the migration guide for routing modern clients through createMcpHandler while preserving legacy session state.'
],
[
'@modelcontextprotocol/server:WebStandardStreamableHTTPServerTransport',
'This WebStandardStreamableHTTPServerTransport entry serves 2025-era clients only. To also serve protocol revision 2026-07-28, use createMcpHandler(() => buildServer()).'
]
]);

export const modernServerEntryNoticeTransform: Transform = {
name: 'Modern server entry notices',
id: 'modern-server-entry-notices',
apply(sourceFile): TransformResult {
const diagnostics: Diagnostic[] = [];
const filePath = sourceFile.getFilePath();

for (const declaration of sourceFile.getImportDeclarations()) {
const specifier = declaration.getModuleSpecifierValue();

for (const namedImport of declaration.getNamedImports()) {
const message = LEGACY_SERVER_TRANSPORTS.get(`${specifier}:${namedImport.getName()}`);
if (message === undefined || namedImport.isTypeOnly() || declaration.isTypeOnly()) continue;

const binding = namedImport.getAliasNode() ?? namedImport.getNameNode().asKind(SyntaxKind.Identifier);
if (binding === undefined) continue;
const constructions = binding
.findReferencesAsNodes()
.filter(reference => reference.getSourceFile() === sourceFile)
.map(reference => reference.getParentIfKind(SyntaxKind.NewExpression))
.filter(construction => construction?.getExpression().getText() === binding.getText());

for (const construction of constructions) {
if (construction === undefined) continue;
diagnostics.push({
...info(filePath, construction.getStartLineNumber(), `${message} See ${MODERN_PROTOCOL_GUIDE}.`),
advisoryOnly: true,
resolveCurrentLine: () => construction.getStartLineNumber()
});
}
}

const namespaceImport = declaration.getNamespaceImport();
if (namespaceImport === undefined || declaration.isTypeOnly()) continue;

for (const [transport, message] of LEGACY_SERVER_TRANSPORTS) {
const separator = transport.lastIndexOf(':');
if (transport.slice(0, separator) !== specifier) continue;
const transportName = transport.slice(separator + 1);

for (const construction of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
const expression = construction.getExpression().asKind(SyntaxKind.PropertyAccessExpression);
if (expression?.getExpression().getText() !== namespaceImport.getText() || expression.getName() !== transportName) {
continue;
}
diagnostics.push({
...info(filePath, construction.getStartLineNumber(), `${message} See ${MODERN_PROTOCOL_GUIDE}.`),
advisoryOnly: true,
resolveCurrentLine: () => construction.getStartLineNumber()
});
}
}
}

return { changesCount: 0, diagnostics };
}
};
134 changes: 134 additions & 0 deletions packages/codemod/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,140 @@ describe('integration', () => {
expect(output).not.toContain('extra');
});

it('points migrated stdio servers at the modern serving entry', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
`import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';`,
`const server = new McpServer({ name: 'test', version: '1.0' });`,
`await server.connect(new StdioServerTransport());`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notice = result.diagnostics.find(d => d.message.includes('serveStdio'));
expect(notice?.level).toBe(DiagnosticLevel.Info);
expect(notice?.message).toContain('2026-07-28');
expect(notice?.advisoryOnly).toBe(true);
});

it('points aliased migrated HTTP servers at the modern serving entry', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import { StreamableHTTPServerTransport as HttpTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';`,
`const transport = new HttpTransport({});`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notice = result.diagnostics.find(d => d.message.includes('createMcpHandler'));
expect(notice?.level).toBe(DiagnosticLevel.Info);
expect(notice?.message).toContain('2026-07-28');
expect(notice?.advisoryOnly).toBe(true);
});

it('points namespace-imported migrated servers at the modern serving entry', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import * as stdio from '@modelcontextprotocol/sdk/server/stdio.js';`,
`const transport = new stdio.StdioServerTransport();`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notice = result.diagnostics.find(d => d.message.includes('serveStdio'));
expect(notice?.level).toBe(DiagnosticLevel.Info);
expect(notice?.message).toContain('2026-07-28');
expect(notice?.advisoryOnly).toBe(true);
});

it('points migrated web-standard HTTP servers at the modern serving entry', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';`,
`const transport = new WebStandardStreamableHTTPServerTransport();`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notice = result.diagnostics.find(d => d.message.includes('createMcpHandler'));
expect(notice?.level).toBe(DiagnosticLevel.Info);
expect(notice?.message).toContain('2026-07-28');
expect(notice?.advisoryOnly).toBe(true);
});

it('emits a modern server notice for every construction call site', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';`,
`if (process.argv[2] === 'a') new StdioServerTransport();`,
`else new StdioServerTransport();`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notices = result.diagnostics.filter(d => d.message.includes('serveStdio'));
expect(notices).toHaveLength(2);
});

it('resolves modern server notice lines after later transforms rewrite imports', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'server.ts'),
[
`import type { IsomorphicHeaders } from '@modelcontextprotocol/sdk/server/mcp.js';`,
`import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';`,
`const headers: IsomorphicHeaders = {};`,
`new StdioServerTransport();`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

const notice = result.diagnostics.find(d => d.message.includes('serveStdio'));
const outputLines = readFileSync(path.join(dir, 'server.ts'), 'utf8').split(/\r?\n/);
expect(notice?.line).toBeDefined();
expect(outputLines[notice!.line! - 1]).toContain('new StdioServerTransport()');
});

it('does not emit a modern server notice for unused server imports or client transports', () => {
const dir = createTempDir();
writeFileSync(
path.join(dir, 'client.ts'),
[
`import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';`,
`import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';`,
`const transport = new StdioClientTransport({ command: 'node', args: ['server.js'] });`,
``
].join('\n')
);

const result = run(migration, { targetDir: dir });

expect(result.diagnostics.some(d => d.message.includes('2026-07-28'))).toBe(false);
});

it('preserves a leading #! shebang on a migrated file', () => {
// Regression: the imports transform consumed the line-1 shebang (leading trivia of the first
// import), silently breaking CLI packages whose `bin` points at the compiled entry.
Expand Down
Loading