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:
parent
e4227d135c
commit
1823aae891
7 changed files with 69131 additions and 9001 deletions
|
|
@ -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')
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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'] }");
|
||||
});
|
||||
});
|
||||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
|
|
@ -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';`,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';`,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue