From c5d9901b01d5db1dbe09520d88ee2a3a87869549 Mon Sep 17 00:00:00 2001 From: Joaquin Hui Gomez Date: Tue, 17 Mar 2026 01:49:21 +0000 Subject: [PATCH 1/2] fix: skip client-side structuredContent validation when isError is true When a tool with outputSchema returns isError: true along with structuredContent that doesn't match the schema, the client-side validation was still running and throwing, preventing the error from reaching the caller. Now both callTool() and callToolStream() skip schema validation for error results, matching the server-side behavior. Fixes #654 --- packages/client/src/client/client.ts | 4 +- .../client/src/experimental/tasks/client.ts | 4 +- test/integration/test/client/client.test.ts | 169 ++++++++++++++++++ 3 files changed, 173 insertions(+), 4 deletions(-) diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index edb08ee58..443f8245a 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -879,8 +879,8 @@ export class Client extends Protocol { ); } - // Only validate structured content if present (not when there's an error) - if (result.structuredContent) { + // Only validate structured content if present and not an error result + if (result.structuredContent && !result.isError) { try { // Validate the structured content against the schema const validationResult = validator(result.structuredContent); diff --git a/packages/client/src/experimental/tasks/client.ts b/packages/client/src/experimental/tasks/client.ts index 696862c9c..0b0cc8501 100644 --- a/packages/client/src/experimental/tasks/client.ts +++ b/packages/client/src/experimental/tasks/client.ts @@ -128,8 +128,8 @@ export class ExperimentalClientTasks { return; } - // Only validate structured content if present (not when there's an error) - if (result.structuredContent) { + // Only validate structured content if present and not an error result + if (result.structuredContent && !result.isError) { try { // Validate the structured content against the schema const validationResult = validator(result.structuredContent); diff --git a/test/integration/test/client/client.test.ts b/test/integration/test/client/client.test.ts index 948d16e17..db5be613c 100644 --- a/test/integration/test/client/client.test.ts +++ b/test/integration/test/client/client.test.ts @@ -1945,6 +1945,98 @@ describe('outputSchema validation', () => { ); }); + /*** + * Test: Skip structuredContent validation when isError is true + */ + test('should not validate structuredContent when isError is true', async () => { + const server = new Server( + { + name: 'test-server', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ); + + // Set up server handlers + server.setRequestHandler('initialize', async request => ({ + protocolVersion: request.params.protocolVersion, + capabilities: { tools: {} }, + serverInfo: { + name: 'test-server', + version: '1.0.0' + } + })); + + server.setRequestHandler('tools/list', async () => ({ + tools: [ + { + name: 'test-tool', + description: 'A test tool', + inputSchema: { + type: 'object', + properties: {} + }, + outputSchema: { + type: 'object', + properties: { + result: { type: 'string' } + }, + required: ['result'] + } + } + ] + })); + + server.setRequestHandler('tools/call', async () => { + // Return isError with structuredContent that does NOT match the schema + return { + isError: true, + content: [{ type: 'text', text: 'Something went wrong' }], + structuredContent: { wrongField: 123 } + }; + }); + + const client = new Client( + { + name: 'test-client', + version: '1.0.0' + }, + { + capabilities: { + tasks: { + requests: { + tools: { + call: {} + }, + tasks: { + get: true, + list: {}, + result: true + } + } + } + } + } + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + // List tools to cache the schemas + await client.listTools(); + + // Call the tool - should NOT throw, error results skip validation + const result = await client.callTool({ name: 'test-tool' }); + expect(result.isError).toBe(true); + expect(result.content).toEqual([{ type: 'text', text: 'Something went wrong' }]); + expect(result.structuredContent).toEqual({ wrongField: 123 }); + }); + /*** * Test: Handle Tools Without outputSchema Normally */ @@ -3998,6 +4090,83 @@ test('callToolStream() should not validate structuredContent when isError is tru await server.close(); }); +test('callToolStream() should not validate structuredContent when isError is true and structuredContent is invalid', async () => { + const server = new Server( + { + name: 'test-server', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ); + + server.setRequestHandler('tools/list', async () => ({ + tools: [ + { + name: 'test-tool', + description: 'A test tool', + inputSchema: { + type: 'object', + properties: {} + }, + outputSchema: { + type: 'object', + properties: { + result: { type: 'string' } + }, + required: ['result'] + } + } + ] + })); + + server.setRequestHandler('tools/call', async () => { + // Return isError with structuredContent that does NOT match the schema + return { + isError: true, + content: [{ type: 'text', text: 'Something went wrong' }], + structuredContent: { wrongField: 123 } + }; + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + const client = new Client( + { + name: 'test-client', + version: '1.0.0' + }, + { + capabilities: {} + } + ); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + await client.listTools(); + + const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} }); + + const messages = []; + for await (const message of stream) { + messages.push(message); + } + + // Should have received result (not error), with isError flag set + expect(messages.length).toBe(1); + expect(messages[0]!.type).toBe('result'); + if (messages[0]!.type === 'result') { + expect(messages[0]!.result.isError).toBe(true); + expect(messages[0]!.result.content).toEqual([{ type: 'text', text: 'Something went wrong' }]); + } + + await client.close(); + await server.close(); +}); + describe('getSupportedElicitationModes', () => { test('should support nothing when capabilities are undefined', () => { const result = getSupportedElicitationModes(undefined); From 696b4bf3d05fa326f202e1674379f83b6faa98ce Mon Sep 17 00:00:00 2001 From: Joaquin Hui Gomez Date: Wed, 18 Mar 2026 00:10:49 +0000 Subject: [PATCH 2/2] Address PR feedback: add structuredContent assertion and changeset Add structuredContent assertion to callToolStream test for parity with callTool test, and add changeset file for the patch. --- .changeset/skip-structured-content-validation-on-error.md | 5 +++++ test/integration/test/client/client.test.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/skip-structured-content-validation-on-error.md diff --git a/.changeset/skip-structured-content-validation-on-error.md b/.changeset/skip-structured-content-validation-on-error.md new file mode 100644 index 000000000..d41ae2e49 --- /dev/null +++ b/.changeset/skip-structured-content-validation-on-error.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/sdk": patch +--- + +Skip structuredContent validation when tool result has isError: true diff --git a/test/integration/test/client/client.test.ts b/test/integration/test/client/client.test.ts index db5be613c..beff5854d 100644 --- a/test/integration/test/client/client.test.ts +++ b/test/integration/test/client/client.test.ts @@ -4161,6 +4161,7 @@ test('callToolStream() should not validate structuredContent when isError is tru if (messages[0]!.type === 'result') { expect(messages[0]!.result.isError).toBe(true); expect(messages[0]!.result.content).toEqual([{ type: 'text', text: 'Something went wrong' }]); + expect(messages[0]!.result.structuredContent).toEqual({ wrongField: 123 }); } await client.close();