From ff438df103c2b43f3d595f5ede78684bb7d951a7 Mon Sep 17 00:00:00 2001 From: Travis Bonnet Date: Thu, 19 Mar 2026 11:15:20 -0500 Subject: [PATCH] fix: allow named interfaces for structuredContent in tool callbacks TypeScript does not allow named interfaces to be assigned to index signatures (Record), even when all properties are compatible. This meant that returning a named interface as structuredContent in a tool callback caused a type error, while inline object types worked fine. Introduces ToolCallbackResult, which uses `object` instead of Record for the structuredContent field in the callback return position. The Zod schema remains unchanged for runtime validation. Adds a compile-time type test in mcp.examples.ts verifying that named interfaces work as structuredContent without type casts. Fixes #837 Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-structured-content-type.md | 14 ++++++++ packages/server/src/server/mcp.examples.ts | 38 ++++++++++++++++++++++ packages/server/src/server/mcp.ts | 14 +++++++- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-structured-content-type.md diff --git a/.changeset/fix-structured-content-type.md b/.changeset/fix-structured-content-type.md new file mode 100644 index 000000000..b5e151451 --- /dev/null +++ b/.changeset/fix-structured-content-type.md @@ -0,0 +1,14 @@ +--- +'@modelcontextprotocol/server': patch +--- + +fix: allow named interfaces for structuredContent in tool callbacks + +Previously, returning a named interface type (e.g., `interface MyResult { data: string }`) +as `structuredContent` in a tool callback caused a TypeScript error because TypeScript +does not allow named interfaces to be assigned to `Record` index signatures. +Inline object types worked fine, making this an inconsistent developer experience. + +The fix introduces a `ToolCallbackResult` type that uses `object` instead of +`Record` for the `structuredContent` field in the callback return position, +while keeping the Zod schema unchanged for runtime validation. diff --git a/packages/server/src/server/mcp.examples.ts b/packages/server/src/server/mcp.examples.ts index 740c1bf18..542415fa1 100644 --- a/packages/server/src/server/mcp.examples.ts +++ b/packages/server/src/server/mcp.examples.ts @@ -122,6 +122,44 @@ async function McpServer_sendLoggingMessage_basic(server: McpServer) { //#endregion McpServer_sendLoggingMessage_basic } +/** + * Example: Using a named interface for structuredContent (verifies fix for #837). + * + * Named interfaces should be assignable to the structuredContent field + * without requiring type casts. Previously, this caused a type error because + * TypeScript does not allow named interfaces to be assigned to index signatures. + */ +interface BmiResult { + bmi: number; + category: string; +} + +function McpServer_registerTool_structuredContent_named_interface(server: McpServer) { + server.registerTool( + 'calculate-bmi-named', + { + title: 'BMI Calculator (named interface)', + description: 'Calculate BMI, returning a named interface type', + inputSchema: z.object({ + weightKg: z.number(), + heightM: z.number() + }), + outputSchema: z.object({ bmi: z.number(), category: z.string() }) + }, + async ({ weightKg, heightM }) => { + const bmi = weightKg / (heightM * heightM); + const result: BmiResult = { + bmi, + category: bmi < 18.5 ? 'underweight' : bmi < 25 ? 'normal' : 'overweight' + }; + return { + content: [{ type: 'text', text: JSON.stringify(result) }], + structuredContent: result + }; + } + ); +} + /** * Example: Logging from inside a tool handler via ctx.mcpReq.log(). */ diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 05136f5b6..d360c75df 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1068,10 +1068,22 @@ export type BaseToolCallback, ctx: Ctx) => ResultT | Promise : (ctx: Ctx) => ResultT | Promise; +/** + * A more permissive version of {@linkcode CallToolResult} for use in tool callback return types. + * + * Uses `object` instead of `Record` for `structuredContent` so that + * named interfaces (not just inline object types) can be returned without type errors. + * This is necessary because TypeScript does not allow named interfaces to be assigned + * to index signatures (`Record`), even when all properties are compatible. + */ +export type ToolCallbackResult = Omit & { + structuredContent?: object; +}; + /** * Callback for a tool handler registered with {@linkcode McpServer.registerTool}. */ -export type ToolCallback = BaseToolCallback; +export type ToolCallback = BaseToolCallback; /** * Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object).