From d522a91e0005e2b48b54ae59943d1fbe6da98c54 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 20 May 2025 15:11:13 -0400 Subject: [PATCH] make content nonoptional, add structured output creation shim --- src/client/index.test.ts | 33 ++++++++----------- src/examples/server/mcpServerOutputSchema.ts | 26 +++++++-------- src/server/mcp.test.ts | 9 ++---- src/server/mcp.ts | 34 ++++++++++++++++++++ src/types.ts | 8 ++--- 5 files changed, 66 insertions(+), 44 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index bbfa80faf..7bbbb9c7d 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -20,6 +20,7 @@ import { import { Transport } from "../shared/transport.js"; import { Server } from "../server/index.js"; import { InMemoryTransport } from "../inMemory.js"; +import { createCallToolStructuredResult } from "../server/mcp.js"; /*** * Test: Initialize with Matching Protocol Version @@ -841,9 +842,7 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { - return { - structuredContent: { result: 'success', count: 42 }, - }; + return createCallToolStructuredResult({ result: 'success', count: 42 }); } throw new Error('Unknown tool'); }); @@ -916,9 +915,7 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { // Return invalid structured content (count is string instead of number) - return { - structuredContent: { result: 'success', count: 'not a number' }, - }; + return createCallToolStructuredResult({ result: 'success', count: 'not a number' }); } throw new Error('Unknown tool'); }); @@ -1145,17 +1142,15 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'complex-tool') { - return { - structuredContent: { - name: 'John Doe', - age: 30, - active: true, - tags: ['user', 'admin'], - metadata: { - created: '2023-01-01T00:00:00Z', - }, + return createCallToolStructuredResult({ + name: 'John Doe', + age: 30, + active: true, + tags: ['user', 'admin'], + metadata: { + created: '2023-01-01T00:00:00Z', }, - }; + }); } throw new Error('Unknown tool'); }); @@ -1230,12 +1225,10 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'strict-tool') { // Return structured content with extra property - return { - structuredContent: { + return createCallToolStructuredResult({ name: 'John', extraField: 'not allowed', - }, - }; + }); } throw new Error('Unknown tool'); }); diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 65230fc73..9327a84dd 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -4,7 +4,7 @@ * This demonstrates how to easily create tools with structured output */ -import { McpServer } from "../../server/mcp.js"; +import { createCallToolStructuredResult, McpServer } from "../../server/mcp.js"; import { StdioServerTransport } from "../../server/stdio.js"; import { z } from "zod"; @@ -45,20 +45,18 @@ server.registerTool( const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10; const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)]; - return { - structuredContent: { - temperature: { - celsius: temp_c, - fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10 - }, - conditions, - humidity: Math.round(Math.random() * 100), - wind: { - speed_kmh: Math.round(Math.random() * 50), - direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)] - } + return createCallToolStructuredResult({ + temperature: { + celsius: temp_c, + fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10 + }, + conditions, + humidity: Math.round(Math.random() * 100), + wind: { + speed_kmh: Math.round(Math.random() * 50), + direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)] } - }; + }) } ); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 1b2f4d4b8..9693562a9 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -18,6 +18,7 @@ import { import { ResourceTemplate } from "./mcp.js"; import { completable } from "./completable.js"; import { UriTemplate } from "../shared/uriTemplate.js"; +import { createCallToolStructuredResult } from "./mcp.js"; describe("McpServer", () => { /*** @@ -1082,12 +1083,10 @@ describe("tool()", () => { timestamp: z.string() }, }, - async ({ input }) => ({ - structuredContent: { + async ({ input }) => createCallToolStructuredResult({ processedInput: input, resultType: "structured", timestamp: "2023-01-01T00:00:00Z" - }, }) ); @@ -1185,13 +1184,11 @@ describe("tool()", () => { timestamp: z.string() }, }, - async ({ input }) => ({ - structuredContent: { + async ({ input }) => createCallToolStructuredResult({ processedInput: input, resultType: "structured", // Missing required 'timestamp' field someExtraField: "unexpected" // Extra field not in schema - }, }) ); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index a5f2d0f1a..0fda31c1f 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1161,3 +1161,37 @@ const EMPTY_COMPLETION_RESULT: CompleteResult = { hasMore: false, }, }; + +/** + * The spec encourages structured tools to return a `content` field + * containing a stringified version of the structured content for backward + * compatibility. + * + * Use this function to create a CallToolStructuredResult with a content + * field from your structured content, like so: + * + * async ({ input }) => createCallToolStructuredResult({ + * firstProp: "first", + * secondProp: 2, + * // ... + * }) + * + * Note: in SDK versions 1.11.*, the content field is required for all tool + * call results, in the interest of backward compatibility. Later versions + * of the SDK will allow tools to omit the content field if they return + * structured content. + */ +export function createCallToolStructuredResult( + structuredContent: CallToolResult["structuredContent"] +): CallToolResult { + return { + structuredContent, + content: [ + { + type: "text", + text: JSON.stringify(structuredContent, null, 2), + }, + ], + }; +} + diff --git a/src/types.ts b/src/types.ts index bd299c8f7..deb378faa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -924,14 +924,14 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({ /** * A list of content objects that represent the result of the tool call. * - * If the Tool defines an outputSchema, this field MAY be present in the result. + * Per the spec, if the Tool defines an outputSchema, this field MAY be present in the result. * - * Tools may use this field to provide compatibility with older clients that - * do not support structured content. + * In this SDK we automatically generate backwards-compatible `content` for older clients, + * so this field can be defined as non-optional. * * Clients that support structured content should ignore this field. */ - content: z.optional(ContentListSchema), + content: ContentListSchema, /** * Whether the tool call ended in an error.