From 693d091889efa9f3f197bd9acc36c70be8b4847a Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 24 Jun 2026 15:47:48 +0000 Subject: [PATCH] feat(codemod): close v1-to-v2 mechanical gaps; drop dead express-middleware transform - registerTool: also wrap raw outputSchema with z.object() (already did inputSchema/argsSchema/uriSchema) - importMap: + sdk/server/express.js, + sdk/server/middleware/hostHeaderValidation.js, + sdk/client/auth-extensions.js - delete expressMiddlewareTransform: it rewrote hostHeaderValidation({allowedHosts:[...]}) -> hostHeaderValidation([...]), but hostHeaderValidation was (string[]) in every released v1.x. The allowedHosts *option* on createMcpExpressApp / SSEServerTransport is unchanged v1->v2 (only the import path moved); the transform targeted the wrong API. - docs/migration/upgrade-to-v2.md: HANDLED/NOT-HANDLED lists updated to match --- .changeset/codemod-v1-to-v2-gaps.md | 5 + docs/client.md | 3 +- docs/migration/upgrade-to-v2.md | 28 ++-- packages/codemod/README.md | 13 +- .../migrations/v1-to-v2/mappings/importMap.ts | 20 +++ .../v1-to-v2/transforms/expressMiddleware.ts | 61 --------- .../migrations/v1-to-v2/transforms/index.ts | 6 +- .../v1-to-v2/transforms/mcpServerApi.ts | 3 + packages/codemod/test/integration.test.ts | 10 +- .../transforms/expressMiddleware.test.ts | 120 ------------------ .../v1-to-v2/transforms/importPaths.test.ts | 32 +++++ .../v1-to-v2/transforms/mcpServerApi.test.ts | 37 ++++++ .../transforms/schemaParamRemoval.test.ts | 13 ++ 13 files changed, 137 insertions(+), 214 deletions(-) create mode 100644 .changeset/codemod-v1-to-v2-gaps.md delete mode 100644 packages/codemod/src/migrations/v1-to-v2/transforms/expressMiddleware.ts delete mode 100644 packages/codemod/test/v1-to-v2/transforms/expressMiddleware.test.ts diff --git a/.changeset/codemod-v1-to-v2-gaps.md b/.changeset/codemod-v1-to-v2-gaps.md new file mode 100644 index 0000000000..d712b55cf2 --- /dev/null +++ b/.changeset/codemod-v1-to-v2-gaps.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': patch +--- + +v1-to-v2: now wraps `outputSchema` raw shapes with `z.object()`; importMap covers `sdk/server/express.js`, `sdk/server/middleware/hostHeaderValidation.js`, and `sdk/client/auth-extensions.js`. The unreachable `expressMiddleware` transform is removed. diff --git a/docs/client.md b/docs/client.md index 09078cb52f..a19dd415c0 100644 --- a/docs/client.md +++ b/docs/client.md @@ -118,8 +118,7 @@ client.getNegotiatedProtocolVersion(); // '2026-07-28' or '2025-11-25' - **`mode: { pin: '2026-07-28' }`** — modern era at exactly that revision; no fallback. Against a 2025-only server `connect()` rejects with a typed error. Use `pin` where a silent downgrade would be worse than an error (tests, CI, servers you control). Once a modern era is negotiated, the client automatically attaches the per-request `_meta` envelope (the reserved protocol-version / client-info / client-capabilities keys) to every outgoing request and notification. You can also configure negotiation pre-connect on an -already-constructed instance via {@linkcode @modelcontextprotocol/client!client/client.Client#setVersionNegotiation | client.setVersionNegotiation()}. See the [2026-07-28 support guide](./migration/support-2026-07-28.md#serving-the-2026-07-28-revision) for the full failure semantics, -probe policy, and the `'auto'`-mode compatibility table. +already-constructed instance via {@linkcode @modelcontextprotocol/client!client/client.Client#setVersionNegotiation | client.setVersionNegotiation()}. See the [2026-07-28 support guide › Probe policy](./migration/support-2026-07-28.md#probe-policy) for the full failure semantics and probe-timeout behavior. ### Disconnecting diff --git a/docs/migration/upgrade-to-v2.md b/docs/migration/upgrade-to-v2.md index 5d0132c29e..c08c2b8867 100644 --- a/docs/migration/upgrade-to-v2.md +++ b/docs/migration/upgrade-to-v2.md @@ -67,8 +67,8 @@ In addition the codemod: - Updates `package.json` dependencies (`@modelcontextprotocol/sdk` → the v2 packages your imports actually use). - Rewrites `.tool()` / `.prompt()` / `.resource()` to `registerTool` / `registerPrompt` - / `registerResource` and wraps `inputSchema` / `argsSchema` / `uriSchema` raw Zod - shapes with `z.object()`. + / `registerResource` and wraps `inputSchema` / `outputSchema` / `argsSchema` / + `uriSchema` raw Zod shapes with `z.object()`. - Drops the result-schema argument from `client.request()` / `client.callTool()` for spec methods. - Rewrites standalone Zod-spec-schema usages: `XSchema.safeParse(v).success` → @@ -104,9 +104,6 @@ recognized but could not safely rewrite with an `@mcp-codemod-error` comment. - **`ctx.mcpReq.send()` schema-arg drop** — the codemod drops the schema arg from `client.request()` / `client.callTool()` but leaves nested `ctx.mcpReq.send()` calls alone. → [Low-level protocol](#low-level-protocol--handler-context-ctx) -- **`outputSchema` `z.object()` wrap** — `inputSchema`, `argsSchema`, and `uriSchema` - raw shapes are wrapped; `outputSchema` is left for review (it was rarely a raw shape - in v1). → [Server registration API](#server-registration-api) - **OAuth error-class consolidation** — `instanceof InvalidGrantError` → `OAuthError` + `OAuthErrorCode` is a judgment rewrite. → [Auth](#auth) - **`SdkErrorCode` branch selection** — the codemod renames `StreamableHTTPError` → @@ -207,13 +204,13 @@ A few transports need a decision the codemod can't make: is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`. The codemod's [`importMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts) - does **not** cover the per-file deep auth/middleware paths - (`…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`, - `…/server/auth/handlers/{authorize,metadata,register,revoke,token}.js`, - `…/server/auth/providers/proxyProvider.js`, - `…/server/middleware/hostHeaderValidation.js`, `…/server/express.js`) — rewrite those - imports by hand to the RS/AS targets above (`createMcpExpressApp` / - `CreateMcpExpressAppOptions` from `…/server/express.js` → `@modelcontextprotocol/express`). + routes every `…/server/auth/**` deep path (including + `…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`, + `…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to + `@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` / + `…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The + AS→`server-legacy` routing is conservative — re-point RS-only call sites + (`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand. ### Low-level protocol & handler context (`ctx`) @@ -328,9 +325,8 @@ The return type is inferred from the method name via `ResultTypeMap` (e.g. The deprecated variadic `.tool()`, `.prompt()`, `.resource()` are removed. Use `registerTool` / `registerPrompt` / `registerResource` with an explicit config object. -The codemod converts the call shape and wraps `inputSchema` / `argsSchema` / `uriSchema` -raw shapes; verify `outputSchema` (which the codemod does not wrap) is wrapped where -present. +The codemod converts the call shape and wraps `inputSchema` / `outputSchema` / +`argsSchema` / `uriSchema` raw shapes. ```typescript // v1 — raw shape, variadic @@ -806,7 +802,7 @@ surfaced per-tool on `callTool`). A tool may now register an `outputSchema` whose root is `type:"array"`, `type:"string"`, etc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward every era a non-object `structuredContent` with no `text` block of its own gets a -`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md](./support-2026-07-28.md#per-era-wire-codecs) for the per-era projection rules. +`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](./support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era. ### Behavioral changes diff --git a/packages/codemod/README.md b/packages/codemod/README.md index 4c048aabac..d498c07df8 100644 --- a/packages/codemod/README.md +++ b/packages/codemod/README.md @@ -27,8 +27,9 @@ The mechanical rename mappings are the source of truth — see `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` Transforms in `src/migrations/v1-to-v2/transforms/` also rewrite `.tool()` → -`registerTool` (with `z.object()` wrap), drop the result-schema argument from -`client.request()` / `client.callTool()` for spec methods, rewrite spec-`*Schema` +`registerTool` (wrapping `inputSchema` / `outputSchema` / `argsSchema` / `uriSchema` +raw shapes with `z.object()`), drop the result-schema argument from `client.request()` +/ `client.callTool()` for spec methods, rewrite spec-`*Schema` constant accesses (`.safeParse` → `isSpecType` / `specTypeSchemas`), rename `StreamableHTTPError` → `SdkHttpError` / `IsomorphicHeaders` → `Headers`, rewrite `SchemaInput` → `StandardSchemaWithJSON.InferInput`, route @@ -55,10 +56,10 @@ grep -rn '@mcp-codemod-error' . CJS→ESM / Node 20 pre-flight, `new Headers()` / `.get()` access rewrites, OAuth error-class consolidation (`instanceof InvalidGrantError` → `OAuthError` + -`OAuthErrorCode`), per-scenario `SdkErrorCode` branch selection, `outputSchema` -`z.object()` wrap, `ctx.mcpReq.send()` schema-arg drop, and behavioral adaptation are -manual — see the [migration guide](../../docs/migration/upgrade-to-v2.md) for what to -do after the codemod runs. +`OAuthErrorCode`), per-scenario `SdkErrorCode` branch selection, `ctx.mcpReq.send()` +schema-arg drop, and behavioral adaptation are manual — see the +[migration guide](../../docs/migration/upgrade-to-v2.md) for what to do after the +codemod runs. The codemod handles the v1→v2 SDK surface upgrade only. Adopting the 2026-07-28 protocol revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`) diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index 24d086f8de..ffc4a2268f 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -25,6 +25,10 @@ export const IMPORT_MAP: Record = { target: '@modelcontextprotocol/client', status: 'moved' }, + '@modelcontextprotocol/sdk/client/auth-extensions.js': { + target: '@modelcontextprotocol/client', + status: 'moved' + }, '@modelcontextprotocol/sdk/client/streamableHttp.js': { target: '@modelcontextprotocol/client', status: 'moved' @@ -83,6 +87,14 @@ export const IMPORT_MAP: Record = { target: '@modelcontextprotocol/express', status: 'moved' }, + '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js': { + target: '@modelcontextprotocol/express', + status: 'moved' + }, + '@modelcontextprotocol/sdk/server/express.js': { + target: '@modelcontextprotocol/express', + status: 'moved' + }, '@modelcontextprotocol/sdk/server/zod-compat.js': { target: '', status: 'removed', @@ -131,6 +143,14 @@ export const IMPORT_MAP: Record = { target: 'RESOLVE_BY_CONTEXT', status: 'moved' }, + '@modelcontextprotocol/sdk/shared/auth-utils.js': { + target: 'RESOLVE_BY_CONTEXT', + status: 'moved' + }, + '@modelcontextprotocol/sdk/client/middleware.js': { + target: '@modelcontextprotocol/client', + status: 'moved' + }, '@modelcontextprotocol/sdk/shared/uriTemplate.js': { target: 'RESOLVE_BY_CONTEXT', status: 'moved' diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/expressMiddleware.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/expressMiddleware.ts deleted file mode 100644 index 319461070a..0000000000 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/expressMiddleware.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { SourceFile } from 'ts-morph'; -import { Node, SyntaxKind } from 'ts-morph'; - -import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js'; -import { info } from '../../../utils/diagnostics.js'; -import { isOriginalNameImportedFromMcp, resolveLocalImportName } from '../../../utils/importUtils.js'; - -export const expressMiddlewareTransform: Transform = { - name: 'Express middleware signature migration', - id: 'express-middleware', - apply(sourceFile: SourceFile, _context: TransformContext): TransformResult { - if (!isOriginalNameImportedFromMcp(sourceFile, 'hostHeaderValidation')) { - return { changesCount: 0, diagnostics: [] }; - } - - const diagnostics: Diagnostic[] = []; - let changesCount = 0; - - const localName = resolveLocalImportName(sourceFile, 'hostHeaderValidation') ?? 'hostHeaderValidation'; - changesCount += rewriteHostHeaderValidation(sourceFile, localName, diagnostics); - - return { changesCount, diagnostics }; - } -}; - -function rewriteHostHeaderValidation(sourceFile: SourceFile, targetName: string, diagnostics: Diagnostic[]): number { - let changesCount = 0; - - const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression); - - for (const call of calls) { - const expr = call.getExpression(); - if (!Node.isIdentifier(expr) || expr.getText() !== targetName) continue; - - const args = call.getArguments(); - if (args.length !== 1) continue; - - const firstArg = args[0]!; - if (!Node.isObjectLiteralExpression(firstArg)) continue; - - const allowedHostsProp = firstArg.getProperty('allowedHosts'); - if (!allowedHostsProp || !Node.isPropertyAssignment(allowedHostsProp)) continue; - - const initializer = allowedHostsProp.getInitializer(); - if (!initializer) continue; - - const arrayText = initializer.getText(); - firstArg.replaceWithText(arrayText); - changesCount++; - - diagnostics.push( - info( - sourceFile.getFilePath(), - call.getStartLineNumber(), - 'hostHeaderValidation({ allowedHosts: [...] }) simplified to hostHeaderValidation([...]). Verify the migration.' - ) - ); - } - - return changesCount; -} diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts index 7b6b54b28b..fe00099514 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts @@ -1,6 +1,5 @@ import type { Transform } from '../../../types.js'; import { contextTypesTransform } from './contextTypes.js'; -import { expressMiddlewareTransform } from './expressMiddleware.js'; import { handlerRegistrationTransform } from './handlerRegistration.js'; import { importPathsTransform } from './importPaths.js'; import { mcpServerApiTransform } from './mcpServerApi.js'; @@ -28,8 +27,8 @@ import { symbolRenamesTransform } from './symbolRenames.js'; // to .registerTool() etc. contextTypes handles both old and new names, // but running mcpServerApi first ensures consistent argument structure. // -// 5. handlerRegistration, schemaParamRemoval, and expressMiddleware are -// independent of each other but all depend on importPaths having run. +// 5. handlerRegistration and schemaParamRemoval are independent of each +// other but both depend on importPaths having run. // // 6. specSchemaAccess runs after handlerRegistration and schemaParamRemoval: // those transforms remove spec schema references they handle. specSchemaAccess @@ -45,7 +44,6 @@ export const v1ToV2Transforms: Transform[] = [ handlerRegistrationTransform, schemaParamRemovalTransform, specSchemaAccessTransform, - expressMiddlewareTransform, contextTypesTransform, mockPathsTransform ]; diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts index 706c1e6e66..631c995884 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts @@ -107,6 +107,9 @@ export const mcpServerApiTransform: Transform = { if (wrapSchemaInConfig(call, 'inputSchema', sourceFile, diagnostics)) { changesCount++; } + if (wrapSchemaInConfig(call, 'outputSchema', sourceFile, diagnostics)) { + changesCount++; + } } for (const call of registerPromptCalls) { diff --git a/packages/codemod/test/integration.test.ts b/packages/codemod/test/integration.test.ts index eeb9de2e17..18942bfcfa 100644 --- a/packages/codemod/test/integration.test.ts +++ b/packages/codemod/test/integration.test.ts @@ -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;`, `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); @@ -470,7 +470,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') ); diff --git a/packages/codemod/test/v1-to-v2/transforms/expressMiddleware.test.ts b/packages/codemod/test/v1-to-v2/transforms/expressMiddleware.test.ts deleted file mode 100644 index 35182e47b2..0000000000 --- a/packages/codemod/test/v1-to-v2/transforms/expressMiddleware.test.ts +++ /dev/null @@ -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.js'; -import type { TransformContext } from '../../../src/types.js'; - -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'] }"); - }); -}); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index f0563f9f69..74fd5f3cb7 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -128,6 +128,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';`, diff --git a/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts b/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts index 461cfb5da0..e775a0c5ff 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts @@ -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 }) => {`, diff --git a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts index f1a2413982..d2d6c86482 100644 --- a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts @@ -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';`,