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).