Skip to content

MCP: startup connects only 3 of 18 servers and never retries; reloads destroy live handles mid-session (NativeMcpHostHandle has been destroyed) #4598

Description

@doomslayer2k

Describe the bug

MCP servers are frequently missing from a session, and the ones that are present can vanish mid-session.

Two user-visible symptoms, which turn out to be the same subsystem:

  1. Servers silently never connect. A session starts with a fraction of the configured MCP servers — in my case 3 of 18 — with no error, no warning, and no retry. The missing tools simply do not exist as far as the agent is concerned. The runtime reports MCP loading as complete ~9 s after start and then never revisits it. One of my runtimes has now sat at 3/18 for over 25 minutes.

  2. Servers disappear mid-session. Tool calls that worked a minute ago start failing with:

    MCP server '<name>': Error: NativeMcpHostHandle has been destroyed
    

    or the tool silently drops out of the available set. This happens while the session is otherwise healthy, and it is triggered by activity in a different session.

The practical effect is that MCP is unreliable in a way the user cannot see, diagnose, or fix — there is no error surfaced and no supported way to force a reload (see #505 on the app tracker).

Affected version

  • Copilot CLI 1.0.80 (copilot --version), running as the engine inside GitHub Copilot desktop app 1.1.12
  • Windows 11 Enterprise 25H2 (build 26200.9106), AMD64

Full environment block below.

Steps to reproduce the behavior

Reproduces most reliably with a large MCP server set (mine merges to 18 across user config, repo .mcp.json, and plugin configs). Server names below are redacted; identities are irrelevant to the failure.

Symptom 1 — incomplete startup:

  1. Configure a large number of MCP servers (≈15+) across several sources.
  2. Start a session in a working directory that has no existing runtime process (a fresh clone, a new git worktree, or a forked session — anything with a new cwd).
  3. Wait. Do not interact.
  4. Observe: only the first handful of servers ever connect. Enable debug logging and grep for session.mcp_servers_loaded — it fires once, seconds after start, with only those few connected.
  5. There is no retry. The remaining servers stay missing indefinitely.

Symptom 2 — mid-session teardown:

  1. With the above session running, cause the app to subscribe to MCP status from any session sharing that runtime (opening MCP settings does this).
  2. ~25 s later the runtime tears down every MCP connection and respawns all of them.
  3. Any tool call landing in that window fails with NativeMcpHostHandle has been destroyed, or the tool is missing from the tool list entirely.

Expected behavior

  • MCP startup should not report completion while configured servers are still unconnected. Servers that miss the initial window should be retried, or their failure surfaced to the user.
  • A status subscription should not tear down live MCP connections, and certainly should not do so for sessions that did not request it.
  • If a reload is genuinely required, in-flight tool calls should get a retriable error, not a destroyed handle.

Additional context

Environment

App version         1.1.12
OS                  Windows 10 Enterprise 25H2 (build 26200.9106)
Architecture        AMD64
WebView2 runtime    151.0.4129.107
Copilot CLI         1.0.80
Agency              2026.8.24.6
GPU 0               Intel(R) Graphics (32.0.101.8132)
GPU 1               NVIDIA GeForce RTX 5090 (32.0.16.1088)
WebView2 flags      --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection,CalculateNativeWinOcclusion,UiaProvider
                    --force-renderer-accessibility

Frequency

Always, on this configuration. Every runtime I inspected showed an incomplete first generation. The mid-session teardown fired 3 times in 11 minutes in one runtime.

Extensions

Not tried with extensions/plugins disabled — but the failure is in MCP client startup and occurs before any agent turn, and two of the three config sources are plugin-independent. Happy to test a clean profile if useful.

Suggested labels

area:mcp (or nearest equivalent), type: Bug. I can't set labels as an outside contributor.

Related issues

  • github/copilot-cli#4392 — same teardown-and-rebuild mechanism, but reported for the orphaned process leak it causes. This report is about two different consequences of that rebuild: the incomplete first generation, and the cross-session trigger.
  • github/copilot-cli#4542 — workspace .mcp.json detected but not connected. Different layer (config plumbing vs. startup race), same user-visible outcome of "configured server is missing".
  • On the app tracker, where several reports of this landed because the symptom appears in the app UI: github/app#2411 (the exact NativeMcpHostHandle has been destroyed string, open, no maintainer response), github/app#2139 (stale binding, mcp_stale=true, observed_session_mcp=0), github/app#1012 (spawn deadline too tight with many servers), github/app#505 (no way to reload MCP).
In-depth investigation

All timestamps UTC, from --log-level all. Paths, usernames and internal server names redacted.

Setup. MCP fleets are owned by the runtime process, which the app pools per working directory (cli_pool: spawned new CLI process for pool working_directory=…). All sessions sharing a cwd share one MCP fleet; a new cwd cold-starts the whole fleet. Three live runtimes were observed, one per cwd.

An enterprise wrapper merges six config sources before handing the runtime a single merged set:

Loaded 3 MCP server(s) from plugin <redacted>
Merging built-in MCPs → 3 server(s) total
Loaded 13 MCP server(s) from <user>/.copilot/mcp-config.json
Skipping default MCP '<redacted>' from user config — wrapper manages default MCPs
User global MCP config merged; 14 server(s) total
Loaded 7 MCP server(s) from <repo>/.mcp.json
Repo-local MCP configs merged; 17 server(s) total
Plugin MCPs merged; 18 server(s) total
MCP source merge complete; 18 server(s) total

Eight of those are additionally fronted by localhost HTTP proxies on sequential ports.

Finding 1 — startup finalizes early and never retries.

On the runtime with the new cwd, 18 servers were in the merged config, yet:

16:22:43.527  All 1 configured MCP registry/registries permit all servers (allow_all)
16:22:43.645  MCP server stderr {"server_name":"<a>", …}
16:22:43.645  MCP server stderr {"server_name":"<b>", …}
16:22:43.645  MCP server stderr {"server_name":"<c>", …}
16:22:44.827  [rust:rmcp::service] Service initialized as client
16:22:44.869  [rust:rmcp::service] Service initialized as client
16:22:48.026  [rust:rmcp::service] Service initialized as client
16:22:48.028  session.mcp_servers_loaded (ephemeral)
16:22:48.030  session.mcp_server_status_changed (ephemeral)

Exactly three Service initialized as client events, then mcp_servers_loaded — 9 s after start. Grepping the whole runtime log confirms only 3 distinct server_name values ever appear, and only 9 lines total from the copilot_runtime::mcp module.

Two checks that rule out the obvious explanations:

  • The other servers were reachable. The 8 HTTP proxies were listening the entire time (verified with a port check). The runtime never connected to any of them — filtering the log for those ports yields zero hits, while an unrelated internal port shows 138.
  • It is not slow startup. That runtime has remained at 3/18 for 25+ minutes with no further attempts. There is no retry path.

Not a policy block either — the registry explicitly logged allow_all.

Finding 2 — a "reload" is a full teardown, not a top-up.

On a second runtime, a reload looks like this:

16:11:50.050  Loaded workspace MCP config from <repo>/.mcp.json
16:11:50.050  Loaded MCP config … (project): 7 server(s)
16:11:50.055  Loaded MCP config from plugin-dir plugins: 3 server(s)
16:11:50.475  [rust:rmcp::service] input stream terminated
16:11:50.475  [rust:rmcp::service] serve finished {"quit_reason":"Closed"}
16:11:50.515  [rust:rmcp::service] task cancelled
16:11:50.515  [rust:rmcp::service] serve finished {"quit_reason":"Cancelled"}
16:11:50.846  MCP server stderr {"server_name":"<redacted>", …}   ← respawn begins

Every live connection is closed and every server respawned. A tool call landing in that window is what produces NativeMcpHostHandle has been destroyed — the handle is genuinely gone, which is why it is not retriable from the caller's side.

Finding 3 — the trigger is subscribe_mcp_status, from an unrelated session.

Correlating the app's websocket messages against the runtime's MCP generations:

subscribe_mcp_status resulting mcp_servers_loaded
16:11:35 16:12:00
16:18:04 16:18:30
16:19:18 16:19:44

Three subscriptions, three full teardown/respawn cycles, ~25 s lag, no other correlated events. The subscription came from a different session id than the one whose tools broke — the runtime is shared per cwd, so the blast radius is every session on it.

Cost. That single runtime performed 51 Service initialized as client operations across 4 generations in 11 minutes:

Generation Time Clients initialized
1 16:08:47 3
2 16:12:00 16
3 16:18:30 16
4 16:19:44 16

Note generation 1 shows the same 3-server under-load as Finding 1 — the incomplete first generation appears to be systematic, and the only reason that runtime recovered is that an unrelated status subscription happened to force a rebuild later.

Why this is user-hostile in combination: the first generation is silently wrong, there is no retry, there is no user-facing reload (github/app#505), and the only thing that fixes it is an unrelated side effect. Users experience this as "MCP randomly doesn't work", and the natural workaround — restart the app — often lands on another incomplete first generation.

Suggested fixes, in rough order of value:

  1. Do not emit mcp_servers_loaded until every configured server has either connected or failed; surface failures.
  2. Retry (with backoff) servers that miss the initial window, instead of dropping them permanently.
  3. Make a status subscription read-only — it should not be able to recycle live connections.
  4. If a rebuild is unavoidable, reuse still-healthy connections rather than tearing down all of them (this would also fix the process leak in #4392).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions