Skip to content

Commit 7a00ec4

Browse files
committed
registerTool: accept ZodType<object> for input and output schema
1 parent 9757ace commit 7a00ec4

File tree

2 files changed

+307
-53
lines changed

2 files changed

+307
-53
lines changed

src/server/mcp.test.ts

Lines changed: 238 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3583,7 +3583,7 @@ describe('Tool title precedence', () => {
35833583
description: 'Tool with regular title'
35843584
},
35853585
async () => ({
3586-
content: [{ type: 'text', text: 'Response' }]
3586+
content: [{ type: 'text' as const, text: 'Response' }]
35873587
})
35883588
);
35893589

@@ -3598,7 +3598,7 @@ describe('Tool title precedence', () => {
35983598
}
35993599
},
36003600
async () => ({
3601-
content: [{ type: 'text', text: 'Response' }]
3601+
content: [{ type: 'text' as const, text: 'Response' }]
36023602
})
36033603
);
36043604

@@ -4162,3 +4162,239 @@ describe('elicitInput()', () => {
41624162
]);
41634163
});
41644164
});
4165+
4166+
describe('Tools with union and intersection schemas', () => {
4167+
test('should support union schemas', async () => {
4168+
const server = new McpServer({
4169+
name: 'test',
4170+
version: '1.0.0'
4171+
});
4172+
4173+
const client = new Client({
4174+
name: 'test-client',
4175+
version: '1.0.0'
4176+
});
4177+
4178+
const unionSchema = z.union([
4179+
z.object({ type: z.literal('email'), email: z.string().email() }),
4180+
z.object({ type: z.literal('phone'), phone: z.string() })
4181+
]);
4182+
4183+
server.registerTool('contact', { inputSchema: unionSchema }, async args => {
4184+
if (args.type === 'email') {
4185+
return {
4186+
content: [{ type: 'text', text: `Email contact: ${args.email}` }]
4187+
};
4188+
} else {
4189+
return {
4190+
content: [{ type: 'text', text: `Phone contact: ${args.phone}` }]
4191+
};
4192+
}
4193+
});
4194+
4195+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
4196+
await server.connect(serverTransport);
4197+
await client.connect(clientTransport);
4198+
4199+
const emailResult = await client.callTool({
4200+
name: 'contact',
4201+
arguments: {
4202+
type: 'email',
4203+
4204+
}
4205+
});
4206+
4207+
expect(emailResult.content).toEqual([
4208+
{
4209+
type: 'text',
4210+
text: 'Email contact: [email protected]'
4211+
}
4212+
]);
4213+
4214+
const phoneResult = await client.callTool({
4215+
name: 'contact',
4216+
arguments: {
4217+
type: 'phone',
4218+
phone: '+1234567890'
4219+
}
4220+
});
4221+
4222+
expect(phoneResult.content).toEqual([
4223+
{
4224+
type: 'text',
4225+
text: 'Phone contact: +1234567890'
4226+
}
4227+
]);
4228+
});
4229+
4230+
test('should support intersection schemas', async () => {
4231+
const server = new McpServer({
4232+
name: 'test',
4233+
version: '1.0.0'
4234+
});
4235+
4236+
const client = new Client({
4237+
name: 'test-client',
4238+
version: '1.0.0'
4239+
});
4240+
4241+
const baseSchema = z.object({ id: z.string() });
4242+
const extendedSchema = z.object({ name: z.string(), age: z.number() });
4243+
const intersectionSchema = z.intersection(baseSchema, extendedSchema);
4244+
4245+
server.registerTool('user', { inputSchema: intersectionSchema }, async args => {
4246+
return {
4247+
content: [
4248+
{
4249+
type: 'text',
4250+
text: `User: ${args.id}, ${args.name}, ${args.age} years old`
4251+
}
4252+
]
4253+
};
4254+
});
4255+
4256+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
4257+
await server.connect(serverTransport);
4258+
await client.connect(clientTransport);
4259+
4260+
const result = await client.callTool({
4261+
name: 'user',
4262+
arguments: {
4263+
id: '123',
4264+
name: 'John Doe',
4265+
age: 30
4266+
}
4267+
});
4268+
4269+
expect(result.content).toEqual([
4270+
{
4271+
type: 'text',
4272+
text: 'User: 123, John Doe, 30 years old'
4273+
}
4274+
]);
4275+
});
4276+
4277+
test('should support complex nested schemas', async () => {
4278+
const server = new McpServer({
4279+
name: 'test',
4280+
version: '1.0.0'
4281+
});
4282+
4283+
const client = new Client({
4284+
name: 'test-client',
4285+
version: '1.0.0'
4286+
});
4287+
4288+
const schema = z.object({
4289+
items: z.array(
4290+
z.union([
4291+
z.object({ type: z.literal('text'), content: z.string() }),
4292+
z.object({ type: z.literal('number'), value: z.number() })
4293+
])
4294+
)
4295+
});
4296+
4297+
server.registerTool('process', { inputSchema: schema }, async args => {
4298+
const processed = args.items.map(item => {
4299+
if (item.type === 'text') {
4300+
return item.content.toUpperCase();
4301+
} else {
4302+
return item.value * 2;
4303+
}
4304+
});
4305+
return {
4306+
content: [
4307+
{
4308+
type: 'text',
4309+
text: `Processed: ${processed.join(', ')}`
4310+
}
4311+
]
4312+
};
4313+
});
4314+
4315+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
4316+
await server.connect(serverTransport);
4317+
await client.connect(clientTransport);
4318+
4319+
const result = await client.callTool({
4320+
name: 'process',
4321+
arguments: {
4322+
items: [
4323+
{ type: 'text', content: 'hello' },
4324+
{ type: 'number', value: 5 },
4325+
{ type: 'text', content: 'world' }
4326+
]
4327+
}
4328+
});
4329+
4330+
expect(result.content).toEqual([
4331+
{
4332+
type: 'text',
4333+
text: 'Processed: HELLO, 10, WORLD'
4334+
}
4335+
]);
4336+
});
4337+
4338+
test('should validate union schema inputs correctly', async () => {
4339+
const server = new McpServer({
4340+
name: 'test',
4341+
version: '1.0.0'
4342+
});
4343+
4344+
const client = new Client({
4345+
name: 'test-client',
4346+
version: '1.0.0'
4347+
});
4348+
4349+
const unionSchema = z.union([
4350+
z.object({ type: z.literal('a'), value: z.string() }),
4351+
z.object({ type: z.literal('b'), value: z.number() })
4352+
]);
4353+
4354+
server.registerTool('union-test', { inputSchema: unionSchema }, async () => {
4355+
return {
4356+
content: [{ type: 'text', text: 'Success' }]
4357+
};
4358+
});
4359+
4360+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
4361+
await server.connect(serverTransport);
4362+
await client.connect(clientTransport);
4363+
4364+
const invalidTypeResult = await client.callTool({
4365+
name: 'union-test',
4366+
arguments: {
4367+
type: 'a',
4368+
value: 123
4369+
}
4370+
});
4371+
4372+
expect(invalidTypeResult.isError).toBe(true);
4373+
expect(invalidTypeResult.content).toEqual(
4374+
expect.arrayContaining([
4375+
expect.objectContaining({
4376+
type: 'text',
4377+
text: expect.stringContaining('Input validation error')
4378+
})
4379+
])
4380+
);
4381+
4382+
const invalidDiscriminatorResult = await client.callTool({
4383+
name: 'union-test',
4384+
arguments: {
4385+
type: 'c',
4386+
value: 'test'
4387+
}
4388+
});
4389+
4390+
expect(invalidDiscriminatorResult.isError).toBe(true);
4391+
expect(invalidDiscriminatorResult.content).toEqual(
4392+
expect.arrayContaining([
4393+
expect.objectContaining({
4394+
type: 'text',
4395+
text: expect.stringContaining('Input validation error')
4396+
})
4397+
])
4398+
);
4399+
});
4400+
});

0 commit comments

Comments
 (0)