Skip to content

Commit 502d9c1

Browse files
committed
fix: support z.object() schemas in tool() method
1 parent 384311b commit 502d9c1

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed

src/server/mcp.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,13 @@ export class McpServer {
948948
cb: ToolCallback<Args>
949949
): RegisteredTool;
950950

951+
/**
952+
* Registers a tool with a ZodObject schema (e.g., z.object({ ... })).
953+
* The schema's shape will be extracted and used for validation.
954+
* @deprecated Use `registerTool` instead.
955+
*/
956+
tool<Schema extends AnyObjectSchema>(name: string, paramsSchema: Schema, cb: ToolCallback<Schema>): RegisteredTool;
957+
951958
/**
952959
* Registers a tool `name` (with a description) taking either parameter schema or annotations.
953960
* This unified overload handles both `tool(name, description, paramsSchema, cb)` and
@@ -964,6 +971,13 @@ export class McpServer {
964971
cb: ToolCallback<Args>
965972
): RegisteredTool;
966973

974+
/**
975+
* Registers a tool with a description and ZodObject schema (e.g., z.object({ ... })).
976+
* The schema's shape will be extracted and used for validation.
977+
* @deprecated Use `registerTool` instead.
978+
*/
979+
tool<Schema extends AnyObjectSchema>(name: string, description: string, paramsSchema: Schema, cb: ToolCallback<Schema>): RegisteredTool;
980+
967981
/**
968982
* Registers a tool with both parameter schema and annotations.
969983
* @deprecated Use `registerTool` instead.
@@ -975,6 +989,18 @@ export class McpServer {
975989
cb: ToolCallback<Args>
976990
): RegisteredTool;
977991

992+
/**
993+
* Registers a tool with a ZodObject schema and annotations.
994+
* The schema's shape will be extracted and used for validation.
995+
* @deprecated Use `registerTool` instead.
996+
*/
997+
tool<Schema extends AnyObjectSchema>(
998+
name: string,
999+
paramsSchema: Schema,
1000+
annotations: ToolAnnotations,
1001+
cb: ToolCallback<Schema>
1002+
): RegisteredTool;
1003+
9781004
/**
9791005
* Registers a tool with description, parameter schema, and annotations.
9801006
* @deprecated Use `registerTool` instead.
@@ -987,6 +1013,19 @@ export class McpServer {
9871013
cb: ToolCallback<Args>
9881014
): RegisteredTool;
9891015

1016+
/**
1017+
* Registers a tool with description, ZodObject schema, and annotations.
1018+
* The schema's shape will be extracted and used for validation.
1019+
* @deprecated Use `registerTool` instead.
1020+
*/
1021+
tool<Schema extends AnyObjectSchema>(
1022+
name: string,
1023+
description: string,
1024+
paramsSchema: Schema,
1025+
annotations: ToolAnnotations,
1026+
cb: ToolCallback<Schema>
1027+
): RegisteredTool;
1028+
9901029
/**
9911030
* tool() implementation. Parses arguments passed to overrides defined above.
9921031
*/
@@ -1023,8 +1062,31 @@ export class McpServer {
10231062
// Or: tool(name, description, paramsSchema, annotations, cb)
10241063
annotations = rest.shift() as ToolAnnotations;
10251064
}
1065+
} else if (typeof firstArg === 'object' && firstArg !== null && isZodSchemaInstance(firstArg)) {
1066+
// It's a Zod schema instance (like z.object()), extract its shape if it's an object schema
1067+
const shape = getObjectShape(firstArg as AnyObjectSchema);
1068+
if (shape) {
1069+
// We found an object schema, use its shape
1070+
inputSchema = shape;
1071+
rest.shift();
1072+
1073+
// Check if the next arg is potentially annotations
1074+
if (
1075+
rest.length > 1 &&
1076+
typeof rest[0] === 'object' &&
1077+
rest[0] !== null &&
1078+
!isZodRawShapeCompat(rest[0]) &&
1079+
!isZodSchemaInstance(rest[0])
1080+
) {
1081+
annotations = rest.shift() as ToolAnnotations;
1082+
}
1083+
} else {
1084+
// It's a schema but not an object schema, treat as annotations
1085+
// (This maintains backward compatibility for edge cases)
1086+
annotations = rest.shift() as ToolAnnotations;
1087+
}
10261088
} else if (typeof firstArg === 'object' && firstArg !== null) {
1027-
// Not a ZodRawShapeCompat, so must be annotations in this position
1089+
// Not a ZodRawShapeCompat or Zod schema, so must be annotations in this position
10281090
// Case: tool(name, annotations, cb)
10291091
// Or: tool(name, description, annotations, cb)
10301092
annotations = rest.shift() as ToolAnnotations;

test/server/mcp.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,107 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
993993
expect(result.tools[1].annotations).toEqual(result.tools[0].annotations);
994994
});
995995

996+
test('should accept z.object() schemas and extract arguments correctly', async () => {
997+
const mcpServer = new McpServer({
998+
name: 'test server',
999+
version: '1.0'
1000+
});
1001+
const client = new Client({
1002+
name: 'test client',
1003+
version: '1.0'
1004+
});
1005+
1006+
// tool() with z.object() schema
1007+
mcpServer.tool(
1008+
'test-zobject',
1009+
'Test with ZodObject',
1010+
z.object({
1011+
message: z.string()
1012+
}),
1013+
async ({ message }) => ({
1014+
content: [{ type: 'text', text: `Echo: ${message}` }]
1015+
})
1016+
);
1017+
1018+
// tool() with z.object() and annotations
1019+
mcpServer.tool(
1020+
'test-zobject-annotations',
1021+
'Test with ZodObject and annotations',
1022+
z.object({
1023+
name: z.string(),
1024+
value: z.number()
1025+
}),
1026+
{ title: 'ZodObject Tool', readOnlyHint: true },
1027+
async ({ name, value }) => ({
1028+
content: [{ type: 'text', text: `${name}: ${value}` }]
1029+
})
1030+
);
1031+
1032+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1033+
1034+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
1035+
1036+
const listResult = await client.request({ method: 'tools/list' }, ListToolsResultSchema);
1037+
1038+
expect(listResult.tools).toHaveLength(2);
1039+
expect(listResult.tools[0].name).toBe('test-zobject');
1040+
expect(listResult.tools[0].inputSchema).toMatchObject({
1041+
type: 'object',
1042+
properties: {
1043+
message: { type: 'string' }
1044+
}
1045+
});
1046+
1047+
expect(listResult.tools[1].name).toBe('test-zobject-annotations');
1048+
expect(listResult.tools[1].inputSchema).toMatchObject({
1049+
type: 'object',
1050+
properties: {
1051+
name: { type: 'string' },
1052+
value: { type: 'number' }
1053+
}
1054+
});
1055+
expect(listResult.tools[1].annotations).toEqual({
1056+
title: 'ZodObject Tool',
1057+
readOnlyHint: true
1058+
});
1059+
1060+
const result1 = await client.request(
1061+
{
1062+
method: 'tools/call',
1063+
params: {
1064+
name: 'test-zobject',
1065+
arguments: { message: 'Hello World' }
1066+
}
1067+
},
1068+
CallToolResultSchema
1069+
);
1070+
1071+
expect(result1.content).toEqual([
1072+
{
1073+
type: 'text',
1074+
text: 'Echo: Hello World'
1075+
}
1076+
]);
1077+
1078+
const result2 = await client.request(
1079+
{
1080+
method: 'tools/call',
1081+
params: {
1082+
name: 'test-zobject-annotations',
1083+
arguments: { name: 'test', value: 42 }
1084+
}
1085+
},
1086+
CallToolResultSchema
1087+
);
1088+
1089+
expect(result2.content).toEqual([
1090+
{
1091+
type: 'text',
1092+
text: 'test: 42'
1093+
}
1094+
]);
1095+
});
1096+
9961097
/***
9971098
* Test: Tool Argument Validation
9981099
*/

0 commit comments

Comments
 (0)