124 lines
4.7 KiB
Markdown
124 lines
4.7 KiB
Markdown
|
|
# 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 <name> --skip-plugin --skip-marker --config-dir <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/<name>/`, 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.
|