Claude Desktop MCP: Four False Signals When Diagnosing a Dead Plugin¶
TL;DR — Claude Desktop's MCP logs will tell you a healthy server died, hide the timestamp you need to correlate it, bury the real cause in a
[warn]you skim past, and let you "fix" a plugin without touching the copy that actually runs. Four independent false signals, all hit in one session on 2026-08-27. Each one has a specific tell.
1. Server disconnected usually means nothing died¶
This is the scary one, and it is almost always noise:
[info] Server transport closed
[info] Server transport closed unexpectedly, this is likely due to the process exiting early.
[error] Server disconnected. For troubleshooting guidance, please visit ...
The per-server log (~/Library/Logs/Claude/mcp-server-<name>.log) then stops forever — no restart line, no reinit. It reads exactly like a server that crashed and was never brought back.
It isn't. Desktop is migrating stdio MCP servers off an old launch path onto a new one. Every session start logs the tell:
When the handoff happens, the old lane's transport closes — and that lane is the thing writing the log file. The replacement process doesn't log there at all. The camera is pointed at a door the server no longer uses.
How to confirm it's a handoff, not a death — three checks, all cheap:
# 1. Is a process actually running right now?
ps -eo pid,lstart,command | grep -i "<server-script-or-binary>" | grep -v grep
# 2. Did Desktop spawn a replacement at the moment of "death"?
grep "not found in installed extensions" ~/Library/Logs/Claude/main.log
# 3. Did tool calls keep succeeding after the "disconnect"?
grep "mcp__<name>__" ~/Library/Logs/Claude/main.log
A UtilityProcess Check: Extension <name> not found in installed extensions within ~5–15 seconds of the disconnect, plus a live process whose start time matches, plus successful tool calls afterwards, means nothing was ever down.
Observed twice in one day: majorvault "died" at 08:20:38 and served journal_patch_section at 08:22:42; mail "died" at 15:59:37 and served smtp_verify_account at 15:59:53.
A related but genuinely fatal pattern — every MCP dropping at once and staying down — is claude-desktop-mcp-mass-disconnect-blocking-reboot. The distinguishing feature there is that nothing respawns.
2. The two logs use different clocks¶
This one silently manufactures false correlations.
| Log | Timezone | Format |
|---|---|---|
mcp-server-<name>.log, mcp.log |
UTC | 2026-08-27T12:20:38.636Z |
main.log |
local | 2026-08-27 12:20:37 |
Both render as "12:20" and they are four hours apart (EDT). Matching 12:20:38Z against 12:20:37 local looks like a one-second cause-and-effect relationship and is actually two unrelated events in different parts of the day.
That trap produced a completely wrong root cause on first pass — an [event-loop-stall] with real memory pressure (sys_free_raw=418MB) at 12:20 local was blamed for a server exit at 12:20 UTC, i.e. 08:20 local. The stall was innocent.
The tell: a trailing Z means UTC. The file's own mtime is in local time, so stat -f %Sm on the log is a free sanity check:
If the mtime is 08:20 and the last line says 12:20Z, you have a four-hour offset — convert before correlating anything.
Also worth knowing: very long stalls tagged [likely sleep: power_event] or [likely sleep: duration_heuristic] are just the Mac sleeping, not a hang.
3. The fatal error is a [warn], and it's in the other log¶
A plugin that fails to start logs an unhelpful error on repeat, roughly every 20–30 minutes:
Connection closed is a symptom, not a cause: the child process exited before completing the handshake. The cause is usually a [warn] a few lines away, from a different subsystem:
[warn] [PluginMcpHostConfig] Plugin "plugin_01..." server "<name>": config references
environment variables outside the MCP stdio safelist: FOO_API_KEY — left unexpanded
(the desktop host does not expose arbitrary host env to plugin servers)
Desktop will not pass arbitrary host environment variables to plugin servers. A .mcp.json written as:
receives the literal seven-character string ${FOO_API_KEY} — not an empty value, and not the real one. No amount of export, ~/.zshenv, or launchctl setenv reaches it. This is distinct from claude-desktop-remote-mcp-url-skipped, where the config shape itself is rejected.
Two fixes that work:
- Have the server read its own credentials — from an Ansible vault, the macOS Keychain, or a
chmod 600file — and drop theenvblock entirely. Preferred: no secret is duplicated, and the same code path works in Claude Code. - Or use a wrapper that sources an env file and
execs the real command, per claude-desktop-mcp-secrets-out-of-config.
And check the interpreter, not just the config. A PEP 723 header pinning nothing —
— re-resolves on every uv run. When the MCP Python SDK went to 2.x, the low-level Server dropped the list_tools/call_tool decorators, so servers written against 1.x now raise AttributeError at import, before reading a single credential. Same Connection closed, entirely different cause. Pin it: "mcp>=1.0,<2".
Run the server by hand to see the real traceback — Desktop swallows it:
4. Fixing the repo does not fix the plugin¶
The most expensive one. An MCP plugin exists as several independent copies, and repairing the one under version control changes nothing about the one Desktop launches.
Copies found for a single plugin on 2026-08-27:
| Copy | Age at discovery |
|---|---|
| The git repo (source of truth) | current |
Desktop's resolved copy under local-agent-mode-sessions/…/rpm/plugin_<id>/ |
4 weeks stale |
| A loose server script in the vault | 4 months stale |
The distributed .plugin bundle |
5 weeks stale |
An orphan .plugin bundle in another folder |
4 months stale, wrong internal layout |
Three of five were months behind. The symptom is a fix that verifies perfectly by hand and changes nothing in the app — because Desktop is running a copy from before the fix existed.
Find the copy that actually runs:
Desktop resolves plugins into ~/Library/Application Support/Claude/local-agent-mode-sessions/<space>/<id>/rpm/plugin_<plugin-id>/. Compare its .mcp.json and server script against the repo before assuming anything.
Verification rule: ✔ Connected is not proof, and neither is a green status dot. Both pass on a stale copy whose failure is at import time. Read the resolved copy's content — grep it for a string only the fixed version contains — or drive a real JSON-RPC tools/call against it.
Prevention: keep exactly one source of truth, delete every decoy copy rather than letting it rot, and treat any .plugin zip as a snapshot that begins drifting the moment it is written. Rebuild it from source on every change, or don't keep it.
Diagnostic order¶
When a Desktop MCP plugin looks dead, work in this order — cheapest and most likely first:
- Is it actually dead?
psfor the process; look for a spawn matching the "disconnect" time. (§1) - Convert timestamps before correlating
main.logagainst the MCP logs. (§2) - Read the
[warn]lines, not just the[error]ones —PluginMcpHostConfignames the real problem. (§3) - Run the server by hand to surface the traceback Desktop hides. (§3)
- Diff the copy Desktop runs against your source before believing any fix landed. (§4)
Related¶
- claude-desktop-mcp-mass-disconnect-blocking-reboot — the genuinely fatal all-MCP drop, and how it differs
- claude-desktop-remote-mcp-url-skipped — Desktop silently skipping non-stdio server configs
- claude-desktop-mcp-secrets-out-of-config — wrapper +
0600env file pattern for MCP secrets - concurrent-claude-sessions-clobber-vault-journal — another "it looked like it worked" failure mode