Files
mcpctl/docs/opencode-extension.md
Michal 9448c117df
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m16s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / lint (pull_request) Successful in 2m42s
CI/CD / smoke (pull_request) Failing after 1m59s
CI/CD / build (pull_request) Successful in 4m54s
CI/CD / publish (pull_request) Has been skipped
feat(opencode): add opencode integration — /mcpctl switcher + footer indicator
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).
2026-08-08 22:45:38 +01:00

4.5 KiB
Raw Permalink Blame History

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:

  1. installs two plugins into opencode's config directory, and
  2. 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).