diff --git a/src/examples/client/testOutputSchemaServers.ts b/src/examples/client/testOutputSchemaServers.ts deleted file mode 100755 index 5542ec36..00000000 --- a/src/examples/client/testOutputSchemaServers.ts +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env node -/** - * Client to test outputSchema servers - * This client connects to either the high-level or low-level outputSchema server - * and calls the get_weather tool to demonstrate structured output - */ - -import { Client } from "../../client/index.js"; -import { StdioClientTransport } from "../../client/stdio.js"; - -async function main() { - const serverPath = process.argv[2]; - - if (!serverPath) { - console.error("Usage: npx tsx src/examples/client/testOutputSchemaServers.ts "); - console.error("Example: npx tsx src/examples/client/testOutputSchemaServers.ts ../server/mcpServerOutputSchema.ts"); - process.exit(1); - } - - console.log(`Connecting to ${serverPath}...`); - - // Create transport that spawns the server process - const transport = new StdioClientTransport({ - command: "npx", - args: ["tsx", serverPath] - }); - - const client = new Client({ - name: "output-schema-test-client", - version: "1.0.0" - }, { - capabilities: {} - }); - - try { - await client.connect(transport); - console.log("Connected to server\n"); - - // List available tools - console.log("Listing available tools..."); - const toolsResult = await client.listTools(); - - console.log("Available tools:"); - for (const tool of toolsResult.tools) { - console.log(`- ${tool.name}: ${tool.description}`); - if (tool.outputSchema) { - console.log(" Has outputSchema: Yes"); - console.log(" Output schema:", JSON.stringify(tool.outputSchema, null, 2)); - } else { - console.log(" Has outputSchema: No"); - } - } - - // Call the weather tool - console.log("\nCalling get_weather tool..."); - // Note: Output schema validation only works when using the high-level Client API - // methods like callTool(). Low-level protocol requests do not validate the response. - const weatherResult = await client.callTool({ - name: "get_weather", - arguments: { - city: "London", - country: "UK" - } - }); - - console.log("\nWeather tool result:"); - if (weatherResult.structuredContent) { - console.log("Structured content:"); - console.log(JSON.stringify(weatherResult.structuredContent, null, 2)); - } - - if (weatherResult.content && Array.isArray(weatherResult.content)) { - console.log("Unstructured content:"); - weatherResult.content.forEach(content => { - if (content.type === "text") { - console.log(content.text); - } - }); - } - - await client.close(); - console.log("\nDisconnected from server"); - } catch (error) { - console.error("Error:", error); - process.exit(1); - } -} - -main(); \ No newline at end of file diff --git a/src/examples/server/lowLevelOutputSchema.ts b/src/examples/server/lowLevelOutputSchema.ts deleted file mode 100644 index dfb28415..00000000 --- a/src/examples/server/lowLevelOutputSchema.ts +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env node -/** - * Example MCP server demonstrating tool outputSchema support using the low-level Server API - * This server manually handles tool listing and invocation requests to return structured data - * For a simpler high-level API approach, see mcpServerOutputSchema.ts - */ - -import { Server } from "../../server/index.js"; -import { StdioServerTransport } from "../../server/stdio.js"; -import { CallToolRequest, CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "../../types.js"; - -const server = new Server( - { - name: "output-schema-low-level-example", - version: "1.0.0", - }, - { - capabilities: { - tools: {}, - }, - } -); - -// Tool with structured output -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [ - { - name: "get_weather", - description: "Get weather information for a city", - inputSchema: { - type: "object", - properties: { - city: { type: "string", description: "City name" }, - country: { type: "string", description: "Country code (e.g., US, UK)" } - }, - required: ["city", "country"] - }, - outputSchema: { - type: "object", - properties: { - temperature: { - type: "object", - properties: { - celsius: { type: "number" }, - fahrenheit: { type: "number" } - }, - required: ["celsius", "fahrenheit"] - }, - conditions: { - type: "string", - enum: ["sunny", "cloudy", "rainy", "stormy", "snowy"] - }, - humidity: { type: "number", minimum: 0, maximum: 100 }, - wind: { - type: "object", - properties: { - speed_kmh: { type: "number" }, - direction: { type: "string" } - }, - required: ["speed_kmh", "direction"] - } - }, - required: ["temperature", "conditions", "humidity", "wind"] - } - } - ] -})); - -server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { - switch (request.params.name) { - case "get_weather": { - const { city, country } = request.params.arguments as { city: string; country: string }; - - // Parameters are available but not used in this example - void city; - void country; - - // Simulate weather API call - const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10; - const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)]; - - // Return structured content matching the outputSchema - 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)] - } - } - }; - } - - default: - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${request.params.name}` - ); - } -}); - -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Low-level Output Schema Example Server running on stdio"); -} - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); \ No newline at end of file diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 73042d64..75bfe690 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -45,21 +45,26 @@ 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 { - content: [], - 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)] - } + const 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 { + content: [{ + type: "text", + text: JSON.stringify(structuredContent, null, 2) + }], + structuredContent + }; } ); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 620a875f..37f0c228 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1224,10 +1224,7 @@ describe("tool()", () => { mcpServer.server.connect(serverTransport), ]); - // First call listTools to cache the outputSchema in the client - await client.listTools(); - - // Call the tool and expect it to throw a validation error + // Call the tool and expect it to throw a server-side validation error await expect( client.callTool({ name: "test", @@ -1235,7 +1232,7 @@ describe("tool()", () => { input: "hello", }, }), - ).rejects.toThrow(/Structured content does not match the tool's output schema/); + ).rejects.toThrow(/Invalid structured content for tool test/); }); /*** diff --git a/src/server/mcp.ts b/src/server/mcp.ts index a5f2d0f1..e68691f4 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -198,40 +198,15 @@ export class McpServer { } } - // Handle structured output and backward compatibility - if (tool.outputSchema) { - // Tool has outputSchema, so result must have structuredContent (unless it's an error) - if (!result.structuredContent && !result.isError) { + if (tool.outputSchema && result.structuredContent) { + // if the tool has an output schema, validate structured content + const parseResult = await tool.outputSchema.safeParseAsync( + result.structuredContent, + ); + if (!parseResult.success) { throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has outputSchema but returned no structuredContent`, - ); - } - - // For backward compatibility, if structuredContent is provided but no content, - // automatically serialize the structured content to text - if (result.structuredContent && !result.content) { - result.content = [ - { - type: "text", - text: JSON.stringify(result.structuredContent, null, 2), - }, - ]; - } - } else { - // Tool must have content if no outputSchema - if (!result.content && !result.isError) { - throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has no outputSchema and must return content`, - ); - } - - // If structuredContent is provided, it's an error - if (result.structuredContent) { - throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has no outputSchema but returned structuredContent`, + ErrorCode.InvalidParams, + `Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`, ); } } diff --git a/src/types.ts b/src/types.ts index 98a51321..ae25848e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -837,10 +837,8 @@ export const ToolSchema = z }) .passthrough(), /** - * An optional JSON Schema object defining the structure of the tool's output. - * - * If set, a CallToolResult for this Tool MUST contain a structuredContent field whose contents validate against this schema. - * If not set, a CallToolResult for this Tool MUST NOT contain a structuredContent field and MUST contain a content field. + * An optional JSON Schema object defining the structure of the tool's output returned in + * the structuredContent field of a CallToolResult. */ outputSchema: z.optional( z.object({ @@ -848,7 +846,7 @@ export const ToolSchema = z properties: z.optional(z.object({}).passthrough()), required: z.optional(z.array(z.string())), }) - .passthrough() + .passthrough() ), /** * Optional additional tool information. @@ -873,25 +871,7 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ /** * The server's response to a tool call. - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to true, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. */ -export const ContentListSchema = z.array( - z.union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - EmbeddedResourceSchema, - ]), -); - export const CallToolResultSchema = ResultSchema.extend({ /** * A list of content objects that represent the result of the tool call. @@ -899,7 +879,13 @@ export const CallToolResultSchema = ResultSchema.extend({ * If the Tool does not define an outputSchema, this field MUST be present in the result. * For backwards compatibility, this field is always present, but it may be empty. */ - content: ContentListSchema.default([]), + content: z.array( + z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + EmbeddedResourceSchema, + ])).default([]), /** * An object containing structured tool output. @@ -912,6 +898,15 @@ export const CallToolResultSchema = ResultSchema.extend({ * Whether the tool call ended in an error. * * If not set, this is assumed to be false (the call was successful). + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. */ isError: z.optional(z.boolean()), }); @@ -1366,7 +1361,6 @@ export type ToolAnnotations = Infer; export type Tool = Infer; export type ListToolsRequest = Infer; export type ListToolsResult = Infer; -export type ContentList = Infer; export type CallToolResult = Infer; export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer;