Skip to content

Commit e9b82a0

Browse files
joaquinhuiclaude
andcommitted
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 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ccb78f2 commit e9b82a0

3 files changed

Lines changed: 173 additions & 4 deletions

File tree

packages/client/src/client/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -879,8 +879,8 @@ export class Client extends Protocol<ClientContext> {
879879
);
880880
}
881881

882-
// Only validate structured content if present (not when there's an error)
883-
if (result.structuredContent) {
882+
// Only validate structured content if present and not an error result
883+
if (result.structuredContent && !result.isError) {
884884
try {
885885
// Validate the structured content against the schema
886886
const validationResult = validator(result.structuredContent);

packages/client/src/experimental/tasks/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ export class ExperimentalClientTasks {
128128
return;
129129
}
130130

131-
// Only validate structured content if present (not when there's an error)
132-
if (result.structuredContent) {
131+
// Only validate structured content if present and not an error result
132+
if (result.structuredContent && !result.isError) {
133133
try {
134134
// Validate the structured content against the schema
135135
const validationResult = validator(result.structuredContent);

test/integration/test/client/client.test.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,6 +1945,98 @@ describe('outputSchema validation', () => {
19451945
);
19461946
});
19471947

1948+
/***
1949+
* Test: Skip structuredContent validation when isError is true
1950+
*/
1951+
test('should not validate structuredContent when isError is true', async () => {
1952+
const server = new Server(
1953+
{
1954+
name: 'test-server',
1955+
version: '1.0.0'
1956+
},
1957+
{
1958+
capabilities: {
1959+
tools: {}
1960+
}
1961+
}
1962+
);
1963+
1964+
// Set up server handlers
1965+
server.setRequestHandler('initialize', async request => ({
1966+
protocolVersion: request.params.protocolVersion,
1967+
capabilities: { tools: {} },
1968+
serverInfo: {
1969+
name: 'test-server',
1970+
version: '1.0.0'
1971+
}
1972+
}));
1973+
1974+
server.setRequestHandler('tools/list', async () => ({
1975+
tools: [
1976+
{
1977+
name: 'test-tool',
1978+
description: 'A test tool',
1979+
inputSchema: {
1980+
type: 'object',
1981+
properties: {}
1982+
},
1983+
outputSchema: {
1984+
type: 'object',
1985+
properties: {
1986+
result: { type: 'string' }
1987+
},
1988+
required: ['result']
1989+
}
1990+
}
1991+
]
1992+
}));
1993+
1994+
server.setRequestHandler('tools/call', async () => {
1995+
// Return isError with structuredContent that does NOT match the schema
1996+
return {
1997+
isError: true,
1998+
content: [{ type: 'text', text: 'Something went wrong' }],
1999+
structuredContent: { wrongField: 123 }
2000+
};
2001+
});
2002+
2003+
const client = new Client(
2004+
{
2005+
name: 'test-client',
2006+
version: '1.0.0'
2007+
},
2008+
{
2009+
capabilities: {
2010+
tasks: {
2011+
requests: {
2012+
tools: {
2013+
call: {}
2014+
},
2015+
tasks: {
2016+
get: true,
2017+
list: {},
2018+
result: true
2019+
}
2020+
}
2021+
}
2022+
}
2023+
}
2024+
);
2025+
2026+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2027+
2028+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
2029+
2030+
// List tools to cache the schemas
2031+
await client.listTools();
2032+
2033+
// Call the tool - should NOT throw, error results skip validation
2034+
const result = await client.callTool({ name: 'test-tool' });
2035+
expect(result.isError).toBe(true);
2036+
expect(result.content).toEqual([{ type: 'text', text: 'Something went wrong' }]);
2037+
expect(result.structuredContent).toEqual({ wrongField: 123 });
2038+
});
2039+
19482040
/***
19492041
* Test: Handle Tools Without outputSchema Normally
19502042
*/
@@ -3998,6 +4090,83 @@ test('callToolStream() should not validate structuredContent when isError is tru
39984090
await server.close();
39994091
});
40004092

4093+
test('callToolStream() should not validate structuredContent when isError is true and structuredContent is invalid', async () => {
4094+
const server = new Server(
4095+
{
4096+
name: 'test-server',
4097+
version: '1.0.0'
4098+
},
4099+
{
4100+
capabilities: {
4101+
tools: {}
4102+
}
4103+
}
4104+
);
4105+
4106+
server.setRequestHandler('tools/list', async () => ({
4107+
tools: [
4108+
{
4109+
name: 'test-tool',
4110+
description: 'A test tool',
4111+
inputSchema: {
4112+
type: 'object',
4113+
properties: {}
4114+
},
4115+
outputSchema: {
4116+
type: 'object',
4117+
properties: {
4118+
result: { type: 'string' }
4119+
},
4120+
required: ['result']
4121+
}
4122+
}
4123+
]
4124+
}));
4125+
4126+
server.setRequestHandler('tools/call', async () => {
4127+
// Return isError with structuredContent that does NOT match the schema
4128+
return {
4129+
isError: true,
4130+
content: [{ type: 'text', text: 'Something went wrong' }],
4131+
structuredContent: { wrongField: 123 }
4132+
};
4133+
});
4134+
4135+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
4136+
4137+
const client = new Client(
4138+
{
4139+
name: 'test-client',
4140+
version: '1.0.0'
4141+
},
4142+
{
4143+
capabilities: {}
4144+
}
4145+
);
4146+
4147+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
4148+
4149+
await client.listTools();
4150+
4151+
const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} });
4152+
4153+
const messages = [];
4154+
for await (const message of stream) {
4155+
messages.push(message);
4156+
}
4157+
4158+
// Should have received result (not error), with isError flag set
4159+
expect(messages.length).toBe(1);
4160+
expect(messages[0]!.type).toBe('result');
4161+
if (messages[0]!.type === 'result') {
4162+
expect(messages[0]!.result.isError).toBe(true);
4163+
expect(messages[0]!.result.content).toEqual([{ type: 'text', text: 'Something went wrong' }]);
4164+
}
4165+
4166+
await client.close();
4167+
await server.close();
4168+
});
4169+
40014170
describe('getSupportedElicitationModes', () => {
40024171
test('should support nothing when capabilities are undefined', () => {
40034172
const result = getSupportedElicitationModes(undefined);

0 commit comments

Comments
 (0)