# mcpctl × opencode — native integration (TUI plugin + MCP bridge) ## Motivation With Claude Code, `mcpctl config claude --project X` wires a project's MCP servers into the agent two ways: 1. a `.mcp.json` entry running `mcpctl mcp -p X` (an MCP **stdio bridge**), and 2. a `SessionStart` hook + `mcpctl skills sync` that materialises server-side **skills** under `~/.claude/skills/`. **opencode** speaks MCP natively (like Claude), consumes Agent Skills `SKILL.md` trees (like pi), and lets plugins render into its TUI. This addon mirrors the prime-agent/pi integration so mcpctl "just works" from inside opencode: - `mcpctl config opencode --project X` registers the project's MCP **stdio bridge** (`mcpctl mcp -p X`) as an opencode `local` MCP server, installs a small **TUI plugin**, and syncs the project's skills. - The TUI plugin gives you a `/mcpctl` slash command to **switch projects** from the opencode prompt, and shows the **current project in the bottom status bar** — next to where opencode prints the model and the token counter — so you always know which project's MCP servers are live. ## How it works ### MCP over stdio opencode has native MCP client support, so unlike pi there is no JSON-RPC client to vendor. `mcpctl config opencode --project X` writes into opencode's global config (`~/.config/opencode/opencode.json`): ```json { "mcp": { "servers": { "homeautomation": { "type": "local", "command": ["mcpctl", "mcp", "-p", "homeautomation"], "mcpctlManaged": true } } } } ``` `mcpctlManaged` is a tag opencode ignores (its schema tolerates unknown keys); mcpctl uses it to recognise — and retire — the single *active* entry when you switch, so exactly one mcpctl project is mounted at a time and hand-configured servers are never touched. ### The TUI plugin (footer indicator + /mcpctl) opencode loads plugins from `~/.config/opencode/plugin/`, but its auto-discovery glob is `*.{ts,js}` — it won't pick up a JSX `.tsx` file. So `config opencode` writes `plugin/mcpctl-switch.tsx` **and** references it from `opencode.json`'s `plugin` array as a relative path: ```json { "plugin": ["./plugin/mcpctl-switch.tsx"] } ``` The plugin is a **TUI plugin** (module `default` exports `{ id, tui }`). It: - registers a `/mcpctl` slash command (surfaces in the prompt's `/` autocomplete) that opens a **filterable** project picker (opencode's `DialogSelect` filters as you type), and - renders the active project in the **`app_bottom`** slot — the bottom strip where opencode shows the model and token counter. Switching runs the CLI: ``` mcpctl config opencode --project --skip-plugin --skip-marker --config-dir ``` which rewrites `opencode.json` (section above). opencode's config watcher reloads MCP so the new project's tools come up — if a running session doesn't pick it up, a restart does. ## Deliverables | Artifact | Purpose | |----------|---------| | `src/opencode-ext/mcpctl-opencode.tsx` | Real, reviewable TUI plugin source | | `scripts/generate-opencode-extension.ts` | Embeds that source into the CLI (`config/opencode-extension.ts`) | | `src/cli/src/config/opencode.ts` | opencode.json read/merge/write + state helpers | | `mcpctl config opencode` | CLI wiring: MCP + plugin + state + marker + skills | | `mcpctl skills sync --agent opencode` | Sync skills into `~/.config/opencode/skills/` | | `docs/opencode-extension.md` | This document | ## Regenerating the embedded plugin The plugin ships as *source* embedded into the CLI, so an installed binary can still provision a working addon. After editing `src/opencode-ext/*.tsx`, regenerate the embedded copy or the CLI will keep installing the old source: ```bash npx tsx scripts/generate-opencode-extension.ts ``` A test (`tests/config/opencode-embed.test.ts`) fails if the embed drifts from the source. ## Skills `mcpctl config opencode` (and `mcpctl skills sync --agent opencode`) materialise the active project's skills under `~/.config/opencode/skills//`, the tree opencode reads for Agent Skills. Sync state is tracked separately (`~/.mcpctl/skills-state-opencode.json`), so opencode, prime-agent and Claude never fight over bookkeeping. ## Switching with `--config-dir` The `/mcpctl` plugin shells out with `--config-dir` pointing at the config dir it was installed into, so a non-default opencode config dir keeps working too. ## Project switching / auth Like the Claude path, this uses the **stdio bridge** (`mcpctl mcp -p X`) and a locally-running `mcplocal` daemon, so there is no HTTP gateway / bearer-token provisioning dance (that's specific to prime-agent's remote gateway). Pointing it at an authenticated `mcplocal serve` needs a project mcptoken wired through `mcpctl mcp` — outside this addon's scope for now.