Skip to content

A tools/list refresh dispatched into a server still blocked by a just-cancelled tool call times out and permanently strips that server's tools for the life of the process #4731

Description

@tecrogue

Describe the bug

When a tool call to a stdio MCP server hits the client-side request timeout, the runtime immediately dispatches a tools/list refresh into the very server it just abandoned. That server is still occupied by the cancelled work and does not answer, so the refresh also times out — at a separate 60 s budget.

The catalog then drops that server's tools. Despite the warning text, the previously-listed tools are not retained:

[rust:rmcp::service] cancelled {"id":"6","reason":"request timeout"}
[rust:rmcp::service] cancelled {"id":"7","reason":"request timeout"}
[rust:copilot_runtime::session::mcp::tool_catalog] Failed to list MCP tools for fabric-rti-mcp;
    continuing with the previously listed tools: MCP request timed out after 60000 ms

Every subsequent call to that server returns instantly:

Tool 'fabric-rti-mcp-kusto_query' does not exist.

The failure is server-scoped: the warning names the server rather than a tool, and afterwards the agent reported no tool from that server present in its available-tools list. There is no re-list and no backoff, so the eviction is permanent for the process. The MCP server process itself stays alive and healthy the whole time — only the runtime's view of it is destroyed. The only recovery observed was restarting the session.

Two things make this expensive in practice:

  1. The agent cannot tell this apart from a tool that was never available, so it "helpfully" falls back to worse strategies instead of reporting a transport fault. In the affected session the agent burned ~11 minutes across two occurrences retrying and sleeping — neither of which can ever work.
  2. It needs a perfectly healthy server. This is not a startup or handshake failure. The server had listed successfully and answered a query 66 seconds before the drop.

The timeout alone is not the bug — the stacked refresh is

Same session, same server, same 180 s client timeout, 11 -32001 timeouts total:

  • 9 survivable (Aug 26–28) — none caused any tool loss; the server kept serving (some later calls errored or timed out again, but the tools stayed registered throughout).
  • 2 fatal (Sep 4, 1.0.83-2) — both had a tools/list refresh stack on top.

And on the fatal path the refresh is provably a consequence of the cancel, not a coincidence — it is the next sequential request id, dispatched within milliseconds:

Occurrence tool call cancelled tools/list cancelled delta
1 id 6 22:52:24.336Z id 7 22:53:24.349Z 60.013 s
2 id 15 23:10:03.568Z id 16 23:11:03.573Z 60.005 s

Controlled isolation: only the client-side cancel is fatal

Every call to this server after a clean restart, in order:

# Duration Outcome Tools after
1 10.4 s server-side error (cold auth) ✅ retained
2 3.8 s OK (print warm=1) ✅ retained
3 17.1 s OK — real query ✅ retained
4 0.7 s server-side error (bad KQL) ✅ retained
5 6.4 s server-side error (memory budget) ✅ retained
6 180.0 s client-side timeout 💀 all tools evicted
7–9 ~0 ms Tool … does not exist 💀

Three server-side errors left the catalog intact. The one client-side cancel destroyed it. The trigger is specifically the client abandoning an in-flight request — not the upstream service failing.

Note that call #6's payload was 606 characterssmaller than three calls that were fine. Payload size is irrelevant; what matters is that the server was mid-work when the client gave up.

The server is still alive after its tools are gone

ps 12 minutes after the eviction shows the stdio child still running (PID 26530, spawned 23:04:44), wedged on the abandoned request. It is not a crash, and it is not a transport close — the runtime simply has no path back to a server it is still connected to.

Affected version

GitHub Copilot CLI 1.0.83-2.

Steps to reproduce the behavior

  1. Configure any stdio MCP server that can take a long time to answer a single tool call (encountered with uvx microsoft-fabric-rti-mcp issuing Kusto queries; anything that can block for >180 s works).
  2. Confirm its tools are listed and working — call something trivial and get a result.
  3. Issue one tool call that will not return within the client timeout (~180 s).
  4. Observe the call fail with MCP error -32001: Request timed out.
  5. Watch the log: a tools/list for the same server is dispatched immediately and dies 60 s later, followed by Failed to list MCP tools for <server>.
  6. Call any tool on that server — including the trivial one from step 2. It returns Tool '<name>' does not exist. instantly.
  7. Wait and retry (the agent tried 20 s / 30 s / 40 s / 45 s / 50 s). The tools did not come back. Only restarting the session recovered them.

Reproduced twice within 22 minutes, the second time in a freshly restarted runtime process.

Expected behavior

  1. A failed tools/list refresh should genuinely retain the previously-listed tools, as the warning already claims. Today the fallback path evicts them, so the message and the behaviour disagree.
  2. Don't dispatch tools/list into a server known to be blocked. The runtime has just cancelled a request against that server for timing out; sending the refresh immediately into the same serial stdio pipe is close to guaranteed to time out too. Defer it, or wait for the server to drain.
  3. Retry the refresh with backoff. A single transient failure should not be terminal for the life of the process, and there is currently no in-session way to recover (no re-list, no reconnect).
  4. If tools genuinely must be dropped, surface it to the user and the model as a transport fault, distinctly from "this tool does not exist". Right now the agent is told the tool never existed and silently degrades.

Additional context

Environment — this is the VS Code Insiders Agent window, not the terminal TUI. Reproducing by running copilot in a shell may not exercise the same path; no copilot invocation is involved.

Host:        VS Code Insiders — Agent window (Agent sessions)
             client_name = vscode-agent-host · producer = copilot-agent
Runtime:     @github/copilot-darwin-arm64 1.0.83-2, spawned headless by VS Code
             (--headless --no-auto-update --stdio --no-auto-login)
MCP config:  workspace .vscode/mcp.json
             (~/.copilot/mcp-config.json does not exist on this machine)
MCP server:  fabric-rti-mcp — stdio, `uvx microsoft-fabric-rti-mcp`
OS:          macOS (darwin-arm64)

The MCP config source may matter: the server is declared only in the workspace .vscode/mcp.json, so reproducing from a user-scope CLI config may exercise a different path.

Possible regression. The tool_catalog warning appears only in the three newest runtime logs on this machine (Sep 3 21:21 onward), which are also the only ones whose startup banner carries the [plugin-dir] line. The older logs cover the same MCP server and the same 180 s timeouts — 9 of them — with no such warning and no tool loss. Correlation only; the responsible change is unconfirmed.

Workaround for anyone hitting this with a Kusto-backed server. Since server-side errors are harmless and only the client cancel is fatal, make the server give up first:

client_request_properties = {"servertimeout": "00:02:00"}

A 120 s server timeout under the 180 s client ceiling converts the fatal case into a normal error and keeps the tools registered. That's server-specific, but the general shape — ensure the server always answers before the client gives up — should apply to any stdio MCP server.

Related

Three orphaned MCP server processes reparented to PID 1 were also observed on this machine, but that's already covered by #4697 / #4392 / #4461 and isn't part of this report.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registryarea:toolsBuilt-in tools: file editing, shell, search, LSP, git, and tool call behavior

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions