# mcpctl × opencode — native integration (TUI plugin) ## Motivation With Claude Code, `mcpctl config claude --project X` wires a project's MCP servers into the agent using `.mcp.json`, and with pi/prime-agent it ships a dedicated extension. This addon does the same for [opencode](https://opencode.ai): 1. a **TUI plugin** (`open-mcpctl.tsx`) that shows the active mcpctl project in opencode's bottom status row (the row that also carries the token counter / model / mode info) and lets you **switch projects from inside the interface** with a searchable picker, and 2. `mcpctl config opencode` which provisions it: writes the plugin into `~/.config/opencode/plugin/`, registers it in `opencode.json`, provisions the plugin deps, and persists the active project. Where pi had to *reimplement* an MCP client (pi has no MCP), opencode has first-class MCP support, so the plugin leans on opencode's own MCP machinery: switching a project re-points opencode's `mcpctl` MCP server at the new project's mcplocal endpoint (`{mcplocalUrl}/projects//mcp`) via the SDK client, and opencode enumerates those tools natively. No JSON-RPC code in the plugin. ## How it works ### The indicator (bottom status row) The plugin registers a slot into `api.slots.register({ slots: { app_bottom } })`. `app_bottom` is the row opencode renders directly beneath the session footer statusline — i.e. beside the token counter / model / mode readout. It renders a compact `mcpctl ` segment in the theme's accent/text colours. ### Switching projects `m` (or the `mcpctl.switch_project` command in the command palette) opens a `DialogSelect` listing projects from mcpd with search-as-you-type filtering (active project first, then alphabetical). Picking one: 1. `client.mcp.disconnect({ name: "mcpctl" })` — drop the previous project's tools, 2. `client.mcp.add({ name: "mcpctl", config: { type: "remote", url, headers } })` + `client.mcp.connect(...)` — point at the new project's mcplocal endpoint, 3. persists the choice to `~/.mcpctl/opencode-state.json`, 4. updates the bottom-row indicator and fires a toast. Because the tools travel over opencode's native MCP, they pick up `begin_session` gating exactly like any other mcpctl client. If a project is gated, opencode only exposes its `begin_session` tool until it is called. ### State The active project lives in `~/.mcpctl/opencode-state.json` (immune to your shell's cwd) and is also inferred from a `.mcpctl-project` marker walk-up, mirroring the pi extension. ## Deliverables | Artifact | Purpose | |----------|---------| | `src/opencode-ext/open-mcpctl.tsx` | Self-contained opencode TUI plugin (the addon) | | `mcpctl config opencode` | CLI wiring: installs the plugin, registers it, provisions deps + skills | | `docs/opencode-extension.md` | This document | ## Using it ```bash mcpctl config opencode --project ``` This: 1. writes `~/.config/opencode/plugin/open-mcpctl.tsx`, 2. registers that `.tsx` in `opencode.json` under `plugin` (opencode's plugin auto-discovery only matches `{ts,js}`, so the explicit entry is required for a JSX plugin), 3. best-effort installs `@opencode-ai/plugin` + `@opentui/*` into the config dir's `node_modules` (opencode provisions the former but not the TUI peers a TUI plugin needs), and 4. syncs the project's skills into `~/.config/opencode/skill`. Restart opencode (or start a new session) — the active project and its MCP tools appear in the bottom status row. Switch with `m`. Re-sync skills later: ```bash mcpctl skills sync --agent opencode --project ``` ## Layout ``` src/opencode-ext/ open-mcpctl.tsx # the plugin (JSX, self-contained) tsconfig.json # typechecks against real @opencode-ai/plugin types ``` The plugin imports only opencode-bundled packages (`@opencode-ai/plugin/tui`, `@opentui/solid`, `solid-js`) and the node standard library — no `@mcpctl/*`, no `~/.claude`. ## Typechecking The plugin ships as *source*: it is embedded into the CLI (`src/cli/src/config/opencode-extension.ts`, generated by `scripts/generate-opencode-extension.ts`) and written verbatim into `~/.config/opencode/plugin/`. The CLI's own build never compiles it, so without a dedicated project nothing would check it against opencode's API — exactly how a slotted component with the wrong shape would ship. `src/opencode-ext/tsconfig.json` closes that gap, checking against the **real** published `@opencode-ai/plugin` + `@opentui/solid` types (dev dependencies): ```bash pnpm run typecheck # includes typecheck:opencode-ext ``` After editing `src/opencode-ext/*.tsx`, regenerate the embedded copy or the CLI will keep installing the old sources: ```bash npx tsx scripts/generate-opencode-extension.ts ``` A test (`tests/config/opencode-extension-embed.test.ts`) fails if you forget. ## Authentication caveat The plugin sends whatever bearer it finds in `~/.mcpctl/credentials`. A locally running `mcpctl-local` daemon does not authenticate `/projects/*`, so this works — the header is simply ignored. `mcplocal serve` registers a token-auth preHandler that accepts **only** `mcpctl_pat_` bearers (project mcptokens), so pointing it at an authenticated `mcplocal serve` needs a project mcptoken (`mcpctl create mcptoken --project `).