Skip to content

Commit bdfd7f0

Browse files
Christian-SidakChristian-SidakclaudeKKonstantinov
authored
fix: retrieve stored result from tasks/result for failed tasks (#1930)
Co-authored-by: Christian-Sidak <csidak@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
1 parent b8886e7 commit bdfd7f0

File tree

3 files changed

+51
-5
lines changed

3 files changed

+51
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
Fix `requestStream` to call `tasks/result` for failed tasks instead of yielding a hardcoded `ProtocolError`. When a task reaches the `failed` terminal status, the stream now retrieves and yields the actual stored result (matching the behavior for `completed` tasks), as required by the spec.

packages/core/src/shared/taskManager.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,12 @@ export class TaskManager {
302302

303303
if (isTerminal(task.status)) {
304304
switch (task.status) {
305-
case 'completed': {
305+
case 'completed':
306+
case 'failed': {
306307
const result = await this.getTaskResult({ taskId }, resultSchema, options);
307308
yield { type: 'result', result };
308309
break;
309310
}
310-
case 'failed': {
311-
yield { type: 'error', error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} failed`) };
312-
break;
313-
}
314311
case 'cancelled': {
315312
yield {
316313
type: 'error',

test/integration/test/taskLifecycle.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,50 @@ describe('Task Lifecycle Integration Tests', () => {
15021502
});
15031503
});
15041504

1505+
describe('callToolStream with failed task', () => {
1506+
it('should yield stored result (isError: true) when task fails, not a generic ProtocolError', async () => {
1507+
const client = new Client(
1508+
{
1509+
name: 'test-client',
1510+
version: '1.0.0'
1511+
},
1512+
{
1513+
capabilities: { tasks: {} }
1514+
}
1515+
);
1516+
1517+
const transport = new StreamableHTTPClientTransport(baseUrl);
1518+
await client.connect(transport);
1519+
1520+
// Use callToolStream with shouldFail: true so the tool stores a failed result
1521+
const stream = client.experimental.tasks.callToolStream(
1522+
{ name: 'long-task', arguments: { duration: 100, shouldFail: true } },
1523+
{ task: { ttl: 60_000 } }
1524+
);
1525+
1526+
// Collect all stream messages
1527+
const messages: Array<{ type: string; task?: unknown; result?: unknown; error?: unknown }> = [];
1528+
for await (const message of stream) {
1529+
messages.push(message);
1530+
}
1531+
1532+
// First message should be taskCreated
1533+
expect(messages[0]!.type).toBe('taskCreated');
1534+
1535+
// Last message must be 'result' (carrying the stored isError content),
1536+
// NOT 'error' (which would mean the generic hardcoded ProtocolError was returned)
1537+
const lastMessage = messages.at(-1)!;
1538+
expect(lastMessage.type).toBe('result');
1539+
1540+
// The stored result should contain isError: true and the real failure content
1541+
const result = lastMessage.result as { content: Array<{ type: string; text: string }>; isError: boolean };
1542+
expect(result.isError).toBe(true);
1543+
expect(result.content).toEqual([{ type: 'text', text: 'Task failed as requested' }]);
1544+
1545+
await transport.close();
1546+
}, 15_000);
1547+
});
1548+
15051549
describe('callToolStream with elicitation', () => {
15061550
it('should deliver elicitation via callToolStream and complete task', async () => {
15071551
const client = new Client(

0 commit comments

Comments
 (0)