fix(claude): stop pre-migration .mcp.json residue outranking a project switch

`mcpctl config claude --project X` writes user scope and never rewrites a
checkout's `.mcp.json`. The status line, however, preferred that file
unconditionally — so a legacy project-named entry an older mcpctl left behind
(`homeautomation` -> `mcpctl mcp -p homeautomation`) kept naming the old project
for good, and every switch looked like it had done nothing.

Reproduced live: with user scope on `sre`, `mcpctl statusline --directory
~/developer/michalzxc/claude/debug` printed `mcpctl:homeautomation` — a project
Claude Code also had in `disabledMcpServers` for that directory, so the line
named a server that was not even mounted.

Rank the sources by how deliberate each one is instead: a canonical `mcpctl`
pin, then user scope, then legacy residue, then the marker. A pin is a decision
and still wins; residue is not and no longer does. At every step, skip a server
Claude Code has switched off for that directory.

`config claude` now also warns when the working directory's `.mcp.json`
contradicts the switch, naming the file — the two scopes are merged rather than
chosen between, so nothing else would tell you.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wUmrfkVQR6CKcYKxENq7k
This commit is contained in:
Michal
2026-08-10 12:15:46 +01:00
parent 2b87cfdbf1
commit 2a7bba11ea
7 changed files with 414 additions and 31 deletions

View File

@@ -101,13 +101,48 @@ to open, and re-scoping it would silently change which skills sync into it.
⏵⏵ bypass permissions on · ← for agents
```
`mcpctl statusline` resolves the project from a directory-scoped `.mcp.json`
first (a repo that pinned itself wins), then the user-scope entry in
`.claude.json`, then a `.mcpctl-project` marker up the tree so a checkout that is scoped but not yet
wired still reports. It reads the directory from the JSON Claude Code pipes in,
so it follows `/cwd` rather than reporting wherever the binary was launched, and
prints **nothing** when no project is active — an empty status line beats one
saying "none" on every unrelated repo.
`mcpctl statusline` reads the directory from the JSON Claude Code pipes in, so it
follows `/cwd` rather than reporting wherever the binary was launched, and prints
**nothing** when no project is active — an empty status line beats one saying
"none" on every unrelated repo.
It then takes the project from the most deliberate source that names one:
1. a canonical `mcpctl` entry in that directory's `.mcp.json` — a repo that
pinned itself wins, and it is the scope Claude Code itself prefers when both
define that server name;
2. the user-scope entry in `.claude.json` — what `config claude --project`
writes, so a switch takes effect everywhere it is not overridden;
3. a **legacy** project-named entry in `.mcp.json` (`homeautomation`,
`docmost`, …), left by an mcpctl older than the constant server name;
4. a `.mcpctl-project` marker up the tree, so a checkout that is scoped but not
yet wired still reports.
> **Legacy entries rank below user scope on purpose.** They used to outrank it,
> which made switching look broken: a user-scope switch never rewrites a
> checkout's `.mcp.json`, so the leftover kept naming the old project for good.
> A pin is a decision; residue is not.
A server Claude Code has switched off for that directory (`disabledMcpServers` /
`disabledMcpjsonServers`) is skipped at every step — a disabled server is not
mounted, so naming its project would be a lie. A `.mcp.json` server that is in
neither list is still awaiting its approval prompt and does count, since blanking
the status line on a fresh checkout is the more confusing failure.
### When a directory contradicts a switch
Claude Code merges the two scopes rather than picking one, so switching in user
scope cannot clean up what a directory declares. `config claude` says so rather
than reporting plain success:
```
Warning: /path/to/repo/.mcp.json still registers 'homeautomation' for this
directory — mounted alongside 'sre', not replaced by it.
Re-run with --scope project to retire it, or delete the entry by hand.
```
A canonical entry pinned to another project gets the stronger wording — it
*overrides* the switch in that directory rather than sitting beside it.
### It is never installed over yours