diff --git a/.changeset/mcpserver-variadic-compat.md b/.changeset/mcpserver-variadic-compat.md new file mode 100644 index 000000000..f3c38d174 --- /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`. diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6e1bba28d..8583d1cf8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -12,6 +12,9 @@ export type { AnyToolHandler, BaseToolCallback, CompleteResourceTemplateCallback, + InferRawShape, + LegacyPromptCallback, + LegacyToolCallback, ListResourcesCallback, PromptCallback, ReadResourceCallback, @@ -21,7 +24,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..7c2f04d21 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 * as z from 'zod/v4'; 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; + // [].every() is true, so an empty object is a valid raw shape (matches v1). + return Object.values(obj).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..7b1ab99cf --- /dev/null +++ b/packages/server/test/server/mcp.compat.test.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-deprecated */ +import * as z from 'zod/v4'; +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(); + }); + + 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()', () => { + 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(); + }); + }); +});