feat: update TypeScript SDK to implement draft spec changes for structured tool output
- Add outputSchema support to Tool interface with proper documentation - Split CallToolResult into structured and unstructured variants - Change structuredContent from string to object type - Add validation that tools without outputSchema cannot return structuredContent - Add validation that tools with outputSchema must return structuredContent - Update client to validate structured content as object (no JSON parsing) - Update tests to use object format for structuredContent - Add tests for new validation constraints - Update LATEST_PROTOCOL_VERSION to DRAFT-2025-v2 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cf12fe0d83
commit
882e0b7d05
3 changed files with 243 additions and 32 deletions
|
|
@ -39,7 +39,7 @@ describe("OAuth Authorization", () => {
|
|||
const [url, options] = calls[0];
|
||||
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
|
||||
expect(options.headers).toEqual({
|
||||
"MCP-Protocol-Version": "2025-03-26"
|
||||
"MCP-Protocol-Version": "DRAFT-2025-v2"
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -802,7 +802,7 @@ describe('outputSchema validation', () => {
|
|||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'test-tool') {
|
||||
return {
|
||||
structuredContent: JSON.stringify({ result: 'success', count: 42 }),
|
||||
structuredContent: { result: 'success', count: 42 },
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
|
|
@ -825,7 +825,7 @@ describe('outputSchema validation', () => {
|
|||
|
||||
// Call the tool - should validate successfully
|
||||
const result = await client.callTool({ name: 'test-tool' });
|
||||
expect(result.structuredContent).toBe('{"result":"success","count":42}');
|
||||
expect(result.structuredContent).toEqual({ result: 'success', count: 42 });
|
||||
});
|
||||
|
||||
test('should throw error when structuredContent does not match schema', async () => {
|
||||
|
|
@ -874,7 +874,7 @@ describe('outputSchema validation', () => {
|
|||
if (request.params.name === 'test-tool') {
|
||||
// Return invalid structured content (count is string instead of number)
|
||||
return {
|
||||
structuredContent: JSON.stringify({ result: 'success', count: 'not a number' }),
|
||||
structuredContent: { result: 'success', count: 'not a number' },
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
|
|
@ -1094,7 +1094,7 @@ describe('outputSchema validation', () => {
|
|||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'complex-tool') {
|
||||
return {
|
||||
structuredContent: JSON.stringify({
|
||||
structuredContent: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
active: true,
|
||||
|
|
@ -1102,7 +1102,7 @@ describe('outputSchema validation', () => {
|
|||
metadata: {
|
||||
created: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
|
|
@ -1126,9 +1126,9 @@ describe('outputSchema validation', () => {
|
|||
// Call the tool - should validate successfully
|
||||
const result = await client.callTool({ name: 'complex-tool' });
|
||||
expect(result.structuredContent).toBeDefined();
|
||||
const parsedContent = JSON.parse(result.structuredContent as string);
|
||||
expect(parsedContent.name).toBe('John Doe');
|
||||
expect(parsedContent.age).toBe(30);
|
||||
const structuredContent = result.structuredContent as { name: string; age: number };
|
||||
expect(structuredContent.name).toBe('John Doe');
|
||||
expect(structuredContent.age).toBe(30);
|
||||
});
|
||||
|
||||
test('should fail validation with additional properties when not allowed', async () => {
|
||||
|
|
@ -1176,10 +1176,10 @@ describe('outputSchema validation', () => {
|
|||
if (request.params.name === 'strict-tool') {
|
||||
// Return structured content with extra property
|
||||
return {
|
||||
structuredContent: JSON.stringify({
|
||||
structuredContent: {
|
||||
name: 'John',
|
||||
extraField: 'not allowed',
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
|
|
@ -1205,4 +1205,137 @@ describe('outputSchema validation', () => {
|
|||
/Structured content does not match the tool's output schema/
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error when tool without outputSchema returns structuredContent', async () => {
|
||||
const server = new Server({
|
||||
name: 'test-server',
|
||||
version: '1.0.0',
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Set up server handlers
|
||||
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
|
||||
protocolVersion: request.params.protocolVersion,
|
||||
capabilities: {},
|
||||
serverInfo: {
|
||||
name: 'test-server',
|
||||
version: '1.0.0',
|
||||
}
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'test-tool',
|
||||
description: 'A test tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
// No outputSchema defined
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'test-tool') {
|
||||
// Incorrectly return structuredContent for a tool without outputSchema
|
||||
return {
|
||||
structuredContent: { result: 'This should not be allowed' },
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: 'test-client',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// List tools to cache the schemas
|
||||
await client.listTools();
|
||||
|
||||
// Call the tool - should throw validation error
|
||||
await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow(
|
||||
/Tool without outputSchema cannot return structuredContent/
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error when structuredContent is not an object', async () => {
|
||||
const server = new Server({
|
||||
name: 'test-server',
|
||||
version: '1.0.0',
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Set up server handlers
|
||||
server.setRequestHandler(InitializeRequestSchema, async (request) => ({
|
||||
protocolVersion: request.params.protocolVersion,
|
||||
capabilities: {},
|
||||
serverInfo: {
|
||||
name: 'test-server',
|
||||
version: '1.0.0',
|
||||
}
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'test-tool',
|
||||
description: 'A test tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'test-tool') {
|
||||
// Try to return a non-object value as structuredContent
|
||||
return {
|
||||
structuredContent: "This should be an object, not a string" as any,
|
||||
};
|
||||
}
|
||||
throw new Error('Unknown tool');
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
name: 'test-client',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// List tools to cache the schemas
|
||||
await client.listTools();
|
||||
|
||||
// Call the tool - should throw validation error
|
||||
await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -438,11 +438,8 @@ export class Client<
|
|||
}
|
||||
|
||||
try {
|
||||
// Parse the structured content as JSON
|
||||
const contentData = JSON.parse(result.structuredContent as string);
|
||||
|
||||
// Validate the content against the schema
|
||||
const validationResult = outputSchema.safeParse(contentData);
|
||||
// Validate the structured content (which is already an object) against the schema
|
||||
const validationResult = outputSchema.safeParse(result.structuredContent);
|
||||
|
||||
if (!validationResult.success) {
|
||||
throw new McpError(
|
||||
|
|
@ -459,6 +456,14 @@ export class Client<
|
|||
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If tool doesn't have outputSchema, it MUST NOT return structuredContent
|
||||
if (result.structuredContent) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
`Tool without outputSchema cannot return structuredContent`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Reference in a new issue