Files
mcpctl/docs/opencode-extension.md
Michal 9aea1189bd
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
feat(opencode): native opencode addon — bottom-row project indicator + in-TUI switcher
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.
2026-08-08 21:48:14 +01:00

5.2 KiB
Raw Blame History

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:

  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

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:

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):

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:

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>).