Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 69131 additions and 9001 deletions

View file

@ -293,7 +293,7 @@ describe('integration', () => {
expect(output).toContain('McpError');
});
it('applies new transforms (removed APIs, SchemaInput, express middleware)', () => {
it('applies new transforms (removed APIs, SchemaInput, middleware import)', () => {
const dir = createTempDir();
const input = [
`import { McpServer, schemaToJson, IsomorphicHeaders } from '@modelcontextprotocol/sdk/server/mcp.js';`,
@ -304,7 +304,7 @@ describe('integration', () => {
`type Input = SchemaInput<typeof mySchema>;`,
`const h: IsomorphicHeaders = {};`,
`if (error instanceof StreamableHTTPError) {}`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost'] }));`,
`app.use(hostHeaderValidation(['localhost']));`,
``
].join('\n');
@ -331,9 +331,9 @@ describe('integration', () => {
// schemaToJson removed (import gone)
expect(output).not.toContain('schemaToJson');
// hostHeaderValidation signature migrated
// hostHeaderValidation import rewritten to @modelcontextprotocol/express; call unchanged
expect(output).toContain("hostHeaderValidation(['localhost'])");
expect(output).not.toContain('allowedHosts');
expect(output).toContain('@modelcontextprotocol/express');
// Diagnostics emitted
expect(result.diagnostics.length).toBeGreaterThan(0);
@ -534,7 +534,7 @@ describe('integration', () => {
[
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
`import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware.js';`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost'] }));`,
`app.use(hostHeaderValidation(['localhost']));`,
``
].join('\n')
);

View file

@ -1,120 +0,0 @@
import { describe, it, expect } from 'vitest';
import { Project } from 'ts-morph';
import { expressMiddlewareTransform } from '../../../src/migrations/v1-to-v2/transforms/expressMiddleware';
import type { TransformContext } from '../../../src/types';
const ctx: TransformContext = { projectType: 'server' };
function applyTransform(code: string) {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', code);
const result = expressMiddlewareTransform.apply(sourceFile, ctx);
return { text: sourceFile.getFullText(), result };
}
describe('express-middleware transform', () => {
it('rewrites hostHeaderValidation({ allowedHosts: [...] }) to hostHeaderValidation([...])', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost', '127.0.0.1'] }));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(text).toContain("hostHeaderValidation(['localhost', '127.0.0.1'])");
expect(text).not.toContain('allowedHosts');
expect(result.changesCount).toBe(1);
});
it('preserves calls that already use array syntax', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`app.use(hostHeaderValidation(['localhost', '127.0.0.1']));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(text).toContain("hostHeaderValidation(['localhost', '127.0.0.1'])");
expect(result.changesCount).toBe(0);
});
it('handles variable references in allowedHosts', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`const hosts = ['localhost'];`,
`app.use(hostHeaderValidation({ allowedHosts: hosts }));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(text).toContain('hostHeaderValidation(hosts)');
expect(text).not.toContain('allowedHosts');
expect(result.changesCount).toBe(1);
});
it('does not modify calls with non-object arguments', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`app.use(hostHeaderValidation(someVariable));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(text).toContain('hostHeaderValidation(someVariable)');
expect(result.changesCount).toBe(0);
});
it('does not modify calls with no arguments', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`app.use(hostHeaderValidation());`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(text).toContain('hostHeaderValidation()');
expect(result.changesCount).toBe(0);
});
it('is idempotent', () => {
const input = [
`import { hostHeaderValidation } from '@modelcontextprotocol/express';`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost'] }));`,
''
].join('\n');
const { text: first } = applyTransform(input);
const { text: second } = applyTransform(first);
expect(second).toBe(first);
});
it('does not modify calls when hostHeaderValidation is not from MCP', () => {
const input = [
`import { hostHeaderValidation } from './my-middleware.js';`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost'] }));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(result.changesCount).toBe(0);
expect(text).toContain("{ allowedHosts: ['localhost'] }");
});
it('applies transform when hostHeaderValidation is aliased', () => {
const input = [
`import { hostHeaderValidation as hhv } from '@modelcontextprotocol/express';`,
`app.use(hhv({ allowedHosts: ['localhost'] }));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(result.changesCount).toBe(1);
expect(text).toContain("hhv(['localhost'])");
expect(text).not.toContain('allowedHosts');
});
it('does not modify non-MCP hostHeaderValidation even when other MCP imports exist', () => {
const input = [
`import { McpServer } from '@modelcontextprotocol/server';`,
`import { hostHeaderValidation } from './my-middleware.js';`,
`app.use(hostHeaderValidation({ allowedHosts: ['localhost'] }));`,
''
].join('\n');
const { text, result } = applyTransform(input);
expect(result.changesCount).toBe(0);
expect(text).toContain("{ allowedHosts: ['localhost'] }");
});
});

View file

@ -93,7 +93,7 @@ describe('handler-registration transform', () => {
it('does not replace schema identifiers from non-MCP packages', () => {
const input = [
`import { CallToolRequestSchema } from './local-schemas.js';`,
`import { CallToolRequestSchema } from './local-schemas';`,
`server.setRequestHandler(CallToolRequestSchema, async (request) => {`,
` return { content: [] };`,
`});`,
@ -106,14 +106,14 @@ describe('handler-registration transform', () => {
it('does not rewrite local import when aliased MCP import has same export name', () => {
const input = [
`import { CallToolRequestSchema } from './local-schemas.js';`,
`import { CallToolRequestSchema } from './local-schemas';`,
`import { CallToolRequestSchema as McpSchema } from '@modelcontextprotocol/sdk/types.js';`,
`server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [] }));`,
`validateSchema(McpSchema);`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain("from './local-schemas.js'");
expect(result).toContain("from './local-schemas'");
expect(result).toContain('setRequestHandler(CallToolRequestSchema');
expect(result).not.toContain("'tools/call'");
});
@ -219,26 +219,39 @@ describe('handler-registration transform', () => {
expect(result).not.toContain('ElicitationCompleteNotificationSchema');
});
it('replaces TaskStatusNotificationSchema with the tasks/status method string', () => {
const input = [
`import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`,
`client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain("setNotificationHandler('notifications/tasks/status'");
expect(result).not.toContain('TaskStatusNotificationSchema');
});
it('replaces task request schemas (GetTaskRequestSchema → tasks/get)', () => {
it('emits a tasks-removed diagnostic for GetTaskRequestSchema (does NOT rewrite to tasks/get)', () => {
const input = [
`import { GetTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`server.setRequestHandler(GetTaskRequestSchema, async () => ({}));`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain("setRequestHandler('tasks/get'");
expect(result).not.toContain('GetTaskRequestSchema');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = handlerRegistrationTransform.apply(sourceFile, ctx);
const text = sourceFile.getFullText();
expect(text).not.toContain("'tasks/get'");
expect(text).toContain('setRequestHandler(GetTaskRequestSchema');
expect(result.changesCount).toBe(0);
expect(result.diagnostics.length).toBe(1);
expect(result.diagnostics[0]!.message).toContain('Task handler registration');
expect(result.diagnostics[0]!.message).toContain('SEP-2663');
});
it('emits a tasks-removed diagnostic for TaskStatusNotificationSchema (does NOT rewrite)', () => {
const input = [
`import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`,
`client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', input);
const result = handlerRegistrationTransform.apply(sourceFile, ctx);
const text = sourceFile.getFullText();
expect(text).not.toContain("'notifications/tasks/status'");
expect(text).toContain('setNotificationHandler(TaskStatusNotificationSchema');
expect(result.changesCount).toBe(0);
expect(result.diagnostics.length).toBe(1);
expect(result.diagnostics[0]!.message).toContain('Task handler registration');
});
it('does not emit diagnostic when first arg is a string literal (v2 style)', () => {

View file

@ -467,6 +467,38 @@ describe('import-paths transform', () => {
expect(result).toContain(`from "@modelcontextprotocol/express"`);
});
it('rewrites server/express.js import to @modelcontextprotocol/express', () => {
const input = `import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';\n`;
const result = applyTransform(input);
expect(result).toContain(`from "@modelcontextprotocol/express"`);
expect(result).toContain('createMcpExpressApp');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('rewrites deep middleware/hostHeaderValidation.js import to @modelcontextprotocol/express', () => {
const input = `import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';\n`;
const result = applyTransform(input);
expect(result).toContain(`from "@modelcontextprotocol/express"`);
expect(result).toContain('hostHeaderValidation');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('rewrites client/auth-extensions.js import to @modelcontextprotocol/client', () => {
const input = `import { discoverAuthorizationServerMetadata } from '@modelcontextprotocol/sdk/client/auth-extensions.js';\n`;
const result = applyTransform(input);
expect(result).toContain(`from "@modelcontextprotocol/client"`);
expect(result).toContain('discoverAuthorizationServerMetadata');
expect(result).not.toContain('@modelcontextprotocol/sdk');
});
it('moves deep server/auth/middleware/bearerAuth.js to server-legacy/auth via catch-all', () => {
const input = `import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.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('renames body references when renamedSymbols applies', () => {
const input = [
`import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';`,

View file

@ -240,6 +240,43 @@ describe('mcp-server-api transform', () => {
expect(result).not.toContain('inputSchema: { msg:');
});
it('wraps raw outputSchema in .registerTool() config', () => {
const input = [
`server.registerTool("echo", { outputSchema: { result: z.string() } }, async () => {`,
` return { content: [], structuredContent: { result: 'ok' } };`,
`});`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain('outputSchema: z.object({ result: z.string() })');
expect(result).not.toContain('outputSchema: { result:');
});
it('wraps both raw inputSchema and outputSchema in the same .registerTool() config', () => {
const input = [
`server.registerTool("echo", { inputSchema: { msg: z.string() }, outputSchema: { result: z.string() } }, async ({ msg }) => {`,
` return { content: [], structuredContent: { result: msg } };`,
`});`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain('inputSchema: z.object({ msg: z.string() })');
expect(result).toContain('outputSchema: z.object({ result: z.string() })');
expect(result).not.toContain('z.object(z.object(');
});
it('does not double-wrap z.object() outputSchema in .registerTool() config', () => {
const input = [
`server.registerTool("echo", { outputSchema: z.object({ result: z.string() }) }, async () => {`,
` return { content: [], structuredContent: { result: 'ok' } };`,
`});`,
''
].join('\n');
const result = applyTransform(input);
expect(result).toContain('outputSchema: z.object({ result: z.string() })');
expect(result).not.toContain('z.object(z.object(');
});
it('does not double-wrap z.object() in .registerTool() config', () => {
const input = [
`server.registerTool("echo", { inputSchema: z.object({ msg: z.string() }) }, async ({ msg }) => {`,
@ -307,4 +344,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});
it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});
it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});
it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});
it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});

View file

@ -63,6 +63,19 @@ describe('schema-param-removal transform', () => {
expect(result).toContain('MyCustomSchema');
});
it('does not remove a non-MCP schema from extra.sendRequest() for a custom method', () => {
const input = [
`import { MySchema } from './my-schemas';`,
`const result = await extra.sendRequest({ method: 'acme/x', params }, MySchema);`,
''
].join('\n');
const result = applyTransform(input);
// The schema arg and its import must be left alone — only MCP-imported
// *Schema identifiers are stripped (same guard as the request/callTool path).
expect(result).toContain("extra.sendRequest({ method: 'acme/x', params }, MySchema)");
expect(result).toContain(`import { MySchema } from './my-schemas';`);
});
it('is idempotent', () => {
const input = [
`import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`,

View file

@ -331,7 +331,7 @@ describe('symbol-renames transform', () => {
it('does not rename RequestHandlerExtra from non-MCP imports', () => {
const input = [
`import type { RequestHandlerExtra } from './my-local-types.js';`,
`import type { RequestHandlerExtra } from './my-local-types';`,
`type MyHandler = (extra: RequestHandlerExtra) => void;`,
''
].join('\n');