From b44029f9ad75a61c775f7f7225dc7fe193aa6f8d Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Apr 2026 12:21:18 +0000 Subject: [PATCH 1/3] feat(compat): restore McpServer.tool()/.prompt()/.resource() variadic overloads --- .changeset/mcpserver-variadic-compat.md | 5 + packages/server/src/index.ts | 5 +- packages/server/src/server/mcp.ts | 173 ++++++++++++++++++ .../server/test/server/mcp.compat.test.ts | 67 +++++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 .changeset/mcpserver-variadic-compat.md create mode 100644 packages/server/test/server/mcp.compat.test.ts diff --git a/.changeset/mcpserver-variadic-compat.md b/.changeset/mcpserver-variadic-compat.md new file mode 100644 index 000000000..7b43e8ede --- /dev/null +++ b/.changeset/mcpserver-variadic-compat.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Restore `McpServer.tool()`, `.prompt()`, `.resource()` variadic overloads as `@deprecated` v1-compat shims forwarding to `registerTool`/`registerPrompt`/`registerResource`. Emits a one-time deprecation warning; removed in v3. diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6e1bba28d..60114c987 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -12,6 +12,8 @@ export type { AnyToolHandler, BaseToolCallback, CompleteResourceTemplateCallback, + LegacyPromptCallback, + LegacyToolCallback, ListResourcesCallback, PromptCallback, ReadResourceCallback, @@ -21,7 +23,8 @@ export type { RegisteredResourceTemplate, RegisteredTool, ResourceMetadata, - ToolCallback + ToolCallback, + ZodRawShape } from './server/mcp.js'; export { McpServer, ResourceTemplate } from './server/mcp.js'; export type { HostHeaderValidationResult } from './server/middleware/hostHeaderValidation.js'; diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 6c2699997..952fc00c4 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -30,6 +30,7 @@ import type { import { assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, + isStandardSchema, promptArgumentsFromStandardSchema, ProtocolError, ProtocolErrorCode, @@ -38,6 +39,7 @@ import { validateAndWarnToolName, validateStandardSchema } from '@modelcontextprotocol/core'; +import { z } from 'zod'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js'; @@ -950,6 +952,132 @@ export class McpServer { return registeredPrompt; } + // --------------------------------------------------------------------- + // v1-compat variadic registration methods. Frozen at 2025-03-26 surface. + // --------------------------------------------------------------------- + + /** @deprecated Use {@linkcode registerTool}. */ + tool(name: string, cb: LegacyToolCallback): RegisteredTool; + /** @deprecated Use {@linkcode registerTool}. */ + tool(name: string, description: string, cb: LegacyToolCallback): RegisteredTool; + /** @deprecated Use {@linkcode registerTool}. */ + tool( + name: string, + paramsSchemaOrAnnotations: Args | ToolAnnotations, + cb: LegacyToolCallback + ): RegisteredTool; + /** @deprecated Use {@linkcode registerTool}. */ + tool( + name: string, + description: string, + paramsSchemaOrAnnotations: Args | ToolAnnotations, + cb: LegacyToolCallback + ): RegisteredTool; + /** @deprecated Use {@linkcode registerTool}. */ + tool( + name: string, + paramsSchema: Args, + annotations: ToolAnnotations, + cb: LegacyToolCallback + ): RegisteredTool; + /** @deprecated Use {@linkcode registerTool}. */ + tool( + name: string, + description: string, + paramsSchema: Args, + annotations: ToolAnnotations, + cb: LegacyToolCallback + ): RegisteredTool; + tool(name: string, ...rest: unknown[]): RegisteredTool { + let description: string | undefined; + let inputSchema: StandardSchemaWithJSON | undefined; + let annotations: ToolAnnotations | undefined; + + if (typeof rest[0] === 'string') description = rest.shift() as string; + + if (rest.length > 1) { + const first = rest[0]; + if (isZodRawShape(first) || isStandardSchema(first)) { + inputSchema = wrapRawShape(rest.shift()); + if ( + rest.length > 1 && + typeof rest[0] === 'object' && + rest[0] !== null && + !isZodRawShape(rest[0]) && + !isStandardSchema(rest[0]) + ) { + annotations = rest.shift() as ToolAnnotations; + } + } else if (typeof first === 'object' && first !== null) { + annotations = rest.shift() as ToolAnnotations; + } + } + + if (this._registeredTools[name]) { + throw new Error(`Tool ${name} is already registered`); + } + const cb = rest[0] as ToolCallback; + return this._createRegisteredTool( + name, + undefined, + description, + inputSchema, + undefined, + annotations, + { taskSupport: 'forbidden' }, + undefined, + cb + ); + } + + /** @deprecated Use {@linkcode registerPrompt}. */ + prompt(name: string, cb: PromptCallback): RegisteredPrompt; + /** @deprecated Use {@linkcode registerPrompt}. */ + prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; + /** @deprecated Use {@linkcode registerPrompt}. */ + prompt(name: string, argsSchema: Args, cb: LegacyPromptCallback): RegisteredPrompt; + /** @deprecated Use {@linkcode registerPrompt}. */ + prompt(name: string, description: string, argsSchema: Args, cb: LegacyPromptCallback): RegisteredPrompt; + prompt(name: string, ...rest: unknown[]): RegisteredPrompt { + let description: string | undefined; + if (typeof rest[0] === 'string') description = rest.shift() as string; + + let argsSchema: StandardSchemaWithJSON | undefined; + if (rest.length > 1) argsSchema = wrapRawShape(rest.shift()); + + if (this._registeredPrompts[name]) { + throw new Error(`Prompt ${name} is already registered`); + } + const cb = rest[0] as PromptCallback; + const registered = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb, undefined); + this.setPromptRequestHandlers(); + this.sendPromptListChanged(); + return registered; + } + + /** @deprecated Use {@linkcode registerResource}. */ + resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; + /** @deprecated Use {@linkcode registerResource}. */ + resource(name: string, uri: string, metadata: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; + /** @deprecated Use {@linkcode registerResource}. */ + resource(name: string, template: ResourceTemplate, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate; + /** @deprecated Use {@linkcode registerResource}. */ + resource( + name: string, + template: ResourceTemplate, + metadata: ResourceMetadata, + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate; + resource(name: string, uriOrTemplate: string | ResourceTemplate, ...rest: unknown[]): RegisteredResource | RegisteredResourceTemplate { + let metadata: ResourceMetadata = {}; + if (typeof rest[0] === 'object') metadata = rest.shift() as ResourceMetadata; + const readCallback = rest[0] as ReadResourceCallback & ReadResourceTemplateCallback; + if (typeof uriOrTemplate === 'string') { + return this.registerResource(name, uriOrTemplate, metadata, readCallback); + } + return this.registerResource(name, uriOrTemplate, metadata, readCallback); + } + /** * Checks if the server is connected to a transport. * @returns `true` if the server is connected @@ -1062,6 +1190,51 @@ export class ResourceTemplate { } } +/** + * A plain record of Zod field schemas, e.g. `{ name: z.string() }`. Used by the v1 variadic + * `.tool()`/`.prompt()` overloads. For `registerTool`/`registerPrompt`, wrap in `z.object({...})`. + */ +export type ZodRawShape = z.ZodRawShape; + +/** Infers `{ [K]: T }` from a {@linkcode ZodRawShape} `{ [K]: z.ZodType }`. */ +export type InferRawShape = { [K in keyof S]: z.infer }; + +/** Callback shape for the v1 variadic `.tool()` overloads. See also {@linkcode ToolCallback}. */ +export type LegacyToolCallback = Args extends ZodRawShape + ? (args: InferRawShape, ctx: ServerContext) => CallToolResult | Promise + : (ctx: ServerContext) => CallToolResult | Promise; + +/** Callback shape for the v1 variadic `.prompt()` overloads. See also {@linkcode PromptCallback}. */ +export type LegacyPromptCallback = Args extends ZodRawShape + ? (args: InferRawShape, ctx: ServerContext) => GetPromptResult | Promise + : (ctx: ServerContext) => GetPromptResult | Promise; + +/** + * Detects a v1 "raw shape" — a plain object whose values are Standard Schema + * field schemas, e.g. `{ name: z.string() }`. Used by the deprecated variadic + * `.tool()`/`.prompt()` shims to disambiguate the schema arg from annotations. + * + * @internal + */ +function isZodRawShape(obj: unknown): obj is ZodRawShape { + if (typeof obj !== 'object' || obj === null) return false; + if (isStandardSchema(obj)) return false; + const values = Object.values(obj); + return values.length > 0 && values.every(v => isStandardSchema(v)); +} + +/** + * Wraps a v1 raw shape in `z.object()` for the variadic shims; passes Standard + * Schemas through unchanged. + * + * @internal + */ +function wrapRawShape(schema: unknown): StandardSchemaWithJSON | undefined { + if (schema === undefined) return undefined; + if (isZodRawShape(schema)) return z.object(schema); + return schema as StandardSchemaWithJSON; +} + export type BaseToolCallback< SendResultT extends Result, Ctx extends ServerContext, diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts new file mode 100644 index 000000000..8347dd90f --- /dev/null +++ b/packages/server/test/server/mcp.compat.test.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-deprecated */ +import { z } from 'zod'; +import { McpServer, ResourceTemplate } from '../../src/server/mcp.js'; + +describe('McpServer v1-compat variadic shims', () => { + describe('.tool()', () => { + it('registers with raw-shape schema', () => { + const server = new McpServer({ name: 't', version: '1' }); + + server.tool('x', { a: z.string() }, ({ a }) => ({ content: [{ type: 'text', text: a }] })); + server.tool('y', { b: z.number() }, ({ b }) => ({ content: [{ type: 'text', text: String(b) }] })); + + // @ts-expect-error private access for test + expect(server._registeredTools['x']).toBeDefined(); + // @ts-expect-error private access for test + expect(server._registeredTools['y']).toBeDefined(); + }); + + it('supports (name, description, paramsSchema, annotations, cb) overload', () => { + const server = new McpServer({ name: 't', version: '1' }); + + const reg = server.tool('x', 'desc', { a: z.string() }, { readOnlyHint: true }, ({ a }) => ({ + content: [{ type: 'text', text: a }] + })); + + expect(reg.description).toBe('desc'); + expect(reg.annotations).toEqual({ readOnlyHint: true }); + expect(reg.inputSchema).toBeDefined(); + }); + + it('supports (name, cb) zero-arg overload', () => { + const server = new McpServer({ name: 't', version: '1' }); + const reg = server.tool('x', () => ({ content: [{ type: 'text', text: 'ok' }] })); + expect(reg.inputSchema).toBeUndefined(); + }); + }); + + describe('.prompt()', () => { + it('registers with raw-shape argsSchema', () => { + const server = new McpServer({ name: 't', version: '1' }); + + server.prompt('p1', { topic: z.string() }, ({ topic }) => ({ + messages: [{ role: 'user', content: { type: 'text', text: topic } }] + })); + server.prompt('p2', () => ({ messages: [] })); + + // @ts-expect-error private access for test + expect(server._registeredPrompts['p1']).toBeDefined(); + // @ts-expect-error private access for test + expect(server._registeredPrompts['p2']).toBeDefined(); + }); + }); + + describe('.resource()', () => { + it('forwards to registerResource for both string URIs and ResourceTemplates', () => { + const server = new McpServer({ name: 't', version: '1' }); + + server.resource('r1', 'file:///a', () => ({ contents: [] })); + server.resource('r2', new ResourceTemplate('file:///{id}', { list: undefined }), () => ({ contents: [] })); + + // @ts-expect-error private access for test + expect(server._registeredResources['file:///a']).toBeDefined(); + // @ts-expect-error private access for test + expect(server._registeredResourceTemplates['r2']).toBeDefined(); + }); + }); +}); From 0dafed31074b6ce7943e13dc4c9ff76b0341736e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 20:46:45 +0000 Subject: [PATCH 2/3] fix: treat empty object as raw shape (matches v1); drop stale changeset claims --- .changeset/mcpserver-variadic-compat.md | 2 +- packages/server/src/server/mcp.ts | 4 ++-- packages/server/test/server/mcp.compat.test.ts | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.changeset/mcpserver-variadic-compat.md b/.changeset/mcpserver-variadic-compat.md index 7b43e8ede..f3c38d174 100644 --- a/.changeset/mcpserver-variadic-compat.md +++ b/.changeset/mcpserver-variadic-compat.md @@ -2,4 +2,4 @@ '@modelcontextprotocol/server': patch --- -Restore `McpServer.tool()`, `.prompt()`, `.resource()` variadic overloads as `@deprecated` v1-compat shims forwarding to `registerTool`/`registerPrompt`/`registerResource`. Emits a one-time deprecation warning; removed in v3. +Restore `McpServer.tool()`, `.prompt()`, `.resource()` variadic overloads as `@deprecated` v1-compat shims forwarding to `registerTool`/`registerPrompt`/`registerResource`. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 952fc00c4..9c2ebf5c5 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1219,8 +1219,8 @@ export type LegacyPromptCallback = Args ex function isZodRawShape(obj: unknown): obj is ZodRawShape { if (typeof obj !== 'object' || obj === null) return false; if (isStandardSchema(obj)) return false; - const values = Object.values(obj); - return values.length > 0 && values.every(v => isStandardSchema(v)); + // [].every() is true, so an empty object is a valid raw shape (matches v1). + return Object.values(obj).every(v => isStandardSchema(v)); } /** diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 8347dd90f..af5591c78 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -33,6 +33,13 @@ describe('McpServer v1-compat variadic shims', () => { const reg = server.tool('x', () => ({ content: [{ type: 'text', text: 'ok' }] })); expect(reg.inputSchema).toBeUndefined(); }); + + it('treats empty object as raw shape, not annotations (matches v1)', () => { + const server = new McpServer({ name: 't', version: '1' }); + const reg = server.tool('x', {}, () => ({ content: [{ type: 'text', text: 'ok' }] })); + expect(reg.inputSchema).toBeDefined(); + expect(reg.annotations).toBeUndefined(); + }); }); describe('.prompt()', () => { From 1e2946bf4c1b1e8ecf0cf2f6368ae7c430b8fd59 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 21:28:00 +0000 Subject: [PATCH 3/3] fix: zod/v4 import convention; re-export InferRawShape from server index --- packages/server/src/index.ts | 1 + packages/server/src/server/mcp.ts | 2 +- packages/server/test/server/mcp.compat.test.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 60114c987..8583d1cf8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -12,6 +12,7 @@ export type { AnyToolHandler, BaseToolCallback, CompleteResourceTemplateCallback, + InferRawShape, LegacyPromptCallback, LegacyToolCallback, ListResourcesCallback, diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 9c2ebf5c5..7c2f04d21 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -39,7 +39,7 @@ import { validateAndWarnToolName, validateStandardSchema } from '@modelcontextprotocol/core'; -import { z } from 'zod'; +import * as z from 'zod/v4'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js'; diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index af5591c78..7b1ab99cf 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-deprecated */ -import { z } from 'zod'; +import * as z from 'zod/v4'; import { McpServer, ResourceTemplate } from '../../src/server/mcp.js'; describe('McpServer v1-compat variadic shims', () => {