# Crush MCP Doctor

> Use when a Crush MCP server fails to connect, shows "error" in crush_info, reports expired or invalid OAuth tokens, dies with EOF or "client is closing", or when the user asks to inspect, clean, or repair the ephemeral MCP token store in Crush's data directory.

- Skill: `detro/crush-mcp-doctor` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add detro/crush-mcp-doctor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/detro/crush-mcp-doctor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: Apache-2.0
- Author: detro (https://skillmd.com/u/detro)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/detro/crush-mcp-doctor

---


# 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_info` reports an MCP server in `error` state.
- An MCP tool call fails with an OAuth error such as `Invalid or expired refresh token`, `400 Bad Request` on token fetch, or `oauth2: cannot fetch token`.
- A stdio MCP server dies immediately with `connection closed`, `client is closing: EOF`, or `executable 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-config` skill 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:

```bash
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:

```bash
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:

```bash
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:

```bash
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 > f` truncates the input before `jq` reads it, destroying the file. `mv` within 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.

```bash
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:

```bash
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> --version` printing several candidate versions or an error naming the version manager. Fix by setting a version (load the `asdf-manager` skill for that), then re-run the probe.
- **Authentication required**: some CLIs start but immediately exit until the user logs in (e.g. a vendor `auth` subcommand).

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 `initialize` probe returning `serverInfo`.
- [ ] The user was told to restart Crush.
- [ ] `purge-all` was 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](https://github.com/charmbracelet/crush)
- [Model Context Protocol specification](https://modelcontextprotocol.io/)
- `scripts/mcp-doctor.sh` (audit, redact, purge, purge-all, probe)
- Built-in `crush-config` skill (config authoring)
- `asdf-manager` skill (version-manager shim failures)

