From 27e4ddfeb31cd7d1f192d972ef2b43f51459bbc1 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Apr 2026 11:59:36 +0000 Subject: [PATCH 1/6] feat(compat): registerTool/registerPrompt accept raw Zod shape (auto-wrap with z.object) --- .changeset/register-rawshape-compat.md | 6 ++ packages/core/src/util/standardSchema.ts | 34 +++++++++ packages/server/src/server/mcp.ts | 72 ++++++++++++++++++- .../server/test/server/mcp.compat.test.ts | 54 ++++++++++++++ 4 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 .changeset/register-rawshape-compat.md create mode 100644 packages/server/test/server/mcp.compat.test.ts diff --git a/.changeset/register-rawshape-compat.md b/.changeset/register-rawshape-compat.md new file mode 100644 index 000000000..5a2a064e2 --- /dev/null +++ b/.changeset/register-rawshape-compat.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/core': patch +'@modelcontextprotocol/server': patch +--- + +`registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with `z.object()`. Both forms are first-class. diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index 9817dc39a..c136ba649 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -6,6 +6,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import * as z from 'zod/v4'; + // Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025) export interface StandardTypedV1 { @@ -136,6 +138,38 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch return isStandardJSONSchema(schema) && isStandardSchema(schema); } +/** + * Detects a "raw shape" — a plain object whose values are Zod (or other + * Standard Schema) field schemas, e.g. `{ name: z.string() }`. Powers the + * auto-wrap in {@linkcode normalizeRawShapeSchema}. + * + * @internal + */ +export function isZodRawShape(obj: unknown): obj is Record { + 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) || (typeof v === 'object' && v !== null && '_def' in v)); +} + +/** + * Accepts either a {@linkcode StandardSchemaWithJSON} or a raw Zod shape + * `{ field: z.string() }` and returns a {@linkcode StandardSchemaWithJSON}. + * Raw shapes are wrapped with `z.object()` so the rest of the pipeline sees a + * uniform schema type; already-wrapped schemas pass through unchanged. + * + * @internal + */ +export function normalizeRawShapeSchema( + schema: StandardSchemaWithJSON | Record | undefined +): StandardSchemaWithJSON | undefined { + if (schema === undefined) return undefined; + if (isZodRawShape(schema)) { + return z.object(schema as z.ZodRawShape) as StandardSchemaWithJSON; + } + return schema; +} + // JSON Schema conversion /** diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 6c2699997..886fee692 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -30,6 +30,7 @@ import type { import { assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, + normalizeRawShapeSchema, promptArgumentsFromStandardSchema, ProtocolError, ProtocolErrorCode, @@ -873,6 +874,31 @@ export class McpServer { _meta?: Record; }, cb: ToolCallback + ): RegisteredTool; + /** @deprecated Wrap with `z.object({...})` instead. Raw-shape form: `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record; it is auto-wrapped with `z.object()`. */ + registerTool( + name: string, + config: { + title?: string; + description?: string; + inputSchema?: InputArgs; + outputSchema?: OutputArgs; + annotations?: ToolAnnotations; + _meta?: Record; + }, + cb: LegacyToolCallback + ): RegisteredTool; + registerTool( + name: string, + config: { + title?: string; + description?: string; + inputSchema?: StandardSchemaWithJSON | ZodRawShape; + outputSchema?: StandardSchemaWithJSON | ZodRawShape; + annotations?: ToolAnnotations; + _meta?: Record; + }, + cb: ToolCallback | LegacyToolCallback ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); @@ -884,8 +910,8 @@ export class McpServer { name, title, description, - inputSchema, - outputSchema, + normalizeRawShapeSchema(inputSchema), + normalizeRawShapeSchema(outputSchema), annotations, { taskSupport: 'forbidden' }, _meta, @@ -928,6 +954,27 @@ export class McpServer { _meta?: Record; }, cb: PromptCallback + ): RegisteredPrompt; + /** @deprecated Wrap with `z.object({...})` instead. Raw-shape form: `argsSchema` may be a plain `{ field: z.string() }` record; it is auto-wrapped with `z.object()`. */ + registerPrompt( + name: string, + config: { + title?: string; + description?: string; + argsSchema?: Args; + _meta?: Record; + }, + cb: LegacyPromptCallback + ): RegisteredPrompt; + registerPrompt( + name: string, + config: { + title?: string; + description?: string; + argsSchema?: StandardSchemaWithJSON | ZodRawShape; + _meta?: Record; + }, + cb: PromptCallback | LegacyPromptCallback ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); @@ -939,7 +986,7 @@ export class McpServer { name, title, description, - argsSchema, + normalizeRawShapeSchema(argsSchema), cb as PromptCallback, _meta ); @@ -1062,6 +1109,25 @@ export class ResourceTemplate { } } +/** + * A plain record of field schemas, e.g. `{ name: z.string() }`. Accepted by + * `registerTool`/`registerPrompt` as a shorthand; auto-wrapped with `z.object()`. + */ +export type ZodRawShape = Record; + +/** Infers the parsed-output type of a {@linkcode ZodRawShape}. */ +export type InferRawShape = { [K in keyof S]: StandardSchemaWithJSON.InferOutput }; + +/** {@linkcode ToolCallback} variant used when `inputSchema` is a {@linkcode ZodRawShape}. */ +export type LegacyToolCallback = Args extends ZodRawShape + ? (args: InferRawShape, ctx: ServerContext) => CallToolResult | Promise + : (ctx: ServerContext) => CallToolResult | Promise; + +/** {@linkcode PromptCallback} variant used when `argsSchema` is a {@linkcode ZodRawShape}. */ +export type LegacyPromptCallback = Args extends ZodRawShape + ? (args: InferRawShape, ctx: ServerContext) => GetPromptResult | Promise + : (ctx: ServerContext) => GetPromptResult | Promise; + 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..63e64e34e --- /dev/null +++ b/packages/server/test/server/mcp.compat.test.ts @@ -0,0 +1,54 @@ +import { isStandardSchema } from '@modelcontextprotocol/core'; +import { describe, expect, it, vi } from 'vitest'; +import * as z from 'zod/v4'; +import { McpServer } from '../../src/index.js'; + +describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () => { + it('registerTool accepts a raw shape for inputSchema, auto-wraps, and does not warn', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const server = new McpServer({ name: 't', version: '1.0.0' }); + + server.registerTool('a', { inputSchema: { x: z.number() } }, async ({ x }) => ({ + content: [{ type: 'text' as const, text: String(x) }] + })); + server.registerTool('b', { inputSchema: { y: z.number() } }, async ({ y }) => ({ + content: [{ type: 'text' as const, text: String(y) }] + })); + + const tools = (server as unknown as { _registeredTools: Record })._registeredTools; + expect(Object.keys(tools)).toEqual(['a', 'b']); + // raw shape was wrapped into a Standard Schema (z.object) + expect(isStandardSchema(tools['a']?.inputSchema)).toBe(true); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('registerTool with z.object() inputSchema also works without warning', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const server = new McpServer({ name: 't', version: '1.0.0' }); + + server.registerTool('c', { inputSchema: z.object({ x: z.number() }) }, async ({ x }) => ({ + content: [{ type: 'text' as const, text: String(x) }] + })); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('registerPrompt accepts a raw shape for argsSchema and does not warn', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const server = new McpServer({ name: 't', version: '1.0.0' }); + + server.registerPrompt('p', { argsSchema: { topic: z.string() } }, async ({ topic }) => ({ + messages: [{ role: 'user' as const, content: { type: 'text' as const, text: topic } }] + })); + + const prompts = (server as unknown as { _registeredPrompts: Record })._registeredPrompts; + expect(Object.keys(prompts)).toContain('p'); + expect(isStandardSchema(prompts['p']?.argsSchema)).toBe(true); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); +}); From 526613112f7608a4859758df58169b2fdc43bfe1 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 20:46:47 +0000 Subject: [PATCH 2/6] fix: isZodRawShape treats empty object as raw shape (matches v1) --- packages/core/src/util/standardSchema.ts | 4 ++-- .../core/test/util/standardSchema.test.ts | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index c136ba649..45d0165fd 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -148,8 +148,8 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch export function isZodRawShape(obj: unknown): obj is Record { 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) || (typeof v === 'object' && v !== null && '_def' in v)); + // [].every() is true, so an empty object is a valid raw shape (matches v1). + return Object.values(obj).every(v => isStandardSchema(v) || (typeof v === 'object' && v !== null && '_def' in v)); } /** diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts index 6c3de99d7..63a584aab 100644 --- a/packages/core/test/util/standardSchema.test.ts +++ b/packages/core/test/util/standardSchema.test.ts @@ -1,6 +1,26 @@ import * as z from 'zod/v4'; -import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js'; +import { isZodRawShape, normalizeRawShapeSchema, standardSchemaToJsonSchema } from '../../src/util/standardSchema.js'; + +describe('isZodRawShape', () => { + test('treats empty object as a raw shape (matches v1)', () => { + expect(isZodRawShape({})).toBe(true); + }); + test('detects raw shape with zod fields', () => { + expect(isZodRawShape({ a: z.string() })).toBe(true); + }); + test('rejects a Standard Schema instance', () => { + expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false); + }); +}); + +describe('normalizeRawShapeSchema', () => { + test('wraps empty raw shape into z.object({})', () => { + const wrapped = normalizeRawShapeSchema({}); + expect(wrapped).toBeDefined(); + expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object'); + }); +}); describe('standardSchemaToJsonSchema', () => { test('emits type:object for plain z.object schemas', () => { From f2fdbe7381e42871658477b9d7416305126d5925 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 21:28:00 +0000 Subject: [PATCH 3/6] docs: changeset wording aligns with @deprecated overloads (not first-class) --- .changeset/register-rawshape-compat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/register-rawshape-compat.md b/.changeset/register-rawshape-compat.md index 5a2a064e2..bca0d9057 100644 --- a/.changeset/register-rawshape-compat.md +++ b/.changeset/register-rawshape-compat.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': patch --- -`registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with `z.object()`. Both forms are first-class. +`registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with `z.object()`. The raw-shape overloads are `@deprecated`; prefer wrapping with `z.object()`. From 9576f20b2936bbd30646e6cea2748f3dda8ca9a9 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 23:40:14 +0000 Subject: [PATCH 4/6] docs: clarify isZodRawShape only supports Zod values for auto-wrap --- packages/core/src/util/standardSchema.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index 45d0165fd..7bf83140b 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -139,9 +139,10 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch } /** - * Detects a "raw shape" — a plain object whose values are Zod (or other - * Standard Schema) field schemas, e.g. `{ name: z.string() }`. Powers the - * auto-wrap in {@linkcode normalizeRawShapeSchema}. + * Detects a "raw shape" — a plain object whose values are Zod field schemas, + * e.g. `{ name: z.string() }`. Powers the auto-wrap in + * {@linkcode normalizeRawShapeSchema}, which wraps with `z.object()`, so only + * Zod values are supported even though the predicate accepts any Standard Schema. * * @internal */ From 0152b266fd4ab51de1144b77dd7bed9d1bae79f0 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 10:53:52 +0000 Subject: [PATCH 5/6] fix(compat): narrow ZodRawShape to Zod-only (detector + type); add outputSchema raw-shape test --- packages/core/src/util/standardSchema.ts | 16 +++++++++++----- packages/core/test/util/standardSchema.test.ts | 4 ++++ packages/server/src/server/mcp.ts | 8 +++++--- packages/server/test/server/mcp.compat.test.ts | 12 ++++++++++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index 7bf83140b..4228bb25a 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -138,19 +138,25 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch return isStandardJSONSchema(schema) && isStandardSchema(schema); } +function isZodSchema(v: unknown): v is z.ZodType { + if (typeof v !== 'object' || v === null) return false; + if ('_def' in v) return true; + return isStandardSchema(v) && (v as StandardSchemaV1)['~standard'].vendor === 'zod'; +} + /** * Detects a "raw shape" — a plain object whose values are Zod field schemas, * e.g. `{ name: z.string() }`. Powers the auto-wrap in * {@linkcode normalizeRawShapeSchema}, which wraps with `z.object()`, so only - * Zod values are supported even though the predicate accepts any Standard Schema. + * Zod values are supported. * * @internal */ -export function isZodRawShape(obj: unknown): obj is Record { +export function isZodRawShape(obj: unknown): obj is Record { 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) || (typeof v === 'object' && v !== null && '_def' in v)); + return Object.values(obj).every(v => isZodSchema(v)); } /** @@ -162,11 +168,11 @@ export function isZodRawShape(obj: unknown): obj is Record | undefined + schema: StandardSchemaWithJSON | Record | undefined ): StandardSchemaWithJSON | undefined { if (schema === undefined) return undefined; if (isZodRawShape(schema)) { - return z.object(schema as z.ZodRawShape) as StandardSchemaWithJSON; + return z.object(schema) as StandardSchemaWithJSON; } return schema; } diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts index 63a584aab..a207121f0 100644 --- a/packages/core/test/util/standardSchema.test.ts +++ b/packages/core/test/util/standardSchema.test.ts @@ -12,6 +12,10 @@ describe('isZodRawShape', () => { test('rejects a Standard Schema instance', () => { expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false); }); + test('rejects a shape with non-Zod Standard Schema fields', () => { + const nonZod = { '~standard': { version: 1, vendor: 'arktype', validate: () => ({ value: 'x' }) } }; + expect(isZodRawShape({ a: nonZod })).toBe(false); + }); }); describe('normalizeRawShapeSchema', () => { diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 886fee692..4caa125c4 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -40,6 +40,7 @@ import { validateStandardSchema } from '@modelcontextprotocol/core'; +import type * as z from 'zod/v4'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js'; import { getCompleter, isCompletable } from './completable.js'; @@ -1110,13 +1111,14 @@ export class ResourceTemplate { } /** - * A plain record of field schemas, e.g. `{ name: z.string() }`. Accepted by + * A plain record of Zod field schemas, e.g. `{ name: z.string() }`. Accepted by * `registerTool`/`registerPrompt` as a shorthand; auto-wrapped with `z.object()`. + * Zod schemas only — `z.object()` cannot wrap other Standard Schema libraries. */ -export type ZodRawShape = Record; +export type ZodRawShape = Record; /** Infers the parsed-output type of a {@linkcode ZodRawShape}. */ -export type InferRawShape = { [K in keyof S]: StandardSchemaWithJSON.InferOutput }; +export type InferRawShape = { [K in keyof S]: z.output }; /** {@linkcode ToolCallback} variant used when `inputSchema` is a {@linkcode ZodRawShape}. */ export type LegacyToolCallback = Args extends ZodRawShape diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 63e64e34e..2d36978c4 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -24,6 +24,18 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () = warn.mockRestore(); }); + it('registerTool accepts a raw shape for outputSchema and auto-wraps it', () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + + server.registerTool('out', { inputSchema: { n: z.number() }, outputSchema: { result: z.string() } }, async ({ n }) => ({ + content: [{ type: 'text' as const, text: String(n) }], + structuredContent: { result: String(n) } + })); + + const tools = (server as unknown as { _registeredTools: Record })._registeredTools; + expect(isStandardSchema(tools['out']?.outputSchema)).toBe(true); + }); + it('registerTool with z.object() inputSchema also works without warning', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const server = new McpServer({ name: 't', version: '1.0.0' }); From 1af9ed2d9aa0fb6d1c3e9d954d74087589f9188b Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 12:41:43 +0000 Subject: [PATCH 6/6] test(compat): add e2e raw-shape tools/call test; drop vestigial warn-spy; cover normalizeRawShapeSchema passthrough/undefined --- .../core/test/util/standardSchema.test.ts | 7 +++ .../server/test/server/mcp.compat.test.ts | 63 ++++++++++++++----- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts index a207121f0..de081b975 100644 --- a/packages/core/test/util/standardSchema.test.ts +++ b/packages/core/test/util/standardSchema.test.ts @@ -24,6 +24,13 @@ describe('normalizeRawShapeSchema', () => { expect(wrapped).toBeDefined(); expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object'); }); + test('passes through an already-wrapped Standard Schema unchanged', () => { + const schema = z.object({ a: z.string() }); + expect(normalizeRawShapeSchema(schema)).toBe(schema); + }); + test('returns undefined for undefined input', () => { + expect(normalizeRawShapeSchema(undefined)).toBeUndefined(); + }); }); describe('standardSchemaToJsonSchema', () => { diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 2d36978c4..8cfd70b27 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -1,11 +1,11 @@ -import { isStandardSchema } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core'; +import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; import { describe, expect, it, vi } from 'vitest'; import * as z from 'zod/v4'; import { McpServer } from '../../src/index.js'; describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () => { - it('registerTool accepts a raw shape for inputSchema, auto-wraps, and does not warn', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('registerTool accepts a raw shape for inputSchema and auto-wraps it', () => { const server = new McpServer({ name: 't', version: '1.0.0' }); server.registerTool('a', { inputSchema: { x: z.number() } }, async ({ x }) => ({ @@ -19,9 +19,6 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () = expect(Object.keys(tools)).toEqual(['a', 'b']); // raw shape was wrapped into a Standard Schema (z.object) expect(isStandardSchema(tools['a']?.inputSchema)).toBe(true); - - expect(warn).not.toHaveBeenCalled(); - warn.mockRestore(); }); it('registerTool accepts a raw shape for outputSchema and auto-wraps it', () => { @@ -36,20 +33,18 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () = expect(isStandardSchema(tools['out']?.outputSchema)).toBe(true); }); - it('registerTool with z.object() inputSchema also works without warning', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('registerTool with z.object() inputSchema also works (passthrough, no auto-wrap)', () => { const server = new McpServer({ name: 't', version: '1.0.0' }); server.registerTool('c', { inputSchema: z.object({ x: z.number() }) }, async ({ x }) => ({ content: [{ type: 'text' as const, text: String(x) }] })); - expect(warn).not.toHaveBeenCalled(); - warn.mockRestore(); + const tools = (server as unknown as { _registeredTools: Record })._registeredTools; + expect(isStandardSchema(tools['c']?.inputSchema)).toBe(true); }); - it('registerPrompt accepts a raw shape for argsSchema and does not warn', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('registerPrompt accepts a raw shape for argsSchema', () => { const server = new McpServer({ name: 't', version: '1.0.0' }); server.registerPrompt('p', { argsSchema: { topic: z.string() } }, async ({ topic }) => ({ @@ -59,8 +54,48 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () = const prompts = (server as unknown as { _registeredPrompts: Record })._registeredPrompts; expect(Object.keys(prompts)).toContain('p'); expect(isStandardSchema(prompts['p']?.argsSchema)).toBe(true); + }); + + it('callback receives validated, typed args end-to-end via tools/call', async () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + + let received: { x: number } | undefined; + server.registerTool('echo', { inputSchema: { x: z.number() } }, async args => { + received = args; + return { content: [{ type: 'text' as const, text: String(args.x) }] }; + }); + + const [client, srv] = InMemoryTransport.createLinkedPair(); + await server.connect(srv); + await client.start(); + + const responses: JSONRPCMessage[] = []; + client.onmessage = m => responses.push(m); + + await client.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'c', version: '1.0.0' } + } + } as JSONRPCMessage); + await client.send({ jsonrpc: '2.0', method: 'notifications/initialized' } as JSONRPCMessage); + await client.send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'echo', arguments: { x: 7 } } + } as JSONRPCMessage); + + await vi.waitFor(() => expect(responses.some(r => 'id' in r && r.id === 2)).toBe(true)); + + expect(received).toEqual({ x: 7 }); + const result = responses.find(r => 'id' in r && r.id === 2) as { result?: { content: Array<{ text: string }> } }; + expect(result.result?.content[0]?.text).toBe('7'); - expect(warn).not.toHaveBeenCalled(); - warn.mockRestore(); + await server.close(); }); });