feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
# 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`
2026-08-09 20:47:45 +01:00
1. **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. This is
the load-bearing reason.
2. **The token. ** Having chosen the gateway, we need
`Authorization: Bearer <mcpctl PAT>` somewhere. `opencode.json` is a mode-0644
file people paste into bug reports; `~/.mcpctl/opencode-state.json` is 0600,
like every other mcpctl credential.
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
2026-08-09 20:47:45 +01:00
> **Reason 2 is not an argument for this design over the alternative.** A
> `type: "local"` entry running `mcpctl mcp -p <project>` — the same stdio
> bridge `config claude` uses — needs no bearer token at all, because the bridge
> reads your own `~/.mcpctl/credentials`. So "no secret in a 0644 file" is not a
> point against that approach; it is just a consequence of having picked the
> HTTP gateway.
>
> The honest trade is: the gateway works against a remote mcpctl with no local
> `mcplocal` daemon, and mounts through an API that can be re-pointed live. The
> stdio bridge is simpler and credential-free, but requires `mcpctl` and a
> reachable mcplocal on the same machine. Both are defensible; this one was
> chosen for the remote case and for the live re-point, not for the token.
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
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 |
|---------|--------------|
2026-08-09 18:02:52 +01:00
| `/mcpctl` (or `<leader>m` ) | Filterable project picker; switches live |
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
| `/mcpctl-status` | Active project, mount state, gateway URL |
| `/mcpctl-skills` | Re-sync this project's skills |
2026-08-09 18:02:52 +01:00
Switching is the thing you do repeatedly, so it gets a chord as well as a slash
command; the other two stay palette-only.
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
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.
2026-08-09 18:02:52 +01:00
A switch **disconnects before re-adding ** . `mcp.add` under the same name does
re-point the tools on its own, but leaves it to opencode whether the previous
client is closed, and an abandoned one keeps its `mcp-session-id` alive on
mcplocal — the very thing that holds a gated project open. Best-effort: on a
first mount there is nothing to disconnect.
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
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 right on the 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 |
2026-08-09 18:02:52 +01:00
There is no slot on the status-bar line itself. The home prompt box is narrow
enough that the default wrap breaks `mcpctl:homeautomation` across two lines
mid-word, so the label renders `wrapMode="none" truncate` — clipping the tail of
a long name reads better than a two-line footer. In a session the prompt is full
width and it always fits.
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
2026-08-08 21:03:53 +01:00
### 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` .