Crush MCP Doctor
Scope: Crush only (
charmbracelet/crush). This skill diagnoses and repairs failing MCP server connections by classifying the failure mode, then applying the narrowest fix for that mode.
Diagnose a broken MCP server in Crush, classify it as an OAuth/token failure or a transport/command failure, apply the matching fix, and verify it before handing control back to the user.
The core idea: Crush keeps two distinct kinds of MCP state in two distinct places. Declarative definitions (transport, URL, command, args) live in the user's config, while ephemeral runtime state (OAuth access and refresh tokens) lives in the data directory. Fixing the wrong one wastes time, so classification comes before any mutation.
When to Use
crush_inforeports an MCP server inerrorstate.- An MCP tool call fails with an OAuth error such as
Invalid or expired refresh token,400 Bad Requeston token fetch, oroauth2: cannot fetch token. - A stdio MCP server dies immediately with
connection closed,client is closing: EOF, orexecutable file not found. - The user asks to audit, list, or clean stored MCP tokens and their expiry times.
- The user explicitly asks to be logged out of all MCP servers, or to wipe every stored token and re-authenticate from scratch.
- The user asks why one MCP server works while another does not.
When Not to Use
- Adding, renaming, or removing an MCP server definition, or any other config authoring task. Load the built-in
crush-configskill instead, since it owns config syntax and merge semantics. - Diagnosing MCP clients other than Crush (Claude Code, Cursor, Gemini CLI). Their token stores and config layouts differ.
- Debugging the MCP server's own business logic once the connection is healthy.
- Wiping all tokens as a diagnostic guess. Global logout is a user-requested operation, never a repair strategy chosen by the agent; see Step 4b.
Inputs
| Input | Required | Description |
|---|---|---|
| Server name | No | Name of the failing MCP server as shown in crush_info (e.g. Atlassian). If omitted, audit every server. |
| Global logout request | No | An explicit user request to log out of every MCP server. Without it, never run purge-all. |
| Fix consent | Yes, before mutating | Explicit user approval before deleting tokens or changing tool versions, because both alter state outside the current project. |
Helper Script
The mechanical parts of this workflow are implemented in
scripts/mcp-doctor.sh, so that the agent spends its attention on
classification rather than on re-deriving fragile shell one-liners. The script
is local-only (no network) and exposes five subcommands:
| Subcommand | Purpose |
|---|---|
audit [data-dir] |
List servers holding tokens with humanised expiry and a valid/expired status |
redact [data-dir] |
Print the ephemeral store with secret values replaced by REDACTED |
purge <server> [data-dir] |
Back up the store, then delete one server's token entry and restore mode 600 |
purge-all --yes-i-mean-it [data-dir] |
Back up the store, then delete every token. User-requested only; see Step 4b |
probe -- <command> [args...] |
Send a JSON-RPC initialize to a stdio server and report whether it answers |
Exit codes: 0 success, 1 usage, 2 missing dependency, 3 store missing
or invalid JSON, 4 server not in store or nothing to purge, 5 probe failed,
6 destructive operation refused. Non-zero codes let the agent branch without
parsing prose.
When [data-dir] is omitted the script falls back to $CRUSH_DATA_DIR, then
$XDG_DATA_HOME/crush, then ~/.local/share/crush. Pass the path resolved
from crush_info whenever you have it: the fallback is a convenience for
quick audits, not a substitute for knowing where Crush actually stores state.
Key Locations
Crush splits MCP state across two locations. Resolve both before diagnosing,
and never assume the paths, because they vary by platform and by
XDG_*/CRUSH_* environment overrides:
| What | Typical path | Contains |
|---|---|---|
| Declarative config | ~/.config/crush/crushrc, crush.json, or project-level crush.json |
mcp add declarations: transport type, URL, command, args, --oauth flag |
| Ephemeral state | <data-dir>/crush.json, commonly ~/.local/share/crush/crush.json |
.mcp.<Name>.oauth_token objects with access_token, refresh_token, expires_at, and client metadata |
Run crush_info first: its [config_files] section lists the exact config
files in play, and the data directory is discoverable from the data_directory
option or by listing the parent of the project's .crush folder. Deriving paths
from the tool output rather than hardcoding them keeps the skill correct across
machines and Crush versions.
Workflow
Step 1: Snapshot current state
Call crush_info and read the [mcp] section. Record, for each server, whether
it is connected or in error, and capture the verbatim error string.
The error text is the primary classification signal, so preserve it exactly
rather than paraphrasing. oauth2: cannot fetch token points at the token
store; client is closing: EOF points at the process that never started.
Step 2: Read the declarative definition
Grep the config files reported by crush_info for the failing server name to
learn its transport:
grep -n -i -A6 '<server-name>' <config-file>
Knowing whether the server is --type http --oauth true or
--type stdio --command <bin> determines which branch below applies. An HTTP
server has no local process to break; a stdio server has no stored token to
expire. Classifying here prevents pointless edits.
Step 3: Audit the ephemeral token store (OAuth servers)
List the servers that currently hold tokens and check their expiry:
scripts/mcp-doctor.sh audit <data-dir>
The script emits only server names, statuses, and timestamps. It never prints
token material, because transcripts are frequently logged, shared, or replayed.
It also handles the BSD/GNU date split (date -r vs date -d @) by probing
both, instead of branching on uname, which is wrong when GNU coreutils are
installed on macOS.
Interpret the result carefully. An expired access_token alone is normal and
self-healing, because Crush will silently refresh it. The failure is real only
when the refresh also fails, which the crush_info error string will say
(for example Invalid or expired refresh token). Deleting a token that would
have refreshed fine costs the user an unnecessary browser login.
If you need to inspect the file's shape, dump a redacted copy:
scripts/mcp-doctor.sh redact <data-dir>
This redacts by key name using jq, not by sed pattern. macOS sed has no
\| alternation in basic regex, so the obvious one-liner silently redacts
nothing there; key-based matching also catches secret fields added by future
Crush versions. URL-valued keys such as token_url are deliberately preserved,
since they are diagnostic rather than sensitive.
Step 4: Repair an OAuth failure
Only when the refresh token is confirmed dead, and only after the user consents:
scripts/mcp-doctor.sh purge <ServerName> <data-dir>
The script encapsulates three details that are easy to get wrong by hand:
- Back up first. The same file holds still-valid tokens for other servers; a botched edit logs the user out of everything. The backup path is printed so it can be restored.
- Write to a sibling temp file, then
mv.jq 'x' f > ftruncates the input beforejqreads it, destroying the file.mvwithin the same directory is atomic. - Restore mode
600. The store holds bearer credentials, but a file created by shell redirection inherits the umask and may be world-readable.
It exits 4 without touching anything if the named server has no stored token,
which guards against a typo silently "succeeding".
Deleting the entry (rather than editing fields) is deliberate: it returns the server to the "never authenticated" state, which is the only state that reliably triggers Crush's interactive OAuth flow on next start.
Step 4b: Global logout (only when the user asks for it)
This branch is not part of diagnosis. Enter it only when the user has asked, in their own words, to be logged out of every MCP server, to wipe all stored tokens, or to re-authenticate everything from scratch. Do not reach for it because a single server is misbehaving: it costs the user one interactive browser login per OAuth server, and it destroys working credentials that had nothing to do with the fault. If the trigger was an error rather than a request, go back to Step 4 and purge the single offending server.
Before running it, state plainly how many servers hold tokens (from audit) and
get explicit confirmation, so the user is agreeing to a known cost rather than a
vague one.
scripts/mcp-doctor.sh purge-all --yes-i-mean-it <data-dir>
The --yes-i-mean-it flag is mandatory and the command exits 6 without it.
That guard exists precisely so an agent improvising a repair cannot stumble into
a global logout; the flag has to be typed deliberately, which makes the intent
auditable in the transcript.
The store is backed up first (path is printed), the mcp map is emptied rather
than deleted so the file shape stays stable, and mode 600 is re-applied. It
exits 4 if no tokens were stored, so a redundant run is a no-op rather than a
silent success.
Then continue to Step 6: nothing takes effect until Crush restarts.
Step 5: Repair a stdio/transport failure
A stdio server fails when its command cannot start. Reproduce it directly, outside Crush, so you see the real error instead of a truncated transport message:
scripts/mcp-doctor.sh probe -- <command> <args...>
A healthy server replies with a JSON-RPC result containing serverInfo; the
script exits 0 in that case and 5 otherwise, printing the raw output plus
the resolved PATH entry for the binary. Anything other than serverInfo,
including silence, is the actual fault. Common causes:
- Binary missing from
PATH: the MCP server tool is not installed. - Version manager shim with no version selected: with
asdf/mise,which <bin>resolves to a shim, but the shim exits non-zero because no version is set for the current directory or globally. The tell-tale sign is<bin> --versionprinting several candidate versions or an error naming the version manager. Fix by setting a version (load theasdf-managerskill for that), then re-run the probe. - Authentication required: some CLIs start but immediately exit until the user logs in (e.g. a vendor
authsubcommand).
Fix the underlying tool, then re-run the probe until it exits 0.
Verifying outside Crush is important because Crush only surfaces EOF, which
looks identical for all three causes above.
Step 6: Verify and warn about the restart
Re-run the stdio probe and/or re-check expiry timestamps to confirm the fix at the filesystem/process level.
Then tell the user plainly: the running Crush session must be restarted. The running process holds the MCP state in memory and may rewrite the ephemeral file on exit, silently resurrecting the deleted token. Only after a restart will Crush re-run the OAuth flow or re-spawn the repaired stdio command. Skipping this warning is the single most common way this repair appears to "not work".
Validation
- The failing server was classified as OAuth or stdio before any file was modified.
- Config paths and the data directory were resolved from
crush_info, not hardcoded. - No token value was printed in the conversation, only names and expiry times.
- A backup of the ephemeral store exists before any deletion.
- The ephemeral store is still valid JSON and other servers' tokens survive.
- File permissions on the ephemeral store are still owner-only (
600). - A stdio server was proven healthy with a manual
initializeprobe returningserverInfo. - The user was told to restart Crush.
-
purge-allwas run only because the user explicitly asked for a global logout, never as a diagnostic guess.
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Deleting a token that only had an expired access token | Check the error string: only delete when the refresh is rejected. Expired access tokens refresh automatically. |
Assuming ~/.local/share/crush/crush.json |
Resolve the data directory from crush_info; it moves with XDG_DATA_HOME and platform. |
| Editing the ephemeral store to add a server | Server definitions belong in crushrc/crush.json; use the crush-config skill. |
which <bin> succeeds but the server still dies |
The path is a version-manager shim. Run <bin> --version|-v (or <bin> version|ver) and check asdf current <bin> / mise current; set a version and reshim. |
| Declaring the fix successful in the same session | Crush caches MCP state in memory and can overwrite the file on exit. Always instruct a restart. |
| Wiping all tokens to "reset" one broken server | Purge the single server (Step 4). purge-all is a user-requested global logout, costing one browser login per server. |
References
- Crush repository
- Model Context Protocol specification
scripts/mcp-doctor.sh(audit, redact, purge, purge-all, probe)- Built-in
crush-configskill (config authoring) asdf-managerskill (version-manager shim failures)