MCP connection failures cancel the agent turn as if the user pressed Stop
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
## Summary
An in-flight MCP tool call can terminate an entire Copilot agent turn when the MCP connection fails, even though the caller's cancellation token was not canceled. The UI shows `Canceled: Canceled`, and Copilot classifies the failed invocation as `The user cancelled the tool call.`
Observed with VS Code 1.136.1 and the bundled Copilot extension 0.64.1. The same tool-boundary behavior is reproducible in tests on current main at `b376c21d3e45b4d4fac2e084efc93696f1a733cd`.
## Observed sequence
I matched the failed tool invocation in the persisted chat to its MCP connection log. There were two failure shapes:
* An HTTP MCP transport returned 502 and entered `Error` while `tools/call` was outstanding.
* A direct HTTP MCP server could no longer obtain authentication silently and entered `Stopped`. Its failed tool result was likewise classified as user cancellation.
In both cases the task ended without the model getting another opportunity to respond to the failure. Sending a new user message resumed work.
Server names, internal queries, credentials, tenant IDs, and private logs are omitted. The underlying transport/authentication failure is not the subject of this issue.
## Reproduction
A deterministic reproduction does not require a real authenticated server:
1. Start an MCP server advertising a tool.
2. Invoke that tool with an uncanceled caller token, leaving the request outstanding.
3. Transition the transport to either `Error` or `Stopped`.
4. Observe that the public MCP tool-call promise rejects with `CancellationError`, indistinguishable from user cancellation.
The test fixtures `TestMcpRegistry` and `TestMcpMessageTransport` can drive this through the real `McpService`, `McpServer`, `McpTool`, request handler, and JSON-RPC implementation.
## Code path
* `src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts` deliberately calls `cancelAllRequests()` when the transport enters `Error` or `Stopped`.
* `JsonRpcProtocol.cancelAllRequests()` cancels the pending promises.
* `McpTool._callWithProgress()` in `mcpServer.ts` currently rethrows that cancellation after its existing retry checks.
* `toolCallErrorToResult()` in `extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx` interprets any cancellation exception as user cancellation.
* `ToolCallingLoop.buildPrompt2()` detects a canceled tool result and throws cancellation; the outer loop exits instead of sending the tool failure back to the model.
Canceling pending RPCs on disconnect is an existing, explicitly tested low-level contract. The missing distinction is at the public MCP tool-call boundary.
## Expected
If the caller actually canceled, preserve cancellation immediately.
If only the connection failed or stopped, expose an ordinary connection/tool error instead. For a stopped connection with reason `needs-user-interaction`, retain an actionable indication that interaction is required.
This should not bypass authentication, automatically retry arbitrary tools, or convert a server's intentional cancellation while its connection is still running.
Related: #280203 and the closed retry proposal #280215. This report is narrower: preserve the distinction between connection failure and user cancellation, without adding a new retry policy.
Contributor guide
Assessment
This issue has not been assessed yet.