Implements mcpctl as an opencode addon, mirroring the prime-agent UX (active-project indicator + a /mcpctl project switcher). opencode is a native MCP client, so instead of a bridge this mounts the active project's proxy MCP gateway through opencode's own live MCP API. - src/opencode-ext/mcpctl-opencode.ts — server plugin (headless runs): mounts the active project under a stable `mcpctl` name via `client.mcp.add`, re-asserting on first contact and before each turn; never throws. - src/opencode-ext/mcpctl-opencode-tui.tsx — TUI plugin: `/mcpctl`, `/mcpctl-status`, `/mcpctl-skills`, and a `mcpctl:<project>` indicator in the prompt footer (next to the model name, above the token counter); the switch delegates to the CLI then re-points the mount live. - `mcpctl config opencode` — provision/reuse the project mcptoken, install + register the plugins (tui.json), write the 0600 state file, sync skills. - `skills sync --agent opencode` — new shared-tree target (~/.config/opencode/skill). - embedded-source generator + embed test, settings/config/order tests, completions, README + docs/opencode-extension.md. Typechecked against the real @opencode-ai/plugin types (1.18.15).
4.5 KiB
mcpctl × opencode — native MCP integration
Motivation
opencode is a native MCP client, so mcpctl can mount a project's MCP servers
without any bridge process. mcpctl config opencode --project X:
- installs two plugins into opencode's config directory, and
- provisions the bearer credential the gateway needs.
Switching projects stays live — no restarting opencode — which is the
difference between this and a static mcp block in opencode.json.
How it works
Both plugins read ~/.mcpctl/opencode-state.json (0600):
{
"project": "monitoring",
"gatewayUrl": "https://mcp.ad.itaz.eu",
"tokens": {
"monitoring": "mcpctl_pat_..."
}
}
opencode.json is never touched. The gateway needs an
Authorization: Bearer <mcpctl PAT> header — a secret that does not belong in a
mode-0644 config file users paste into bug reports — and the mount has to be
re-pointable at runtime, which a config file cannot do.
Server plugin (plugin/mcpctl.ts)
Auto-discovered by opencode; covers headless runs (opencode run ...),
which load no TUI plugins. It mounts the active project as a remote MCP server
under the constant name mcpctl via opencode's own live MCP API:
- on first server contact (
event), and - before every user turn (
chat.message), re-reading the state file so a switch made externally takes effect on the next message.
The mount is only re-registered when the (url, token) target actually changed
— re-adding an unchanged config would rebuild the connection and drop mcplocal's
gated begin_session state mid-conversation. Never throws: an unreachable
gateway degrades to "no mcpctl tools", never to "opencode fails to start".
TUI plugin (mcpctl/mcpctl-tui.tsx, registered in tui.json)
Adds to the opencode TUI:
/mcpctl— switch the active project from a filterable picker (active project first; terms match name or description; a pre-filter prompt is offered when the list is long), matching the prime-agent switcher UX;/mcpctl-status— show the active project and its MCP mount state;/mcpctl-skills— re-sync this project's skills into opencode's skill dir;- a
mcpctl:<project>indicator in the prompt footer, immediately right of the model name and just above the token counter.
The switch is delegated to the mcpctl CLI (--skip-plugin --skip-marker), so
token minting, state and skills stay in the CLI; once the state file is updated
the TUI re-points the live MCP mount through api.client.mcp.add.
Deliverables
| Artifact | Purpose |
|---|---|
src/opencode-ext/mcpctl-opencode.ts |
Server plugin (headless mount) |
src/opencode-ext/mcpctl-opencode-tui.tsx |
TUI plugin (/mcpctl switcher + footer indicator) |
src/cli/src/utils/opencode-settings.ts |
CLI wiring: paths, tui.json registration, state file |
mcpctl config opencode |
Provision token + install/register plugins + skill sync |
docs/opencode-extension.md |
This document |
Skills
mcpctl config opencode (when given a project and an API client) syncs the
project's skills into ~/.config/opencode/skill/. Re-sync later with:
mcpctl skills sync --agent opencode --project <name>
opencode is a shared-tree target (like prime-agent/pi): its own flat skill
tree and its own state file (~/.mcpctl/skills-state-opencode.json), no Claude
integration.
Layout & typechecking
The plugins ship as source: they are embedded into the CLI
(src/cli/src/config/opencode-extension.ts, generated by
scripts/generate-opencode-extension.ts) and written verbatim into
~/.config/opencode/. The CLI's own build never compiles them, so
src/opencode-ext/tsconfig.json checks them against the real
@opencode-ai/plugin types (dev dependency pinned to the opencode release they
target) — the same guarantee src/pi-ext/ gives.
pnpm run typecheck # includes typecheck:opencode-ext
After editing src/opencode-ext/*, regenerate the embedded copy or the CLI will
keep installing old sources:
npx tsx scripts/generate-opencode-extension.ts
A test (tests/config/opencode-extension-embed.test.ts) fails if you forget.
Authentication caveat
The server and TUI plugins send the stored mcpctl_pat_ project token as the
bearer. A locally running mcpctl-local daemon does not authenticate
/projects/*, so the header is simply ignored; an authenticated
mcplocal serve accepts only mcpctl_pat_ bearers (mcpctl config opencode
mints one per project).