diff --git a/src/examples/client/parallelToolCallsClient.ts b/src/examples/client/parallelToolCallsClient.ts index 3783992d..17a77872 100644 --- a/src/examples/client/parallelToolCallsClient.ts +++ b/src/examples/client/parallelToolCallsClient.ts @@ -61,13 +61,19 @@ async function main(): Promise { // Log the results from each tool call for (const [caller, result] of Object.entries(toolResults)) { console.log(`\n=== Tool result for ${caller} ===`); - result.content.forEach((item: { type: string; text?: string; }) => { - if (item.type === 'text') { - console.log(` ${item.text}`); - } else { - console.log(` ${item.type} content:`, item); - } - }); + if (result.content) { + result.content.forEach((item: { type: string; text?: string; }) => { + if (item.type === 'text') { + console.log(` ${item.text}`); + } else { + console.log(` ${item.type} content:`, item); + } + }); + } else if (result.structuredContent) { + console.log(` Structured content: ${result.structuredContent}`); + } else { + console.log(` No content returned`); + } } // 3. Wait for all notifications (10 seconds) diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index c1501a57..2debacf6 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -341,13 +341,19 @@ async function callTool(name: string, args: Record): Promise { - if (item.type === 'text') { - console.log(` ${item.text}`); - } else { - console.log(` ${item.type} content:`, item); - } - }); + if (result.content) { + result.content.forEach(item => { + if (item.type === 'text') { + console.log(` ${item.text}`); + } else { + console.log(` ${item.type} content:`, item); + } + }); + } else if (result.structuredContent) { + console.log(` Structured content: ${result.structuredContent}`); + } else { + console.log(' No content returned'); + } } catch (error) { console.log(`Error calling tool ${name}: ${error}`); } diff --git a/src/examples/client/streamableHttpWithSseFallbackClient.ts b/src/examples/client/streamableHttpWithSseFallbackClient.ts index 7646f0f7..06f35004 100644 --- a/src/examples/client/streamableHttpWithSseFallbackClient.ts +++ b/src/examples/client/streamableHttpWithSseFallbackClient.ts @@ -173,13 +173,19 @@ async function startNotificationTool(client: Client): Promise { const result = await client.request(request, CallToolResultSchema); console.log('Tool result:'); - result.content.forEach(item => { - if (item.type === 'text') { - console.log(` ${item.text}`); - } else { - console.log(` ${item.type} content:`, item); - } - }); + if (result.content) { + result.content.forEach(item => { + if (item.type === 'text') { + console.log(` ${item.text}`); + } else { + console.log(` ${item.type} content:`, item); + } + }); + } else if (result.structuredContent) { + console.log(` Structured content: ${result.structuredContent}`); + } else { + console.log(' No content returned'); + } } catch (error) { console.log(`Error calling notification tool: ${error}`); } diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 5f50df68..433fc8f2 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -768,6 +768,104 @@ describe("tool()", () => { mcpServer.tool("tool2", () => ({ content: [] })); }); + test("should support tool with outputSchema and structuredContent", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + // Register a tool with outputSchema + const registeredTool = mcpServer.tool( + "test", + "Test tool with structured output", + { + input: z.string(), + }, + async ({ input }) => ({ + // When outputSchema is defined, return structuredContent instead of content + structuredContent: JSON.stringify({ + processedInput: input, + resultType: "structured", + timestamp: "2023-01-01T00:00:00Z" + }), + }), + ); + + // Update the tool to add outputSchema + registeredTool.update({ + outputSchema: { + type: "object", + properties: { + processedInput: { type: "string" }, + resultType: { type: "string" }, + timestamp: { type: "string", format: "date-time" } + }, + required: ["processedInput", "resultType", "timestamp"] + } + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + // Verify the tool registration includes outputSchema + const listResult = await client.request( + { + method: "tools/list", + }, + ListToolsResultSchema, + ); + + expect(listResult.tools).toHaveLength(1); + expect(listResult.tools[0].outputSchema).toEqual({ + type: "object", + properties: { + processedInput: { type: "string" }, + resultType: { type: "string" }, + timestamp: { type: "string", format: "date-time" } + }, + required: ["processedInput", "resultType", "timestamp"] + }); + + // Call the tool and verify it returns structuredContent + const result = await client.request( + { + method: "tools/call", + params: { + name: "test", + arguments: { + input: "hello", + }, + }, + }, + CallToolResultSchema, + ); + + expect(result.structuredContent).toBeDefined(); + expect(result.content).toBeUndefined(); // Should not have content when structuredContent is used + + const parsed = JSON.parse(result.structuredContent || "{}"); + expect(parsed.processedInput).toBe("hello"); + expect(parsed.resultType).toBe("structured"); + expect(parsed.timestamp).toBe("2023-01-01T00:00:00Z"); + }); + test("should pass sessionId to tool callback via RequestHandlerExtra", async () => { const mcpServer = new McpServer({ name: "test server", @@ -871,7 +969,7 @@ describe("tool()", () => { expect(receivedRequestId).toBeDefined(); expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); - expect(result.content[0].text).toContain("Received request ID:"); + expect(result.content && result.content[0].text).toContain("Received request ID:"); }); test("should provide sendNotification within tool call", async () => { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 97459fb1..bf1abbe0 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -119,6 +119,7 @@ export class McpServer { strictUnions: true, }) as Tool["inputSchema"]) : EMPTY_OBJECT_JSON_SCHEMA, + outputSchema: tool.outputSchema, annotations: tool.annotations, }; }, @@ -696,6 +697,7 @@ export class McpServer { description, inputSchema: paramsSchema === undefined ? undefined : z.object(paramsSchema), + outputSchema: undefined, annotations, callback: cb, enabled: true, @@ -709,6 +711,7 @@ export class McpServer { } if (typeof updates.description !== "undefined") registeredTool.description = updates.description if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema) + if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = updates.outputSchema if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled @@ -896,6 +899,11 @@ export class ResourceTemplate { * Callback for a tool handler registered with Server.tool(). * * 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 + * - Both fields are optional but typically one should be provided */ export type ToolCallback = Args extends ZodRawShape @@ -908,12 +916,13 @@ export type ToolCallback = export type RegisteredTool = { description?: string; inputSchema?: AnyZodObject; + outputSchema?: Tool["outputSchema"]; annotations?: ToolAnnotations; callback: ToolCallback; enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, description?: string, paramsSchema?: Args, callback?: ToolCallback, annotations?: ToolAnnotations, enabled?: boolean }): void + update(updates: { name?: string | null, description?: string, paramsSchema?: Args, outputSchema?: Tool["outputSchema"], callback?: ToolCallback, annotations?: ToolAnnotations, enabled?: boolean }): void remove(): void }; diff --git a/src/types.ts b/src/types.ts index 2ee0f752..2d5ae51d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -831,6 +831,10 @@ export const ToolSchema = z properties: z.optional(z.object({}).passthrough()), }) .passthrough(), + /** + * A JSON Schema object defining the expected output for the tool. + */ + outputSchema: z.object({type: z.any()}).passthrough().optional(), /** * Optional additional tool information. */ @@ -858,7 +862,8 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ export const CallToolResultSchema = ResultSchema.extend({ content: z.array( z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]), - ), + ).optional(), + structuredContent: z.string().optional(), isError: z.boolean().default(false).optional(), });