MCP task with status "failed" never calls tasks/result, discarding the tool result payload (structuredContent)
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
Does this issue occur when all extensions are disabled?: Yes
- VS Code Version: 1.136.2
- OS Version: Windows 11
- MCP server: Qt Creator 20.0.1 built-in MCP server (Streamable HTTP / SSE), which declares `tasks.requests.tools.call` and returns task-augmented `tools/call` results.
## Steps to Reproduce
1. Connect an MCP server whose tool legitimately fails but returns a structured payload with `isError: true` (e.g. Qt Creator's `mc_build` when compilation fails).
2. In agent mode, invoke the tool. The server returns `CreateTaskResult` with `status: "working"`.
3. The client polls `tasks/get` until the task reaches terminal status `failed`, with `statusMessage: "Build failed"`.
4. Observed: the tool call fails with `ERROR while calling tool: Task failed: Build failed`. The underlying result payload is lost — the build verdict (`succeeded`, `error_count`, `issues[]`, `summary_text`) in `structuredContent` never reaches the model.
5. Expected: per spec, the underlying `CallToolResult` (with `isError: true`, `content`, and `structuredContent`) should still be retrievable and surfaced.
## Root cause
`src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts`, class `McpTask`, step "2. Get the result once it's available (or propagate errors)":
```typescript
if (status === 'failed') {
const current = this._lastTaskState.read(undefined);
this.promise.error(new Error(`Task ${current.taskId} failed: ${current.statusMessage ?? 'unknown error'}`));
store.dispose();
} else if (status === 'cancelled') {
this.promise.cancel();
store.dispose();
} else if (status === 'input_required') {
// ...calls tasks/result to obtain the SSE stream
} else if (status === 'completed') {
const handler = this._handler.read(reader);
if (handler) {
this.promise.settleWith(handler.getTaskResult({ taskId: _task.taskId }, _token) as Promise);
store.dispose();
}
}
```
On `completed` the client calls `tasks/result` (`getTaskResult`); on `failed` it immediately rejects using only `statusMessage`, and never calls `tasks/result`. Any payload the server attached to the failed task is unrecoverable by the client.
## Why this is incorrect
Per the MCP 2025-11-25 tasks spec (`basic/utilities/tasks`):
- **Result Retrieval**: "When a receiver receives a `tasks/result` request for a task in a terminal status (`completed`, `failed`, or `cancelled`), it **MUST** return the final result of the underlying request, whether that is a successful result or a JSON-RPC error." and "For tasks in a terminal status, receivers **MUST** return from `tasks/result` exactly what the underlying request would have returned, whether that is a successful result or a JSON-RPC error."
- **Task Status**: "`failed`: The associated request did not complete successfully. For tool calls specifically, this includes cases where the tool call result has `isError` set to true."
- **Task Execution Errors**: "If the request completed with a JSON-RPC response, `tasks/result` **MUST** return a successful JSON-RPC response containing that result."
So a tool result with `isError: true` is expected to map to `failed` status, and the payload must remain retrievable via `tasks/result`. Rejecting on `failed` without fetching it drops that payload.
## Impact
Any MCP server that reports tool failures through a structured error payload loses it in VS Code. In the Qt Creator case the agent sees only `Build failed` and cannot see the compiler diagnostics, even though the server returned them. (Workaround: the user must ask the agent to call a separate `list_issues` tool.)
## Suggested fix
In the `failed` branch, call `handler.getTaskResult(...)` and preserve the returned result — either reject with an error that carries the result payload, or resolve with the `CallToolResult` and let the caller handle `isError: true` — so `content` / `structuredContent` are not discarded.
Note: the MCP SDK bundled in the Copilot Chat extension has the same asymmetry in `requestStream()` (it yields `Task failed` without calling `getTaskResult`), so it may need the same treatment.
Related: #308961 (task blocking UX) touches the same code area but is a different problem.
Contributor guide
Assessment
This issue has not been assessed yet.