feat: support Standard Schema for tool/prompt schemas (#1689)
Co-authored-by: Matt Carey <mcarey@cloudflare.com>
This commit is contained in:
parent
ccb78f2043
commit
0784be1a67
2 changed files with 1327 additions and 212 deletions
|
|
@ -34,19 +34,22 @@
|
|||
"test:integration:deno": "deno test --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/core": "workspace:^",
|
||||
"@cfworker/json-schema": "catalog:runtimeShared",
|
||||
"@modelcontextprotocol/client": "workspace:^",
|
||||
"@modelcontextprotocol/server": "workspace:^",
|
||||
"@modelcontextprotocol/core": "workspace:^",
|
||||
"@modelcontextprotocol/eslint-config": "workspace:^",
|
||||
"@modelcontextprotocol/express": "workspace:^",
|
||||
"@modelcontextprotocol/node": "workspace:^",
|
||||
"@cfworker/json-schema": "catalog:runtimeShared",
|
||||
"zod": "catalog:runtimeShared",
|
||||
"vitest": "catalog:devTools",
|
||||
"supertest": "catalog:devTools",
|
||||
"wrangler": "catalog:devTools",
|
||||
"@modelcontextprotocol/server": "workspace:^",
|
||||
"@modelcontextprotocol/test-helpers": "workspace:^",
|
||||
"@modelcontextprotocol/tsconfig": "workspace:^",
|
||||
"@modelcontextprotocol/vitest-config": "workspace:^",
|
||||
"@modelcontextprotocol/eslint-config": "workspace:^",
|
||||
"@modelcontextprotocol/test-helpers": "workspace:^"
|
||||
"@valibot/to-json-schema": "catalog:devTools",
|
||||
"arktype": "catalog:devTools",
|
||||
"supertest": "catalog:devTools",
|
||||
"valibot": "catalog:devTools",
|
||||
"vitest": "catalog:devTools",
|
||||
"wrangler": "catalog:devTools",
|
||||
"zod": "catalog:runtimeShared"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
716
test/integration/test/standardSchema.test.ts
Normal file
716
test/integration/test/standardSchema.test.ts
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
/**
|
||||
* Integration tests for Standard Schema support (StandardJSONSchemaV1)
|
||||
* Tests ArkType and Valibot schemas with the MCP SDK
|
||||
*/
|
||||
|
||||
import { Client } from '@modelcontextprotocol/client';
|
||||
import type { TextContent } from '@modelcontextprotocol/core';
|
||||
import { AjvJsonSchemaValidator, fromJsonSchema, InMemoryTransport } from '@modelcontextprotocol/core';
|
||||
import { completable, McpServer } from '@modelcontextprotocol/server';
|
||||
import { toStandardJsonSchema } from '@valibot/to-json-schema';
|
||||
import { type } from 'arktype';
|
||||
import * as v from 'valibot';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
describe('Standard Schema Support', () => {
|
||||
let mcpServer: McpServer;
|
||||
let client: Client;
|
||||
|
||||
beforeEach(async () => {
|
||||
mcpServer = new McpServer({
|
||||
name: 'test server',
|
||||
version: '1.0'
|
||||
});
|
||||
client = new Client({
|
||||
name: 'test client',
|
||||
version: '1.0'
|
||||
});
|
||||
});
|
||||
|
||||
async function connectClientAndServer() {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
|
||||
}
|
||||
|
||||
describe('ArkType schemas', () => {
|
||||
describe('tool registration', () => {
|
||||
test('should register tool with ArkType input schema', async () => {
|
||||
const inputSchema = type({
|
||||
name: 'string',
|
||||
age: 'number'
|
||||
});
|
||||
|
||||
mcpServer.registerTool(
|
||||
'greet',
|
||||
{
|
||||
description: 'Greet a person',
|
||||
inputSchema
|
||||
},
|
||||
async ({ name, age }) => ({
|
||||
content: [{ type: 'text', text: `Hello ${name}, you are ${age} years old` }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({ method: 'tools/list' });
|
||||
|
||||
expect(result.tools).toHaveLength(1);
|
||||
expect(result.tools[0].name).toBe('greet');
|
||||
expect(result.tools[0].inputSchema).toMatchObject({
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
}
|
||||
});
|
||||
// Check required array contains both fields (order may vary by library)
|
||||
expect(result.tools[0].inputSchema.required).toEqual(expect.arrayContaining(['name', 'age']));
|
||||
});
|
||||
|
||||
test('should register tool with ArkType input and output schemas', async () => {
|
||||
const inputSchema = type({ x: 'number', y: 'number' });
|
||||
const outputSchema = type({ result: 'number', operation: 'string' });
|
||||
|
||||
mcpServer.registerTool(
|
||||
'add',
|
||||
{
|
||||
description: 'Add two numbers',
|
||||
inputSchema,
|
||||
outputSchema
|
||||
},
|
||||
async ({ x, y }) => ({
|
||||
content: [{ type: 'text', text: `${x + y}` }],
|
||||
structuredContent: { result: x + y, operation: 'addition' }
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({ method: 'tools/list' });
|
||||
|
||||
expect(result.tools[0].outputSchema).toMatchObject({
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'number' },
|
||||
operation: { type: 'string' }
|
||||
}
|
||||
});
|
||||
expect(result.tools[0].outputSchema!.required).toEqual(expect.arrayContaining(['result', 'operation']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool validation', () => {
|
||||
test('should validate valid input and execute tool', async () => {
|
||||
const inputSchema = type({ value: 'number' });
|
||||
|
||||
mcpServer.registerTool('double', { inputSchema }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `${value * 2}` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'double', arguments: { value: 21 } }
|
||||
});
|
||||
|
||||
expect(result.content[0]).toEqual({ type: 'text', text: '42' });
|
||||
});
|
||||
|
||||
test('should return validation error for invalid input type', async () => {
|
||||
const inputSchema = type({ value: 'number' });
|
||||
|
||||
mcpServer.registerTool('double', { inputSchema }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `${value * 2}` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'double', arguments: { value: 'not a number' } }
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
expect(errorText).toContain('value');
|
||||
expect(errorText).toContain('number');
|
||||
});
|
||||
|
||||
test('should return validation error for invalid enum value', async () => {
|
||||
const inputSchema = type({
|
||||
operation: "'add' | 'subtract' | 'multiply'"
|
||||
});
|
||||
|
||||
mcpServer.registerTool('calculate', { inputSchema }, async ({ operation }) => ({
|
||||
content: [{ type: 'text', text: operation }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'calculate', arguments: { operation: 'divide' } }
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
expect(errorText).toMatch(/add|subtract|multiply/);
|
||||
});
|
||||
|
||||
test('should return validation error for missing required field', async () => {
|
||||
const inputSchema = type({ name: 'string', age: 'number' });
|
||||
|
||||
mcpServer.registerTool('greet', { inputSchema }, async ({ name, age }) => ({
|
||||
content: [{ type: 'text', text: `Hello ${name}, ${age}` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'greet', arguments: { name: 'Alice' } }
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
expect(errorText).toContain('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Valibot schemas', () => {
|
||||
describe('tool registration', () => {
|
||||
test('should register tool with Valibot input schema', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
age: v.number()
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'greet',
|
||||
{
|
||||
description: 'Greet a person',
|
||||
inputSchema
|
||||
},
|
||||
async ({ name, age }) => ({
|
||||
content: [{ type: 'text', text: `Hello ${name}, you are ${age} years old` }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({ method: 'tools/list' });
|
||||
|
||||
expect(result.tools).toHaveLength(1);
|
||||
expect(result.tools[0].name).toBe('greet');
|
||||
expect(result.tools[0].inputSchema).toMatchObject({
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
});
|
||||
});
|
||||
|
||||
test('should register tool with Valibot schema with descriptions', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
city: v.pipe(v.string(), v.description('The city name')),
|
||||
country: v.pipe(v.string(), v.description('The country code'))
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool('weather', { inputSchema }, async () => ({
|
||||
content: [{ type: 'text', text: 'sunny' }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({ method: 'tools/list' });
|
||||
|
||||
expect(result.tools[0].inputSchema.properties).toMatchObject({
|
||||
city: { type: 'string', description: 'The city name' },
|
||||
country: { type: 'string', description: 'The country code' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool validation', () => {
|
||||
test('should validate valid input and execute tool', async () => {
|
||||
const inputSchema = toStandardJsonSchema(v.object({ value: v.number() }));
|
||||
|
||||
mcpServer.registerTool('double', { inputSchema }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `${value * 2}` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'double', arguments: { value: 21 } }
|
||||
});
|
||||
|
||||
expect(result.content[0]).toEqual({ type: 'text', text: '42' });
|
||||
});
|
||||
|
||||
test('should return validation error for invalid input type', async () => {
|
||||
const inputSchema = toStandardJsonSchema(v.object({ value: v.number() }));
|
||||
|
||||
mcpServer.registerTool('double', { inputSchema }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `${value * 2}` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'double', arguments: { value: 'not a number' } }
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
expect(errorText).toContain('number');
|
||||
});
|
||||
|
||||
test('should return validation error for invalid picklist value', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
operation: v.picklist(['add', 'subtract', 'multiply'])
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool('calculate', { inputSchema }, async ({ operation }) => ({
|
||||
content: [{ type: 'text', text: operation }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'calculate', arguments: { operation: 'divide' } }
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
});
|
||||
|
||||
test('should validate min/max constraints', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
percentage: v.pipe(v.number(), v.minValue(0), v.maxValue(100))
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool('setPercentage', { inputSchema }, async ({ percentage }) => ({
|
||||
content: [{ type: 'text', text: `${percentage}%` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
// Valid value
|
||||
const validResult = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'setPercentage', arguments: { percentage: 50 } }
|
||||
});
|
||||
expect(validResult.isError).toBeFalsy();
|
||||
|
||||
// Invalid value (too high)
|
||||
const invalidResult = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'setPercentage', arguments: { percentage: 150 } }
|
||||
});
|
||||
expect(invalidResult.isError).toBe(true);
|
||||
const errorText = (invalidResult.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed schema libraries', () => {
|
||||
test('should support tools with different schema libraries in same server', async () => {
|
||||
// Zod tool
|
||||
mcpServer.registerTool('zod-tool', { inputSchema: z.object({ value: z.string() }) }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `zod: ${value}` }]
|
||||
}));
|
||||
|
||||
// ArkType tool
|
||||
mcpServer.registerTool('arktype-tool', { inputSchema: type({ value: 'string' }) }, async ({ value }) => ({
|
||||
content: [{ type: 'text', text: `arktype: ${value}` }]
|
||||
}));
|
||||
|
||||
// Valibot tool
|
||||
mcpServer.registerTool(
|
||||
'valibot-tool',
|
||||
{ inputSchema: toStandardJsonSchema(v.object({ value: v.string() })) },
|
||||
async ({ value }) => ({ content: [{ type: 'text', text: `valibot: ${value}` }] })
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const tools = await client.request({ method: 'tools/list' });
|
||||
expect(tools.tools).toHaveLength(3);
|
||||
|
||||
// Call each tool
|
||||
const zodResult = await client.request({ method: 'tools/call', params: { name: 'zod-tool', arguments: { value: 'test' } } });
|
||||
expect((zodResult.content[0] as TextContent).text).toBe('zod: test');
|
||||
|
||||
const arktypeResult = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'arktype-tool', arguments: { value: 'test' } }
|
||||
});
|
||||
expect((arktypeResult.content[0] as TextContent).text).toBe('arktype: test');
|
||||
|
||||
const valibotResult = await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'valibot-tool', arguments: { value: 'test' } }
|
||||
});
|
||||
expect((valibotResult.content[0] as TextContent).text).toBe('valibot: test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Raw JSON Schema via fromJsonSchema', () => {
|
||||
const validator = new AjvJsonSchemaValidator();
|
||||
|
||||
test('should register tool with raw JSON Schema input', async () => {
|
||||
const inputSchema = fromJsonSchema<{ name: string }>(
|
||||
{ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
||||
validator
|
||||
);
|
||||
|
||||
mcpServer.registerTool('greet', { inputSchema }, async ({ name }) => ({
|
||||
content: [{ type: 'text', text: `Hello, ${name}!` }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const listed = await client.request({ method: 'tools/list' });
|
||||
expect(listed.tools[0].inputSchema).toMatchObject({
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name']
|
||||
});
|
||||
|
||||
const result = await client.request({ method: 'tools/call', params: { name: 'greet', arguments: { name: 'World' } } });
|
||||
expect((result.content[0] as TextContent).text).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
test('should reject invalid input via AJV validation', async () => {
|
||||
const inputSchema = fromJsonSchema(
|
||||
{ type: 'object', properties: { count: { type: 'number' } }, required: ['count'] },
|
||||
validator
|
||||
);
|
||||
|
||||
mcpServer.registerTool('double', { inputSchema }, async args => {
|
||||
const { count } = args as { count: number };
|
||||
return { content: [{ type: 'text', text: `${count * 2}` }] };
|
||||
});
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({ method: 'tools/call', params: { name: 'double', arguments: { count: 'not a number' } } });
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
expect(errorText).toContain('Input validation error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt completions with Zod completable', () => {
|
||||
// Note: completable() is currently Zod-specific
|
||||
// These tests verify that Zod schemas with completable still work
|
||||
|
||||
test('should support completion with Zod completable schemas', async () => {
|
||||
mcpServer.registerPrompt(
|
||||
'greeting',
|
||||
{
|
||||
argsSchema: z.object({
|
||||
name: completable(z.string(), value =>
|
||||
['Alice', 'Bob', 'Charlie'].filter(n => n.toLowerCase().startsWith(value.toLowerCase()))
|
||||
)
|
||||
})
|
||||
},
|
||||
async ({ name }) => ({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: `Hello ${name}` } }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
// Test completion
|
||||
const result = await client.request({
|
||||
method: 'completion/complete',
|
||||
params: {
|
||||
ref: { type: 'ref/prompt', name: 'greeting' },
|
||||
argument: { name: 'name', value: 'a' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.completion.values).toEqual(['Alice']);
|
||||
});
|
||||
|
||||
test('should return all completions when prefix is empty', async () => {
|
||||
mcpServer.registerPrompt(
|
||||
'greeting',
|
||||
{
|
||||
argsSchema: z.object({
|
||||
name: completable(z.string(), () => ['Alice', 'Bob', 'Charlie'])
|
||||
})
|
||||
},
|
||||
async ({ name }) => ({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: `Hello ${name}` } }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'completion/complete',
|
||||
params: {
|
||||
ref: { type: 'ref/prompt', name: 'greeting' },
|
||||
argument: { name: 'name', value: '' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.completion.values).toEqual(['Alice', 'Bob', 'Charlie']);
|
||||
expect(result.completion.total).toBe(3);
|
||||
});
|
||||
|
||||
test('should support completion for optional completable fields', async () => {
|
||||
mcpServer.registerPrompt(
|
||||
'greeting',
|
||||
{
|
||||
argsSchema: z.object({
|
||||
name: completable(z.string(), value =>
|
||||
['Alice', 'Bob', 'Charlie'].filter(n => n.toLowerCase().startsWith(value.toLowerCase()))
|
||||
).optional()
|
||||
})
|
||||
},
|
||||
async ({ name }) => ({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: `Hello ${name ?? 'there'}` } }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'completion/complete',
|
||||
params: {
|
||||
ref: { type: 'ref/prompt', name: 'greeting' },
|
||||
argument: { name: 'name', value: 'b' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.completion.values).toEqual(['Bob']);
|
||||
});
|
||||
|
||||
test('should return empty result for nonexistent argument name', async () => {
|
||||
mcpServer.registerPrompt(
|
||||
'greeting',
|
||||
{
|
||||
argsSchema: z.object({
|
||||
name: completable(z.string(), () => ['Alice', 'Bob'])
|
||||
})
|
||||
},
|
||||
async ({ name }) => ({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: `Hello ${name}` } }]
|
||||
})
|
||||
);
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'completion/complete',
|
||||
params: {
|
||||
ref: { type: 'ref/prompt', name: 'greeting' },
|
||||
argument: { name: 'nonexistent', value: '' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.completion.values).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error message quality', () => {
|
||||
test('ArkType should provide descriptive error messages', async () => {
|
||||
const inputSchema = type({
|
||||
email: 'string',
|
||||
age: 'number',
|
||||
status: "'active' | 'inactive'"
|
||||
});
|
||||
|
||||
mcpServer.registerTool('test', { inputSchema }, async () => ({
|
||||
content: [{ type: 'text', text: 'ok' }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'test',
|
||||
arguments: {
|
||||
email: 123,
|
||||
age: 'not a number',
|
||||
status: 'unknown'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
|
||||
// Check that error mentions the specific issues
|
||||
expect(errorText).toContain('Input validation error');
|
||||
// ArkType should mention type mismatches
|
||||
expect(errorText).toMatch(/email|age|status/i);
|
||||
});
|
||||
|
||||
test('Valibot should provide descriptive error messages', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
email: v.string(),
|
||||
age: v.number(),
|
||||
status: v.picklist(['active', 'inactive'])
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool('test', { inputSchema }, async () => ({
|
||||
content: [{ type: 'text', text: 'ok' }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'test',
|
||||
arguments: {
|
||||
email: 123,
|
||||
age: 'not a number',
|
||||
status: 'unknown'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
|
||||
// Check that error mentions the specific issues
|
||||
expect(errorText).toContain('Input validation error');
|
||||
// Valibot should provide "Invalid type" messages
|
||||
expect(errorText).toContain('Invalid type');
|
||||
});
|
||||
|
||||
test('Zod should provide descriptive error messages', async () => {
|
||||
const inputSchema = z.object({
|
||||
email: z.string(),
|
||||
age: z.number(),
|
||||
status: z.enum(['active', 'inactive'])
|
||||
});
|
||||
|
||||
mcpServer.registerTool('test', { inputSchema }, async () => ({
|
||||
content: [{ type: 'text', text: 'ok' }]
|
||||
}));
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'test',
|
||||
arguments: {
|
||||
email: 123,
|
||||
age: 'not a number',
|
||||
status: 'unknown'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const errorText = (result.content[0] as TextContent).text;
|
||||
|
||||
// Check that error mentions the specific issues
|
||||
expect(errorText).toContain('Input validation error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type inference', () => {
|
||||
test('ArkType callback should receive correctly typed arguments', async () => {
|
||||
const inputSchema = type({
|
||||
name: 'string',
|
||||
count: 'number',
|
||||
enabled: 'boolean'
|
||||
});
|
||||
|
||||
// This test verifies TypeScript compilation succeeds with correct types
|
||||
mcpServer.registerTool('typed-tool', { inputSchema }, async ({ name, count, enabled }) => {
|
||||
// TypeScript should infer these types correctly
|
||||
const _name: string = name;
|
||||
const _count: number = count;
|
||||
const _enabled: boolean = enabled;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `${_name}: ${_count}, enabled: ${_enabled}` }]
|
||||
};
|
||||
});
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'typed-tool',
|
||||
arguments: { name: 'test', count: 42, enabled: true }
|
||||
}
|
||||
});
|
||||
|
||||
expect((result.content[0] as TextContent).text).toBe('test: 42, enabled: true');
|
||||
});
|
||||
|
||||
test('Valibot callback should receive correctly typed arguments', async () => {
|
||||
const inputSchema = toStandardJsonSchema(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
count: v.number(),
|
||||
enabled: v.boolean()
|
||||
})
|
||||
);
|
||||
|
||||
mcpServer.registerTool('typed-tool', { inputSchema }, async ({ name, count, enabled }) => {
|
||||
// TypeScript should infer these types correctly
|
||||
const _name: string = name;
|
||||
const _count: number = count;
|
||||
const _enabled: boolean = enabled;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `${_name}: ${_count}, enabled: ${_enabled}` }]
|
||||
};
|
||||
});
|
||||
|
||||
await connectClientAndServer();
|
||||
|
||||
const result = await client.request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'typed-tool',
|
||||
arguments: { name: 'test', count: 42, enabled: true }
|
||||
}
|
||||
});
|
||||
|
||||
expect((result.content[0] as TextContent).text).toBe('test: 42, enabled: true');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue