Files
mcpctl/docs/opencode-extension.md
Michal 2022e584d1
Some checks failed
CI/CD / lint (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / typecheck (pull_request) Successful in 3m8s
CI/CD / smoke (pull_request) Failing after 1m55s
CI/CD / build (pull_request) Successful in 2m8s
CI/CD / publish (pull_request) Has been skipped
feat(opencode): native opencode addon — /mcpctl switcher, live project switching, footer indicator
Implement mcpctl as an opencode addon mirroring the pi/prime-agent
integrations. Mounts the active project's MCP gateway through opencode's own
live MCP API, so switching projects needs no restart and opencode.json is
never touched (the bearer token stays in a 0600 state file).

- server plugin (plugin/mcpctl.ts): headless mount under a stable 'mcpctl'
  name, re-asserted on first contact + before each turn; skips re-registering
  an unchanged mount so a gated project's begin_session state survives.
- TUI plugin (mcpctl/mcpctl-tui.tsx): /mcpctl filterable picker (live switch),
  /mcpctl-status, /mcpctl-skills, and a mcpctl:<project> footer indicator
  right of the model name, directly above the token counter.
- mcpctl config opencode: mint/reuse project mcptoken, install + register
  plugins, write 0600 state, write marker, sync skills.
- skills sync --agent opencode: new shared-tree target (XDG-aware).
- shared credential plumbing lifted from config prime-agent and parameterised
  by agent so both hosts share mint/reuse/retire logic.
- embedded-source generator + freshness test; typechecked against the real
  @opencode-ai/plugin types (1.18.15); unit tests for settings, order, embed.

typecheck (incl. opencode-ext) and the full cli suite (619 tests) pass.
2026-08-09 00:54:51 +01:00

186 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mcpctl × opencode — project switching from the TUI
## Motivation
`mcpctl config claude` and `mcpctl config prime-agent` both work the same way:
write the project's MCP server into the host's config file, then restart the
host so it picks the change up. That is fine when you pick a project once, and
tiresome when you switch between `homeautomation`, `sre` and `docmost` all day.
[opencode](https://opencode.ai) can do better, because it exposes two things
the other hosts do not:
- an **HTTP API for its own MCP registry** (`POST /mcp`,
`/mcp/{name}/disconnect`) — so a mount can be re-pointed while the app runs;
- a **TUI plugin API** (`tui.json`) with slash commands, dialogs and UI slots —
so the switcher can be a first-class part of the interface rather than a
shell command you run in another terminal.
So the opencode integration does not write an MCP entry into `opencode.json` at
all. It ships two plugins, and switching projects takes effect on the next turn
with no restart.
## How it works
```
~/.mcpctl/opencode-state.json (0600)
{ project, gatewayUrl, tokens: { <project>: <mcpctl PAT> } }
│ read by both plugins
~/.config/opencode/plugin/mcpctl.ts server plugin ─┐
~/.config/opencode/mcpctl/mcpctl-tui.tsx TUI plugin ─┤
│ client.mcp.add({
▼ name: "mcpctl", type: "remote", url, headers })
opencode's MCP registry
https://<gateway>/projects/<project>/mcp
```
### The state file, not `opencode.json`
Two reasons the project does not live in opencode's own config:
1. **The token.** The gateway needs `Authorization: Bearer <mcpctl PAT>`.
`opencode.json` is a mode-0644 file people paste into bug reports;
`~/.mcpctl/opencode-state.json` is 0600, like every other mcpctl credential.
2. **The restart.** A config file is read at startup. Re-pointing the mount
through the running server's MCP API is what makes `/mcpctl` instant.
Tokens are kept **per project**, so switching back to a project you have
already used needs no new mint — and a failed mint for project B cannot cost
you the credential for project A.
### The server plugin (`plugin/mcpctl.ts`)
Auto-discovered by opencode. It mounts the active project's gateway under the
fixed MCP server name `mcpctl`, on the first server event and again before
every user turn.
Two details matter:
- **The name is constant.** Tools keep a stable `mcpctl_*` prefix across
switches, and because opencode re-resolves the tool list per request, the
model simply sees the new project's tools on its next turn. (Contrast pi,
where the extension has to *tell* the model its old tool names are dead.)
- **It does not re-register an unchanged mount.** `mcp.add` rebuilds the
connection, and mcplocal binds a gated project's unlocked state to that
connection's `mcp-session-id` — re-adding every turn would re-lock a project
you had just opened with `begin_session`.
It also exists so that **headless** runs (`opencode run …`), which load no TUI
plugins at all, still get the project's tools.
> The mount is deliberately *not* performed during plugin setup. Setup runs
> before opencode's server accepts connections, and `client.mcp.add` calls back
> into that same server — awaiting it there hangs the app on a blank screen
> before the TUI ever draws.
### The TUI plugin (`mcpctl/mcpctl-tui.tsx`)
Registered in `~/.config/opencode/tui.json` (opencode does not auto-discover
TUI plugins). It adds:
| Command | What it does |
|---------|--------------|
| `/mcpctl` | Filterable project picker; switches live |
| `/mcpctl-status` | Active project, mount state, gateway URL |
| `/mcpctl-skills` | Re-sync this project's skills |
and a `mcpctl:<project>` indicator in the prompt footer, next to the model name
and one line above the token counter.
Switching delegates to the CLI —
`mcpctl config opencode --project X --skip-plugin --skip-marker` — so token
minting, state and skills stay in one place and the plugin stays a UI shell.
`--skip-plugin` avoids rewriting the very file opencode has already loaded;
`--skip-marker` stops a switch from silently re-scoping whichever repository
opencode happened to be started in.
The picker needs no pre-filter prompt (unlike the pi and prime-agent
switchers): opencode's select dialog filters as you type, so the plugin only
has to order the list — active project first, then alphabetical.
The indicator is published through `api.kv`, which is a reactive store: writing
it re-renders the slot with no signal plumbing, and it persists across sessions
so the label is correct on the very first frame.
#### Why the footer and not the status bar
opencode exposes UI slots, not arbitrary layout. In the footer region the
options are:
| Slot | Result |
|------|--------|
| `session_prompt_right` / `home_prompt_right` | **used** — renders on the prompt's bottom line, right of the model name, directly above the token counter |
| `home_footer` | sits on the counter's line, but *replaces* the cwd/version footer instead of adding to it |
| `app_bottom` | costs a whole extra terminal row |
There is no slot on the status-bar line itself. On the home screen the prompt
box is narrow, so a long project name wraps onto a second line; in a session
(where the prompt is full width) it always fits on one.
### Skills
opencode implements the Agent Skills standard and loads `SKILL.md` trees from
`~/.config/opencode/skill/` (XDG-aware). `mcpctl skills sync --agent opencode`
syncs there, with the same shared-tree semantics as `--agent pi` and
`--agent prime-agent`: a flat tree with per-project ownership, its own state
file (`~/.mcpctl/skills-state-opencode.json`), no SessionStart hooks, no
`postInstall`, and untracked skill directories are never clobbered.
## Usage
```bash
mcpctl config opencode --project monitoring
```
That mints (or reuses) the project token, writes the 0600 state file, installs
both plugins, registers the TUI plugin in `tui.json`, and runs an initial
skills sync. Start opencode and the project's tools are there.
Skip individual steps:
```bash
mcpctl config opencode --project monitoring --token mcpctl_pat_xxx # provide token, don't mint
mcpctl config opencode --project monitoring --skip-skills # don't sync skills
mcpctl config opencode --project monitoring --skip-plugin # state only, leave plugins alone
mcpctl config opencode --project monitoring --skip-marker # don't touch .mcpctl-project here
mcpctl config opencode --project monitoring --dry-run # print the plan, write nothing
mcpctl config opencode --project monitoring --opencode-dir /path # non-default opencode config dir
```
## Failure semantics
- **No usable credential → the switch fails.** The command exits non-zero and
leaves the state file untouched, so the previously active project keeps
working rather than being replaced by a mount that 401s. `/mcpctl` reads that
exit code and reports the switch as failed instead of claiming success over a
project with no tools.
- **Gateway unreachable → no mcpctl tools.** Never a failed startup: the server
plugin swallows mount errors.
- **Corrupt `tui.json` → refuses to write.** One syntax error must not silently
drop every other TUI plugin you installed.
## Files
| Path | Purpose |
|------|---------|
| `src/opencode-ext/mcpctl-opencode.ts` | Server plugin source |
| `src/opencode-ext/mcpctl-opencode-tui.tsx` | TUI plugin source |
| `src/cli/src/config/opencode-extension.ts` | Generated embed of both (do not edit) |
| `scripts/generate-opencode-extension.ts` | Regenerates that embed |
| `src/cli/src/utils/opencode-settings.ts` | Install / register / state helpers |
The plugin sources are **embedded** in the CLI so `mcpctl config opencode`
works from an installed binary with no source tree. After editing either
source, re-run:
```bash
npx tsx scripts/generate-opencode-extension.ts
```
A test fails if you forget. They are typechecked against the real
`@opencode-ai/plugin` types via `pnpm typecheck:opencode-ext`.