Some checks failed
CI/CD / lint (pull_request) Successful in 1m7s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / typecheck (pull_request) Successful in 2m49s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 2m16s
CI/CD / publish (pull_request) Has been skipped
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.
|