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;