microsoft / microsoft/vscode

MCP task with status "failed" never calls tasks/result, discarding the tool result payload (structuredContent)

Open
#335,229 0 comments 0 reactions 1 assignee Claimed by @meganrogge View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.