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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user