Files
mcpctl/docs/prime-agent-extension.md
Michal a9fcd83ed8
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
refactor(prime-agent): extract the /mcpctl switcher to typechecked source; pi --dry-run; docs
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
2026-08-09 19:20:36 +01:00

102 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mcpctl × prime-agent
## What `mcpctl config prime-agent --project X` wires up
| Piece | Where | Purpose |
|-------|-------|---------|
| Proxy MCP | `~/.prime/agent/settings.json` | `{ type: "http", url: "<gateway>/projects/X/mcp" }` |
| Bearer credential | `~/.prime/agent/auth.json` | `mcp:X` → a minted mcpctl PAT |
| Skills | `~/.prime/agent/skills/` | the project's `SKILL.md` bundles |
| `/mcpctl` switcher | `~/.prime/agent/extensions/mcpctl-switch.ts` | switch projects, and the active-project indicator |
Unlike Claude Code (a stdio bridge, no token) and pi (native tools over
JSON-RPC), prime-agent talks to the **HTTP gateway**, so the switch is really
three things: a credential, a settings entry, and a reload.
## Credentials are the fragile part
`config prime-agent` mints an `mcptoken` per project, stores it under
`mcp:<project>`, and retires the one it replaced. Three rules make that safe:
- **A key being present proves nothing.** A revoked token would short-circuit
provisioning and leave prime-agent unable to reach the gateway while the
command reported success. mcptokens are shown once, so the stored token's
16-char `tokenPrefix` is compared against the project's *active* tokens rather
than sending the secret.
- **No credential means the switch fails.** Non-zero exit, `settings.json`
untouched, so the previously active project keeps working instead of being
replaced by a mount that 401s — and the `/mcpctl` switcher, which reads that
exit code, reports failure rather than success over a project with no tools.
- **Only the token we replaced is revoked.** Sweeping every `prime-agent` token
for a project would kill the one another machine is using. Anything else that
looks orphaned is reported, not deleted.
This plumbing is shared with `config opencode`, parameterised by agent rather
than copied.
## The `/mcpctl` switcher
`pi.registerCommand('mcpctl', …)` opens a picker, shells out to
`mcpctl config prime-agent --project X --skip-extension --skip-marker`, then
calls `ctx.reload()` — which re-reads `settings.json` and `auth.json` and
rebuilds the MCP map, so the switch lands without restarting the app.
`--skip-extension` stops it rewriting the very file it is running from;
`--skip-marker` stops it re-scoping whatever repository prime-agent was started
in, which Claude Code's own skills sync would then pick up.
Above 20 projects it asks for a filter first: prime-agent's selector is an
arrow-key list with no search, and real installs run to hundreds of projects.
(opencode's dialog filters as you type, so its switcher needs no such prompt.)
## The active-project indicator
Published with `ctx.ui.setStatus('mcpctl', …)`, which prime-agent renders in its
tray line next to the model name.
Two quirks worth knowing:
- prime-agent emits `session_start` **only from `reload()`**, never at startup —
so the indicator is also published on `turn_start`, the earliest moment with a
real UI context bound.
- `resetExtensionUI()` clears extension statuses *after* `session_start`, so the
indicator set there is wiped before anyone sees it. It is re-published on a
short retry schedule to land after that reset.
> The rendering itself only exists in prime-agent from
> `prime-agent-extension-status.patch` (upstream PR pending). On an unpatched
> build `setStatus` silently does nothing and no indicator appears.
## The extension is real source now
It used to exist **only** as a string literal inside
`src/cli/src/config/prime-agent-extension.ts` — so nothing typechecked or linted
it, which is precisely the 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`, checked against the real
`@earendil-works/pi-coding-agent` types:
```bash
pnpm typecheck:prime-agent-ext
npx tsx scripts/generate-prime-agent-extension.ts # after editing it
```
A test fails if the embed goes stale. Extracting it found six lint problems in
code that had never been linted — all null-guard and return-type issues rather
than live bugs, but exactly the class of thing that ships silently when nothing
is looking.
## What it deliberately does *not* do
The MCP entry is still **named after the project**, not the constant `mcpctl`
that `config claude` and `config opencode` now use. prime-agent's switcher
already unmounts the previous project, so it never accumulates entries the way
`config claude` did — the bug the constant name fixes does not exist here.
The remaining difference is tool-prefix stability: switching changes tool names,
so the model can hold stale ones. Moving prime-agent to a constant name would
also re-key `auth.json` from `mcp:<project>` to `mcp:mcpctl`, giving up
per-project token caching and needing a migration. Worth doing, but as its own
change rather than folded into a parity pass.