diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7e61b4364..c3c83bcaf 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -948,6 +948,13 @@ export class McpServer { cb: ToolCallback ): RegisteredTool; + /** + * Registers a tool with a ZodObject schema (e.g., z.object({ ... })). + * The schema's shape will be extracted and used for validation. + * @deprecated Use `registerTool` instead. + */ + tool(name: string, paramsSchema: Schema, cb: ToolCallback): RegisteredTool; + /** * Registers a tool `name` (with a description) taking either parameter schema or annotations. * This unified overload handles both `tool(name, description, paramsSchema, cb)` and @@ -964,6 +971,13 @@ export class McpServer { cb: ToolCallback ): RegisteredTool; + /** + * Registers a tool with a description and ZodObject schema (e.g., z.object({ ... })). + * The schema's shape will be extracted and used for validation. + * @deprecated Use `registerTool` instead. + */ + tool(name: string, description: string, paramsSchema: Schema, cb: ToolCallback): RegisteredTool; + /** * Registers a tool with both parameter schema and annotations. * @deprecated Use `registerTool` instead. @@ -975,6 +989,18 @@ export class McpServer { cb: ToolCallback ): RegisteredTool; + /** + * Registers a tool with a ZodObject schema and annotations. + * The schema's shape will be extracted and used for validation. + * @deprecated Use `registerTool` instead. + */ + tool( + name: string, + paramsSchema: Schema, + annotations: ToolAnnotations, + cb: ToolCallback + ): RegisteredTool; + /** * Registers a tool with description, parameter schema, and annotations. * @deprecated Use `registerTool` instead. @@ -987,6 +1013,19 @@ export class McpServer { cb: ToolCallback ): RegisteredTool; + /** + * Registers a tool with description, ZodObject schema, and annotations. + * The schema's shape will be extracted and used for validation. + * @deprecated Use `registerTool` instead. + */ + tool( + name: string, + description: string, + paramsSchema: Schema, + annotations: ToolAnnotations, + cb: ToolCallback + ): RegisteredTool; + /** * tool() implementation. Parses arguments passed to overrides defined above. */ @@ -1023,8 +1062,31 @@ export class McpServer { // Or: tool(name, description, paramsSchema, annotations, cb) annotations = rest.shift() as ToolAnnotations; } + } else if (typeof firstArg === 'object' && firstArg !== null && isZodSchemaInstance(firstArg)) { + // It's a Zod schema instance (like z.object()), extract its shape if it's an object schema + const shape = getObjectShape(firstArg as AnyObjectSchema); + if (shape) { + // We found an object schema, use its shape + inputSchema = shape; + rest.shift(); + + // Check if the next arg is potentially annotations + if ( + rest.length > 1 && + typeof rest[0] === 'object' && + rest[0] !== null && + !isZodRawShapeCompat(rest[0]) && + !isZodSchemaInstance(rest[0]) + ) { + annotations = rest.shift() as ToolAnnotations; + } + } else { + // It's a schema but not an object schema, treat as annotations + // (This maintains backward compatibility for edge cases) + annotations = rest.shift() as ToolAnnotations; + } } else if (typeof firstArg === 'object' && firstArg !== null) { - // Not a ZodRawShapeCompat, so must be annotations in this position + // Not a ZodRawShapeCompat or Zod schema, so must be annotations in this position // Case: tool(name, annotations, cb) // Or: tool(name, description, annotations, cb) annotations = rest.shift() as ToolAnnotations; diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index f6c2124e1..cf516f665 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -993,6 +993,125 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); + test('should accept z.object() schema and pass arguments correctly', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.tool( + 'test-zobject', + 'Test with ZodObject', + z.object({ + message: z.string() + }), + async ({ message }) => ({ + content: [{ type: 'text', text: `Echo: ${message}` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const listResult = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + + expect(listResult.tools).toHaveLength(1); + expect(listResult.tools[0].name).toBe('test-zobject'); + expect(listResult.tools[0].inputSchema).toMatchObject({ + type: 'object', + properties: { + message: { type: 'string' } + } + }); + + // Verify tool call receives arguments correctly (not the extra object) + const result = await client.request( + { + method: 'tools/call', + params: { + name: 'test-zobject', + arguments: { message: 'Hello World' } + } + }, + CallToolResultSchema + ); + + expect(result.content).toEqual([ + { + type: 'text', + text: 'Echo: Hello World' + } + ]); + }); + + test('should accept z.object() schema with annotations', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.tool( + 'test-zobject-annotations', + 'Test with ZodObject and annotations', + z.object({ + name: z.string(), + value: z.number() + }), + { title: 'ZodObject Tool', readOnlyHint: true }, + async ({ name, value }) => ({ + content: [{ type: 'text', text: `${name}: ${value}` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const listResult = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + + expect(listResult.tools).toHaveLength(1); + expect(listResult.tools[0].name).toBe('test-zobject-annotations'); + expect(listResult.tools[0].inputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'number' } + } + }); + expect(listResult.tools[0].annotations).toEqual({ + title: 'ZodObject Tool', + readOnlyHint: true + }); + + // Verify tool call receives arguments correctly + const result = await client.request( + { + method: 'tools/call', + params: { + name: 'test-zobject-annotations', + arguments: { name: 'test', value: 42 } + } + }, + CallToolResultSchema + ); + + expect(result.content).toEqual([ + { + type: 'text', + text: 'test: 42' + } + ]); + }); + /*** * Test: Tool Argument Validation */