|
1 | 1 | import { Client, getSupportedElicitationModes } from '@modelcontextprotocol/client'; |
2 | | -import type { Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core'; |
| 2 | +import type { CallToolResult, Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core'; |
3 | 3 | import { |
4 | 4 | CallToolResultSchema, |
5 | 5 | ElicitResultSchema, |
@@ -2278,6 +2278,72 @@ describe('outputSchema validation', () => { |
2278 | 2278 | /Structured content does not match the tool's output schema/ |
2279 | 2279 | ); |
2280 | 2280 | }); |
| 2281 | + |
| 2282 | + /*** |
| 2283 | + * Test: Skip outputSchema validation when result is an error (mirrors server-side behavior) |
| 2284 | + */ |
| 2285 | + test('should not validate structuredContent against outputSchema when isError is true', async () => { |
| 2286 | + const server = new Server( |
| 2287 | + { |
| 2288 | + name: 'test-server', |
| 2289 | + version: '1.0.0' |
| 2290 | + }, |
| 2291 | + { |
| 2292 | + capabilities: { |
| 2293 | + tools: {} |
| 2294 | + } |
| 2295 | + } |
| 2296 | + ); |
| 2297 | + |
| 2298 | + server.setRequestHandler('tools/list', async () => ({ |
| 2299 | + tools: [ |
| 2300 | + { |
| 2301 | + name: 'test-tool', |
| 2302 | + description: 'A test tool', |
| 2303 | + inputSchema: { |
| 2304 | + type: 'object', |
| 2305 | + properties: {} |
| 2306 | + }, |
| 2307 | + outputSchema: { |
| 2308 | + type: 'object', |
| 2309 | + properties: { |
| 2310 | + result: { type: 'string' }, |
| 2311 | + count: { type: 'number' } |
| 2312 | + }, |
| 2313 | + required: ['result', 'count'] |
| 2314 | + } |
| 2315 | + } |
| 2316 | + ] |
| 2317 | + })); |
| 2318 | + |
| 2319 | + server.setRequestHandler('tools/call', async () => { |
| 2320 | + // Error envelope carrying a structuredContent shape that does NOT match outputSchema. |
| 2321 | + // Server-side validateToolOutput short-circuits on isError; client must behave the same. |
| 2322 | + return { |
| 2323 | + isError: true, |
| 2324 | + content: [{ type: 'text', text: 'NOT_FOUND' }], |
| 2325 | + structuredContent: { error: { code: 'NOT_FOUND', message: 'nope' } } |
| 2326 | + }; |
| 2327 | + }); |
| 2328 | + |
| 2329 | + const client = new Client( |
| 2330 | + { |
| 2331 | + name: 'test-client', |
| 2332 | + version: '1.0.0' |
| 2333 | + }, |
| 2334 | + { capabilities: { tasks: { requests: { tools: { call: {} } } } } } |
| 2335 | + ); |
| 2336 | + |
| 2337 | + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 2338 | + |
| 2339 | + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); |
| 2340 | + |
| 2341 | + await client.listTools(); |
| 2342 | + |
| 2343 | + const result = await client.callTool({ name: 'test-tool' }); |
| 2344 | + expect(result.isError).toBe(true); |
| 2345 | + expect(result.structuredContent).toEqual({ error: { code: 'NOT_FOUND', message: 'nope' } }); |
| 2346 | + }); |
2281 | 2347 | }); |
2282 | 2348 |
|
2283 | 2349 | describe('Task-based execution', () => { |
@@ -4087,6 +4153,85 @@ test('callToolStream() should not validate structuredContent when isError is tru |
4087 | 4153 | await server.close(); |
4088 | 4154 | }); |
4089 | 4155 |
|
| 4156 | +test('callToolStream() should not validate structuredContent when isError is true', async () => { |
| 4157 | + const server = new Server( |
| 4158 | + { |
| 4159 | + name: 'test-server', |
| 4160 | + version: '1.0.0' |
| 4161 | + }, |
| 4162 | + { |
| 4163 | + capabilities: { |
| 4164 | + tools: {} |
| 4165 | + } |
| 4166 | + } |
| 4167 | + ); |
| 4168 | + |
| 4169 | + server.setRequestHandler('tools/list', async () => ({ |
| 4170 | + tools: [ |
| 4171 | + { |
| 4172 | + name: 'test-tool', |
| 4173 | + description: 'A test tool', |
| 4174 | + inputSchema: { |
| 4175 | + type: 'object', |
| 4176 | + properties: {} |
| 4177 | + }, |
| 4178 | + outputSchema: { |
| 4179 | + type: 'object', |
| 4180 | + properties: { |
| 4181 | + result: { type: 'string' } |
| 4182 | + }, |
| 4183 | + required: ['result'] |
| 4184 | + } |
| 4185 | + } |
| 4186 | + ] |
| 4187 | + })); |
| 4188 | + |
| 4189 | + server.setRequestHandler('tools/call', async () => { |
| 4190 | + // Error envelope carrying a structuredContent shape that does NOT match outputSchema. |
| 4191 | + // Server-side validateToolOutput short-circuits on isError; client must behave the same. |
| 4192 | + return { |
| 4193 | + isError: true, |
| 4194 | + content: [{ type: 'text', text: 'NOT_FOUND' }], |
| 4195 | + structuredContent: { error: { code: 'NOT_FOUND', message: 'nope' } } |
| 4196 | + }; |
| 4197 | + }); |
| 4198 | + |
| 4199 | + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 4200 | + |
| 4201 | + const client = new Client( |
| 4202 | + { |
| 4203 | + name: 'test-client', |
| 4204 | + version: '1.0.0' |
| 4205 | + }, |
| 4206 | + { |
| 4207 | + capabilities: { tasks: { requests: { tools: { call: {} } } } } |
| 4208 | + } |
| 4209 | + ); |
| 4210 | + |
| 4211 | + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); |
| 4212 | + |
| 4213 | + await client.listTools(); |
| 4214 | + |
| 4215 | + const stream = client.experimental.tasks.callToolStream({ name: 'test-tool', arguments: {} }); |
| 4216 | + |
| 4217 | + const messages = []; |
| 4218 | + for await (const message of stream) { |
| 4219 | + messages.push(message); |
| 4220 | + } |
| 4221 | + |
| 4222 | + // Should have received result (not error), with isError flag set and structuredContent preserved |
| 4223 | + expect(messages.length).toBe(1); |
| 4224 | + expect(messages[0]!.type).toBe('result'); |
| 4225 | + if (messages[0]!.type === 'result') { |
| 4226 | + const result = messages[0]!.result as CallToolResult; |
| 4227 | + expect(result.isError).toBe(true); |
| 4228 | + expect(result.structuredContent).toEqual({ error: { code: 'NOT_FOUND', message: 'nope' } }); |
| 4229 | + } |
| 4230 | + |
| 4231 | + await client.close(); |
| 4232 | + await server.close(); |
| 4233 | +}); |
| 4234 | + |
4090 | 4235 | describe('getSupportedElicitationModes', () => { |
4091 | 4236 | test('should support nothing when capabilities are undefined', () => { |
4092 | 4237 | const result = getSupportedElicitationModes(undefined); |
|
0 commit comments