From 85510c276614bec739786f3a9c2e19f4d827ea1a Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 10 Aug 2025 09:49:44 +0300 Subject: [PATCH 1/5] type generics for CallToolResult - type require structuredContent return if outputSchema defined --- src/examples/server/mcpServerOutputSchema.ts | 2 +- src/server/mcp.test.ts | 3 ++ src/server/mcp.ts | 24 +++++++++++----- src/types.ts | 30 +++++++++++--------- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 75bfe6900..da9bcb658 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -43,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 = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)] as "sunny" | "cloudy" | "rainy" | "stormy" | "snowy"; const structuredContent = { temperature: { diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 10e550df4..b9a4c3a48 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1175,6 +1175,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: [ @@ -1230,6 +1231,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: [ { @@ -1295,6 +1297,7 @@ describe("tool()", () => { timestamp: 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: [ { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..dcb6e8659 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -41,6 +41,8 @@ import { ServerRequest, ServerNotification, ToolAnnotations, + CallToolResultUnstructured, + CallToolResultStructured, } from "../types.js"; import { Completable, CompletableDef } from "./completable.js"; import { UriTemplate, Variables } from "../shared/uriTemplate.js"; @@ -920,7 +922,7 @@ export class McpServer { /** * Registers a tool with a config object and callback. */ - registerTool( + registerTool( name: string, config: { title?: string; @@ -929,7 +931,7 @@ export class McpServer { outputSchema?: OutputArgs; annotations?: ToolAnnotations; }, - cb: ToolCallback + cb: ToolCallback ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); @@ -1148,13 +1150,21 @@ export class ResourceTemplate { * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback = - Args extends ZodRawShape +export type ToolCallback = + InputArgs extends ZodRawShape ? ( - args: z.objectOutputType, + args: z.objectOutputType, extra: RequestHandlerExtra, - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; + ) => OutputArgs extends ZodRawShape + ? CallToolResultStructured | Promise> + : OutputArgs extends undefined + ? CallToolResultUnstructured | Promise + : never + : (extra: RequestHandlerExtra) => OutputArgs extends ZodRawShape + ? CallToolResultStructured | Promise> + : OutputArgs extends undefined + ? CallToolResultUnstructured | Promise + : never; export type RegisteredTool = { title?: string; diff --git a/src/types.ts b/src/types.ts index 323e37389..e74130f22 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"; @@ -951,10 +951,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. * @@ -962,14 +959,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. * @@ -987,6 +976,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. */ @@ -1595,6 +1595,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; From 62abfee130cfb961510e1beb42b17c0a79238e5c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Sep 2025 15:04:46 +0100 Subject: [PATCH 2/5] Apply @cliffhall's suggestions to mcpServerOutputSchema.ts --- src/examples/server/mcpServerOutputSchema.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index da9bcb658..094ffae2c 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -15,6 +15,9 @@ const server = new McpServer( } ); +const weatherConditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"] as const; +const windDirections = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const; + // Define a tool with structured output - Weather data server.registerTool( "get_weather", @@ -29,11 +32,11 @@ 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(), - direction: z.string() + direction: z.enum(windDirections) }) }, }, @@ -43,7 +46,8 @@ 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)] as "sunny" | "cloudy" | "rainy" | "stormy" | "snowy"; + const conditions = + weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; const structuredContent = { temperature: { @@ -54,7 +58,8 @@ server.registerTool( 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)] + direction: + windDirections[Math.floor(Math.random() * windDirections.length)] } }; @@ -77,4 +82,4 @@ async function main() { main().catch((error) => { console.error("Server error:", error); process.exit(1); -}); \ No newline at end of file +}); From dbd994581e0182a6ab27c9f8b165629fe8225e8b Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 29 Sep 2025 22:24:30 +0300 Subject: [PATCH 3/5] call tool result reuse type, add tsdoc comments --- src/server/mcp.ts | 26 ++++++++++++++------------ src/types.ts | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 597331504..b03600f12 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1163,8 +1163,8 @@ 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 = @@ -1172,16 +1172,18 @@ export type ToolCallback, extra: RequestHandlerExtra, - ) => OutputArgs extends ZodRawShape - ? CallToolResultStructured | Promise> - : OutputArgs extends undefined - ? CallToolResultUnstructured | Promise - : never - : (extra: RequestHandlerExtra) => OutputArgs extends ZodRawShape - ? CallToolResultStructured | Promise> - : OutputArgs extends undefined - ? CallToolResultUnstructured | Promise - : never; + ) => CallToolResultByOutputArgsType + : (extra: RequestHandlerExtra) => CallToolResultByOutputArgsType; + +/** + * CallToolResult type generated based on OutputArgs. + */ +export type CallToolResultByOutputArgsType = + OutputArgs extends ZodRawShape + ? CallToolResultStructured | Promise> + : OutputArgs extends undefined + ? CallToolResultUnstructured | Promise + : never; export type RegisteredTool = { title?: string; diff --git a/src/types.ts b/src/types.ts index 8df99ca6c..ab278faac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1647,7 +1647,7 @@ export type ListToolsResult = Infer; export type CallToolResult = Infer; export type CallToolResultUnstructured = Infer; export type CallToolResultStructured = Infer & { - structuredContent: z.infer>; + structuredContent: z.infer>; } export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; From 0ab3a11b507a67f1311e280a5b7bbbdb72499be3 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 31 Oct 2025 08:45:55 +0200 Subject: [PATCH 4/5] simplify CallToolResultByOutputArgsType, CallToolResultStructured rename OArgs to OutputArgs --- src/server/mcp.ts | 4 +--- src/types.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 387261b5d..fb314149b 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1030,9 +1030,7 @@ export type ToolCallback = OutputArgs extends ZodRawShape ? CallToolResultStructured | Promise> - : OutputArgs extends undefined - ? CallToolResultUnstructured | Promise - : never; + : CallToolResultUnstructured | Promise; export type RegisteredTool = { title?: string; diff --git a/src/types.ts b/src/types.ts index f3b563bde..12a100c39 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'; @@ -1594,8 +1594,8 @@ 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 CallToolResultStructured = Infer & { + structuredContent: z.infer>; } export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; From a13252cceb13dd19dbf45f1e6fa824685b85e0fd Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 31 Oct 2025 08:46:55 +0200 Subject: [PATCH 5/5] prettier fix --- src/examples/server/mcpServerOutputSchema.ts | 2 +- src/server/mcp.test.ts | 4 +- src/server/mcp.ts | 24 ++++---- src/types.ts | 60 ++++++++++---------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 361848664..f4d3821a2 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -79,4 +79,4 @@ async function main() { main().catch(error => { console.error('Server error:', error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 06c52ef9d..c5c68fcd3 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1127,7 +1127,7 @@ 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 () => { @@ -4166,4 +4166,4 @@ describe('elicitInput()', () => { } ]); }); -}); \ No newline at end of file +}); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index fb314149b..3aa4c5b57 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -40,7 +40,6 @@ import { UriTemplate, Variables } from '../shared/uriTemplate.js'; import { RequestHandlerExtra } from '../shared/protocol.js'; import { Transport } from '../shared/transport.js'; - /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying @@ -1016,21 +1015,22 @@ export class ResourceTemplate { * - `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 = - InputArgs extends ZodRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => CallToolResultByOutputArgsType - : (extra: RequestHandlerExtra) => CallToolResultByOutputArgsType; +export type ToolCallback< + InputArgs extends undefined | ZodRawShape = undefined, + OutputArgs extends undefined | ZodRawShape = undefined +> = InputArgs extends ZodRawShape + ? ( + args: z.objectOutputType, + extra: RequestHandlerExtra + ) => CallToolResultByOutputArgsType + : (extra: RequestHandlerExtra) => CallToolResultByOutputArgsType; /** * CallToolResult type generated based on OutputArgs. */ -export type CallToolResultByOutputArgsType = - OutputArgs extends ZodRawShape - ? CallToolResultStructured | Promise> - : CallToolResultUnstructured | Promise; +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 12a100c39..d58cadbdb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -978,37 +978,37 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ }); export const CallToolResultUnstructuredSchema = ResultSchema.extend({ - /** - * A list of content objects that represent the result of the tool call. - * - * 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: z.array(ContentBlockSchema).default([]), - /** - * 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()), + /** + * A list of content objects that represent the result of the tool call. + * + * 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: z.array(ContentBlockSchema).default([]), + /** + * 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()) }); 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(), + /** + * 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]); @@ -1595,8 +1595,8 @@ export type ListToolsResult = Infer; export type CallToolResult = Infer; export type CallToolResultUnstructured = Infer; export type CallToolResultStructured = Infer & { - structuredContent: z.infer>; -} + structuredContent: z.infer>; +}; export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; export type ToolListChangedNotification = Infer;