feat(opencode): native opencode addon — bottom-row project indicator + in-TUI switcher
Some checks failed
CI/CD / lint (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m58s
CI/CD / smoke (pull_request) Failing after 1m55s
CI/CD / build (pull_request) Successful in 4m56s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m58s
CI/CD / smoke (pull_request) Failing after 1m55s
CI/CD / build (pull_request) Successful in 4m56s
CI/CD / publish (pull_request) Has been skipped
Adds an opencode integration mirroring the existing pi and prime-agent addons.
The addon is a TUI plugin (`src/opencode-ext/open-mcpctl.tsx`) that:
- renders the active mcpctl project into opencode's `app_bottom` slot — the
status row that also carries the token counter / model info,
- registers a `mcpctl.switch_project` command bound to `<leader>m` that opens a
searchable `DialogSelect` project picker,
- switches live by re-pointing opencode's `mcpctl` MCP server at the new
project's mcplocal endpoint via the SDK client (native MCP, no JSON-RPC code),
- persists state in `~/.mcpctl/opencode-state.json` (+ `.mcpctl-project` marker).
Pure helpers (`filterProjects`, `toolChangeAnnouncement`) live in
`projects.ts` so they are unit-tested.
`mcpctl config opencode --project X` provisions it: writes the plugin into
`~/.config/opencode/plugin/`, registers the `.tsx` in opencode.json's plugin
array (auto-discovery only matches {ts,js}), best-effort installs the TUI peer
deps (`@opentui/*`), and syncs skills into `~/.config/opencode/skill`.
`mcpctl skills sync --agent opencode` is added as a fourth shared-tree target.
Closes typecheck gap with `typecheck:opencode-ext` against the real
`@opencode-ai/plugin` + `@opentui/solid` types.
Tests: embed freshness, opencode-settings, projects helpers, skills agent
root. Full `pnpm run typecheck` + cli tests pass; completions regenerated.
This commit is contained in:
134
docs/opencode-extension.md
Normal file
134
docs/opencode-extension.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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/<project>/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 <project>` segment in the theme's accent/text colours.
|
||||
|
||||
### Switching projects
|
||||
|
||||
`<leader>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 <name>
|
||||
```
|
||||
|
||||
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 `<leader>m`.
|
||||
|
||||
Re-sync skills later:
|
||||
|
||||
```bash
|
||||
mcpctl skills sync --agent opencode --project <name>
|
||||
```
|
||||
|
||||
## 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 <name> --project <name>`).
|
||||
Reference in New Issue
Block a user