diff --git a/.changeset/validate-wrapped-output-schemas.md b/.changeset/validate-wrapped-output-schemas.md new file mode 100644 index 0000000000..126402701e --- /dev/null +++ b/.changeset/validate-wrapped-output-schemas.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Validate structured tool output against the registered output schema so wrapped Zod schemas are enforced correctly. diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 9fe0ed549c..8928806ff0 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -306,8 +306,7 @@ export class McpServer { } // if the tool has an output schema, validate structured content - const outputObj = normalizeObjectSchema(tool.outputSchema) as AnyObjectSchema; - const parseResult = await safeParseAsync(outputObj, result.structuredContent); + const parseResult = await safeParseAsync(tool.outputSchema, result.structuredContent); if (!parseResult.success) { const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; const errorMessage = getParseErrorMessage(error); diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index 575d6a300e..40c198ae74 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -1295,6 +1295,54 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(JSON.parse(textContent.text)).toEqual(result.structuredContent); }); + test('should validate structuredContent against wrapped and union outputSchema values', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const outputSchemas = { + optional: z.object({ data: z.string() }).optional(), + nullable: z.object({ data: z.string() }).nullable(), + nullish: z.object({ data: z.string() }).nullish(), + union: z.union([z.object({ data: z.string() }), z.object({ value: z.string() })]) + }; + + for (const [name, outputSchema] of Object.entries(outputSchemas)) { + mcpServer.registerTool( + name, + { + outputSchema + }, + async () => ({ + content: [], + structuredContent: { + data: name + } + }) + ); + } + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + for (const name of Object.keys(outputSchemas)) { + const result = await client.callTool({ + name, + arguments: {} + }); + + expect(result.isError).not.toBe(true); + expect(result.structuredContent).toEqual({ data: name }); + } + }); + /*** * Test: Tool with Output Schema Must Provide Structured Content */