diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 5d1cab0bd..f4d3821a2 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -13,6 +13,8 @@ const server = new McpServer({ version: '1.0.0' }); +const weatherConditions = ['sunny', 'cloudy', 'rainy', 'stormy', 'snowy'] as const; + // Define a tool with structured output - Weather data server.registerTool( 'get_weather', @@ -27,7 +29,7 @@ server.registerTool( celsius: z.number(), fahrenheit: z.number() }), - conditions: z.enum(['sunny', 'cloudy', 'rainy', 'stormy', 'snowy']), + conditions: z.enum(weatherConditions), humidity: z.number().min(0).max(100), wind: z.object({ speed_kmh: z.number(), @@ -41,7 +43,7 @@ server.registerTool( 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)]; + const conditions = weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; const structuredContent = { temperature: { diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index f3669fa64..c5c68fcd3 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1127,6 +1127,8 @@ describe('tool()', () => { /*** * Test: Tool with Output Schema Must Provide Structured Content + * + * We expect a type error as well, as outputSchema is defined, but structuredContent is not returned. */ test('should throw error when tool with outputSchema returns no structuredContent', async () => { const mcpServer = new McpServer({ @@ -1152,6 +1154,7 @@ describe('tool()', () => { resultType: z.string() } }, + // @ts-expect-error - This is a test - we are not providing structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test. async ({ input }) => ({ // Only return content without structuredContent content: [ @@ -1213,6 +1216,7 @@ describe('tool()', () => { resultType: z.string() } }, + // @ts-expect-error - This is a test - we are not providing structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test. async ({ input }) => ({ content: [ { @@ -1274,6 +1278,7 @@ describe('tool()', () => { timestamp: z.string() } }, + // @ts-expect-error - This is a test - we are not providing a valid structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test. async ({ input }) => ({ content: [ { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index fb93bd326..3aa4c5b57 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -31,7 +31,9 @@ import { ServerRequest, ServerNotification, ToolAnnotations, - LoggingMessageNotification + LoggingMessageNotification, + CallToolResultStructured, + CallToolResultUnstructured } from '../types.js'; import { Completable, CompletableDef } from './completable.js'; import { UriTemplate, Variables } from '../shared/uriTemplate.js'; @@ -788,7 +790,7 @@ export class McpServer { /** * Registers a tool with a config object and callback. */ - registerTool( + registerTool( name: string, config: { title?: string; @@ -798,7 +800,7 @@ export class McpServer { annotations?: ToolAnnotations; _meta?: Record; }, - cb: ToolCallback + cb: ToolCallback ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); @@ -1009,16 +1011,26 @@ export class ResourceTemplate { * Parameters will include tool arguments, if applicable, as well as other request handler context. * * The callback should return: - * - `structuredContent` if the tool has an outputSchema defined - * - `content` if the tool does not have an outputSchema + * - `structuredContent` if the tool has an outputSchema defined. + * - `content` if the tool does not have an outputSchema OR if an outputSchema is defined, content *SHOULD* have the serialized JSON structuredContent in a text content for backwards compatibility * - Both fields are optional but typically one should be provided */ -export type ToolCallback = Args extends ZodRawShape +export type ToolCallback< + InputArgs extends undefined | ZodRawShape = undefined, + OutputArgs extends undefined | ZodRawShape = undefined +> = InputArgs extends ZodRawShape ? ( - args: z.objectOutputType, + args: z.objectOutputType, extra: RequestHandlerExtra - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; + ) => CallToolResultByOutputArgsType + : (extra: RequestHandlerExtra) => CallToolResultByOutputArgsType; + +/** + * CallToolResult type generated based on OutputArgs. + */ +export type CallToolResultByOutputArgsType = OutputArgs extends ZodRawShape + ? CallToolResultStructured | Promise> + : CallToolResultUnstructured | Promise; export type RegisteredTool = { title?: string; diff --git a/src/types.ts b/src/types.ts index e6d3fe46e..d58cadbdb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { z, ZodTypeAny } from 'zod'; +import { z, ZodRawShape, ZodTypeAny } from 'zod'; import { AuthInfo } from './server/auth/types.js'; export const LATEST_PROTOCOL_VERSION = '2025-06-18'; @@ -977,10 +977,7 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ tools: z.array(ToolSchema) }); -/** - * The server's response to a tool call. - */ -export const CallToolResultSchema = ResultSchema.extend({ +export const CallToolResultUnstructuredSchema = ResultSchema.extend({ /** * A list of content objects that represent the result of the tool call. * @@ -988,14 +985,6 @@ export const CallToolResultSchema = ResultSchema.extend({ * For backwards compatibility, this field is always present, but it may be empty. */ content: z.array(ContentBlockSchema).default([]), - - /** - * An object containing structured tool output. - * - * If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. - */ - structuredContent: z.object({}).passthrough().optional(), - /** * Whether the tool call ended in an error. * @@ -1013,6 +1002,17 @@ export const CallToolResultSchema = ResultSchema.extend({ isError: z.optional(z.boolean()) }); +export const CallToolResultStructuredSchema = CallToolResultUnstructuredSchema.extend({ + /** + * An object containing structured tool output. + * + * If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. + */ + structuredContent: z.object({}).passthrough().optional() +}); + +export const CallToolResultSchema = z.union([CallToolResultUnstructuredSchema, CallToolResultStructuredSchema]); + /** * CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07. */ @@ -1593,6 +1593,10 @@ export type Tool = Infer; export type ListToolsRequest = Infer; export type ListToolsResult = Infer; export type CallToolResult = Infer; +export type CallToolResultUnstructured = Infer; +export type CallToolResultStructured = Infer & { + structuredContent: z.infer>; +}; export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; export type ToolListChangedNotification = Infer;