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
4.8 KiB
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
tokenPrefixis compared against the project's active tokens rather than sending the secret. - No credential means the switch fails. Non-zero exit,
settings.jsonuntouched, so the previously active project keeps working instead of being replaced by a mount that 401s — and the/mcpctlswitcher, 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-agenttoken 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_startonly fromreload(), never at startup — so the indicator is also published onturn_start, the earliest moment with a real UI context bound. resetExtensionUI()clears extension statuses aftersession_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 buildsetStatussilently 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:
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.