feat(claude): register the MCP server in user scope by default

`config claude` wrote a per-directory `.mcp.json`, so you had to re-run it in
every checkout you opened — and in a repo that commits `.mcp.json` (this one
does) it dirtied the working tree. Every other integration is already global:
pi, prime-agent and opencode each have one active project, wired once.

Claude Code's user scope is `mcpServers` in `.claude.json`, which applies in
every directory and window. That is now the default. `--scope project`, or an
explicit `-o/--output`, keeps the old per-directory file for a repo that wants
its own pinned project. `--inspect` stays project-scope — it is a debugging
server you turn on for one checkout.

Details worth knowing:

  - The file path is asymmetric: `$CLAUDE_CONFIG_DIR/.claude.json` when that is
    set, but `$HOME/.claude.json` by default — beside `~/.claude/`, not inside
    it. Verified against a live Claude Code run with an isolated config dir.
  - `.claude.json` also holds onboarding state, caches and a per-project map
    that Claude Code rewrites while running, so this merges into the document
    and writes through a temp file + rename.
  - User scope writes no `.mcpctl-project` marker: it scopes nothing to a
    directory, and a marker beside `.claude.json` would sit in $HOME and scope
    every repo under it.
  - `statusline` now resolves directory-scoped `.mcp.json` first (a repo that
    pinned itself wins), then user scope, then the marker.

Scope selection reads Commander's option source rather than process.argv —
argv is the test runner's command line when the command is driven in-process,
which the suite caught immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
Michal
2026-08-09 19:59:48 +01:00
parent 834aa704ed
commit b7c0de2bf0
8 changed files with 299 additions and 26 deletions

View File

@@ -4,7 +4,7 @@
| Piece | Where | Purpose |
|-------|-------|---------|
| MCP entry | `./.mcp.json` | one server, always named `mcpctl`, running the stdio bridge `mcpctl mcp -p X` |
| MCP entry | `~/.claude.json` (**user scope, every directory**) | one server, always named `mcpctl`, running the stdio bridge `mcpctl mcp -p X` |
| Project marker | `./.mcpctl-project` | what `skills sync` reads to know the scope (`--skip-marker` opts out) |
| Skills | `~/.claude/skills/` | the project's `SKILL.md` bundles |
| SessionStart hook | `~/.claude/settings.json` | `mcpctl skills sync --quiet` on every session |
@@ -14,6 +14,37 @@
`--claude-dir` (or Claude Code's own `CLAUDE_CONFIG_DIR`) redirects everything
under `~/.claude`.
## User scope by default: wire it once, not per checkout
Claude Code has two MCP scopes: **project** (`./.mcp.json`, that directory only,
and usually committed — so writing to it dirties the repo) and **user**
(`mcpServers` in `.claude.json`, every directory and every window).
mcpctl now defaults to **user** scope. One active project everywhere is how the
pi, prime-agent and opencode integrations already behave; per-directory wiring
meant re-running `config claude` in every checkout you opened. Switching the
project is then one command from anywhere.
```bash
mcpctl config claude --project homeautomation # applies everywhere
mcpctl config claude --project docmost --scope project # just this repo
mcpctl config claude --project docmost -o .mcp.json # same thing; -o implies project scope
```
User scope writes **no `.mcpctl-project` marker** — it scopes nothing to a
directory, and a marker beside `.claude.json` would sit in `$HOME` and scope
every repo under it. `--inspect` stays project-scope: it is a debugging server
you turn on for one checkout.
> `.claude.json` also holds onboarding state, caches and a per-project map that
> Claude Code rewrites while running, so mcpctl merges into it and writes
> through a temp file + rename. A truncated write there costs far more than a
> stale MCP entry.
>
> Note the path asymmetry: with `CLAUDE_CONFIG_DIR` set the file is
> `$CLAUDE_CONFIG_DIR/.claude.json`, but by default it is `$HOME/.claude.json` —
> *beside* `~/.claude/`, not inside it.
## One server named `mcpctl`, not one per project
The MCP entry used to be named after the project. Because `.mcp.json` is
@@ -70,8 +101,9 @@ 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 `.mcp.json`, falling back to a
`.mcpctl-project` marker up the tree so a checkout that is scoped but not yet
`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