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

4.8 KiB
Raw Permalink Blame History

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:

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.