# mcpctl × prime-agent ## What `mcpctl config prime-agent --project X` wires up | Piece | Where | Purpose | |-------|-------|---------| | Proxy MCP | `~/.prime/agent/settings.json` | `{ type: "http", url: "/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:`, 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:` 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.