refactor(prime-agent): extract the /mcpctl switcher to typechecked source; pi --dry-run; docs
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 2m1s
CI/CD / build (pull_request) Successful in 2m37s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 2m1s
CI/CD / build (pull_request) Successful in 2m37s
CI/CD / publish (pull_request) Has been skipped
The prime-agent switcher existed only as a 275-line string literal inside
prime-agent-extension.ts, so nothing typechecked or linted it — the exact gap
that let a wrong ctx.ui.select() option shape ship in the pi extension. It now
lives at src/prime-agent-ext/mcpctl-switch.ts with a generator, a tsconfig
checking it against the real @earendil-works/pi-coding-agent types, eslint
coverage and an embed-freshness test, matching pi and opencode.
The extraction was verified byte-identical before any edit, so the behaviour
shipped today is exactly what was captured. Linting it then found six problems
in code nothing had ever checked: object-truthiness null guards, a nullable
string conditional and a missing return type. All behaviour-preserving to fix,
but exactly the class of thing that ships silently when nothing is looking.
Also:
- `config pi` gains --dry-run, the last agent without it.
- The SessionStart hook installer now drops untagged duplicates of its own
exact command — rows left behind before the marker existed, or by a suite
that used to write into a real ~/.claude. Invisible in the UI; they just run
the sync twice per session. A hook the user wrote is never touched, even one
calling `mcpctl skills sync` with different flags.
- docs/claude-integration.md and docs/prime-agent-extension.md, the two
integrations that had no page.
prime-agent deliberately keeps its per-project MCP entry name rather than the
constant `mcpctl` claude and opencode now use: its switcher already unmounts the
previous project, so it never accumulates entries, and re-keying auth.json from
mcp:<project> to mcp:mcpctl would give up per-project token caching and needs a
migration. Documented as its own change rather than folded in here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
114
docs/claude-integration.md
Normal file
114
docs/claude-integration.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# mcpctl × Claude Code
|
||||
|
||||
## What `mcpctl config claude --project X` wires up
|
||||
|
||||
| Piece | Where | Purpose |
|
||||
|-------|-------|---------|
|
||||
| MCP entry | `./.mcp.json` | 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 |
|
||||
| Status line | `~/.claude/settings.json` | the active project, bottom of the screen |
|
||||
| `/mcpctl` | `~/.claude/commands/mcpctl.md` | switch projects from inside a session |
|
||||
|
||||
`--claude-dir` (or Claude Code's own `CLAUDE_CONFIG_DIR`) redirects everything
|
||||
under `~/.claude`.
|
||||
|
||||
## One server named `mcpctl`, not one per project
|
||||
|
||||
The MCP entry used to be named after the project. Because `.mcp.json` is
|
||||
*merged*, configuring a second project left the first mounted too — every
|
||||
project you had ever configured stayed connected, with duplicate tool names and
|
||||
nothing marking which was active.
|
||||
|
||||
There is now exactly one entry, `mcpctl`, and switching rewrites what sits
|
||||
behind it:
|
||||
|
||||
```jsonc
|
||||
{ "mcpServers": { "mcpctl": { "command": "mcpctl", "args": ["mcp", "-p", "docmost"] } } }
|
||||
```
|
||||
|
||||
Two things follow. The tool prefix is stable across switches, so the model never
|
||||
sees a tool namespace disappear mid-conversation. And because Claude Code can
|
||||
reconnect an existing MCP server from `/mcp`, a switch lands without restarting
|
||||
the app.
|
||||
|
||||
Entries an older CLI wrote are retired on the next run. They are recognised by
|
||||
the pairing that makes retiring them safe — *our* command, named after the very
|
||||
project it bridges to. A hand-configured server is never touched, even one
|
||||
called `docmost`, unless it also runs `mcpctl mcp -p docmost`, at which point it
|
||||
is the same entry anyway.
|
||||
|
||||
## Switching: `/mcpctl`
|
||||
|
||||
```
|
||||
❯ /mcpctl docmost
|
||||
● Bash(mcpctl config claude --project docmost --skip-marker)
|
||||
⎿ Wrote .mcp.json (1 server(s))
|
||||
● Now on docmost. Reconnect the mcpctl server from /mcp for its tools to load.
|
||||
```
|
||||
|
||||
With no argument it lists the projects and asks.
|
||||
|
||||
Claude Code slash commands are **prompt files, not code**, so unlike opencode's
|
||||
picker this drives the model through CLI calls — there is no keyboard picker to
|
||||
be had. `allowed-tools` is scoped to the four exact `mcpctl` invocations it
|
||||
needs, so accepting it does not hand the session a general shell.
|
||||
|
||||
> Every `` !`…` `` block in a slash command is permission-checked against
|
||||
> `allowed-tools` *before* the model runs. Omitting one fails the whole command
|
||||
> with a permission error and no explanation. A test asserts every pre-executed
|
||||
> command in ours is covered.
|
||||
|
||||
`--skip-marker` is deliberate: the session's directory is whatever you happened
|
||||
to open, and re-scoping it would silently change which skills sync into it.
|
||||
|
||||
## The status line
|
||||
|
||||
```
|
||||
mcpctl:docmost
|
||||
⏵⏵ 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
|
||||
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.
|
||||
|
||||
### It is never installed over yours
|
||||
|
||||
A status line is a single slot, so overwriting a custom one silently deletes
|
||||
work. When `config claude` finds a foreign one it leaves it and prints the
|
||||
snippet to add instead:
|
||||
|
||||
```
|
||||
Left your existing status line alone (my-fancy-prompt).
|
||||
To show the project too, append: $(mcpctl statusline)
|
||||
```
|
||||
|
||||
> **Ownership is decided by the command string, not a marker.** Claude Code
|
||||
> rewrites `settings.json` against its own schema and **strips unknown keys from
|
||||
> `statusLine`** — a tagged entry comes back as a bare `{type, command}`.
|
||||
> (Hooks keep their marker; `statusLine` does not.) Matching on the command is
|
||||
> what stops us reporting our own line as foreign forever. A line that merely
|
||||
> *composes* ours — `my-prompt && mcpctl statusline` — is yours, and is left
|
||||
> alone.
|
||||
|
||||
## Skills
|
||||
|
||||
`mcpctl skills sync` installs into `~/.claude/skills/`. Claude Code is the only
|
||||
target that also gets hooks, `postInstall` and `mcpServers` auto-attach; pi,
|
||||
prime-agent and opencode share the simpler flat-tree semantics.
|
||||
|
||||
The SessionStart hook keeps them current. It carries a `_mcpctl_managed` marker,
|
||||
and an install now also drops **untagged duplicates of that exact command** —
|
||||
rows left behind before the marker existed (or by a test suite that used to
|
||||
write into a real `~/.claude`). They are invisible in the UI and just run the
|
||||
sync twice per session. A hook you wrote is never touched, even one that also
|
||||
calls `mcpctl skills sync` with different flags.
|
||||
|
||||
## Running Claude Code on the homelab LLM
|
||||
|
||||
See [claude-vllm.md](claude-vllm.md).
|
||||
Reference in New Issue
Block a user