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>
This commit is contained in:
Christian Sidak 2026-04-20 04:56:04 -07:00 • committed by GitHub
commit bdfd7f0154
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 5 deletions

View file

@ -0,0 +1,5 @@
---
'@modelcontextprotocol/core': patch
---
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.

View file

@ -302,15 +302,12 @@ export class TaskManager {
if (isTerminal(task.status)) {
switch (task.status) {
case 'completed': {
case 'completed':
case 'failed': {
const result = await this.getTaskResult({ taskId }, resultSchema, options);
yield { type: 'result', result };
break;
}
case 'failed': {
yield { type: 'error', error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} failed`) };
break;
}
case 'cancelled': {
yield {
type: 'error',

View file

@ -1502,6 +1502,50 @@ describe('Task Lifecycle Integration Tests', () => {
});
});
describe('callToolStream with failed task', () => {
it('should yield stored result (isError: true) when task fails, not a generic ProtocolError', async () => {
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: { tasks: {} }
}
);
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
// Use callToolStream with shouldFail: true so the tool stores a failed result
const stream = client.experimental.tasks.callToolStream(
{ name: 'long-task', arguments: { duration: 100, shouldFail: true } },
{ task: { ttl: 60_000 } }
);
// Collect all stream messages
const messages: Array<{ type: string; task?: unknown; result?: unknown; error?: unknown }> = [];
for await (const message of stream) {
messages.push(message);
}
// First message should be taskCreated
expect(messages[0]!.type).toBe('taskCreated');
// Last message must be 'result' (carrying the stored isError content),
// NOT 'error' (which would mean the generic hardcoded ProtocolError was returned)
const lastMessage = messages.at(-1)!;
expect(lastMessage.type).toBe('result');
// The stored result should contain isError: true and the real failure content
const result = lastMessage.result as { content: Array<{ type: string; text: string }>; isError: boolean };
expect(result.isError).toBe(true);
expect(result.content).toEqual([{ type: 'text', text: 'Task failed as requested' }]);
await transport.close();
}, 15_000);
});
describe('callToolStream with elicitation', () => {
it('should deliver elicitation via callToolStream and complete task', async () => {
const client = new Client(