Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcpserver-variadic-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

Restore `McpServer.tool()`, `.prompt()`, `.resource()` variadic overloads as `@deprecated` v1-compat shims forwarding to `registerTool`/`registerPrompt`/`registerResource`.
5 changes: 4 additions & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
AnyToolHandler,
BaseToolCallback,
CompleteResourceTemplateCallback,
LegacyPromptCallback,
LegacyToolCallback,

Check warning on line 16 in packages/server/src/index.ts

View check run for this annotation

Claude / Claude Code Review

InferRawShape exported from mcp.ts but not re-exported from index.ts

nit: This PR adds 4 new exported types in `mcp.ts` (`ZodRawShape`, `InferRawShape`, `LegacyToolCallback`, `LegacyPromptCallback`) but only re-exports 3 of them here — `InferRawShape` is omitted, leaving it half-exported. Either add `InferRawShape` to this list alongside its siblings (it appears in the public `Legacy*Callback` signatures), or drop the `export` keyword from it in `mcp.ts` since it's only referenced in-file.
Comment thread
claude[bot] marked this conversation as resolved.
ListResourcesCallback,
PromptCallback,
ReadResourceCallback,
Expand All @@ -21,7 +23,8 @@
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';
Expand Down
173 changes: 173 additions & 0 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
isStandardSchema,
promptArgumentsFromStandardSchema,
ProtocolError,
ProtocolErrorCode,
Expand All @@ -38,6 +39,7 @@
validateAndWarnToolName,
validateStandardSchema
} from '@modelcontextprotocol/core';
import { z } from 'zod';

Check warning on line 42 in packages/server/src/server/mcp.ts

View check run for this annotation

Claude / Claude Code Review

Inconsistent zod import path: 'zod' instead of 'zod/v4'

nit: This uses `import { z } from 'zod'` (also in `mcp.compat.test.ts:2`), but every other zod import in the repo uses `import * as z from 'zod/v4'` and CLAUDE.md:112 explicitly states "The SDK uses `zod/v4` internally." No runtime impact since the catalog pins zod ^4, but worth aligning both imports with the established convention.
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js';
Expand Down Expand Up @@ -950,6 +952,132 @@
return registeredPrompt;
}

// ---------------------------------------------------------------------
// v1-compat variadic registration methods. Frozen at 2025-03-26 surface.
// ---------------------------------------------------------------------
Comment thread
felixweinberger marked this conversation as resolved.

/** @deprecated Use {@linkcode registerTool}. */
tool(name: string, cb: LegacyToolCallback<undefined>): RegisteredTool;
/** @deprecated Use {@linkcode registerTool}. */
tool(name: string, description: string, cb: LegacyToolCallback<undefined>): RegisteredTool;
/** @deprecated Use {@linkcode registerTool}. */
tool<Args extends ZodRawShape>(
name: string,
paramsSchemaOrAnnotations: Args | ToolAnnotations,
cb: LegacyToolCallback<Args>
): RegisteredTool;
/** @deprecated Use {@linkcode registerTool}. */
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchemaOrAnnotations: Args | ToolAnnotations,
cb: LegacyToolCallback<Args>
): RegisteredTool;
/** @deprecated Use {@linkcode registerTool}. */
tool<Args extends ZodRawShape>(
name: string,
paramsSchema: Args,
annotations: ToolAnnotations,
cb: LegacyToolCallback<Args>
): RegisteredTool;
/** @deprecated Use {@linkcode registerTool}. */
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
annotations: ToolAnnotations,
cb: LegacyToolCallback<Args>
): 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<StandardSchemaWithJSON | undefined>;
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<Args extends ZodRawShape>(name: string, argsSchema: Args, cb: LegacyPromptCallback<Args>): RegisteredPrompt;
/** @deprecated Use {@linkcode registerPrompt}. */
prompt<Args extends ZodRawShape>(name: string, description: string, argsSchema: Args, cb: LegacyPromptCallback<Args>): 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<StandardSchemaWithJSON | undefined>;
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
Expand Down Expand Up @@ -1062,6 +1190,51 @@
}
}

/**
* 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<T> }`. */
export type InferRawShape<S extends ZodRawShape> = { [K in keyof S]: z.infer<S[K]> };

/** Callback shape for the v1 variadic `.tool()` overloads. See also {@linkcode ToolCallback}. */
export type LegacyToolCallback<Args extends ZodRawShape | undefined> = Args extends ZodRawShape
? (args: InferRawShape<Args>, ctx: ServerContext) => CallToolResult | Promise<CallToolResult>
: (ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;

/** Callback shape for the v1 variadic `.prompt()` overloads. See also {@linkcode PromptCallback}. */
export type LegacyPromptCallback<Args extends ZodRawShape | undefined> = Args extends ZodRawShape
? (args: InferRawShape<Args>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>
: (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;

/**
* 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));
}
Comment thread
claude[bot] marked this conversation as resolved.

/**
* 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,
Expand Down
74 changes: 74 additions & 0 deletions packages/server/test/server/mcp.compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* 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();
});

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();
});
});
});
Loading