Skip to content

feat: allow the Cursor agent to use opencode plugins - #104

Merged
justin-carper merged 7 commits into
mainfrom
allow-the-cursor-agent-to-use-opencode-plugins
Aug 26, 2026
Merged

feat: allow the Cursor agent to use opencode plugins#104
justin-carper merged 7 commits into
mainfrom
allow-the-cursor-agent-to-use-opencode-plugins

Conversation

@justin-carper

Copy link
Copy Markdown
Collaborator

What

Lets the Cursor agent use installed opencode plugins, in two parts:

  1. Plugin-bundled skills are mirrored into .cursor/skills/ alongside
    the existing skill sources.
  2. Other plugins' custom tools (e.g. opencode-pty's pty_spawn,
    context-mode's ctx_*) are bridged to the Cursor agent as an
    opencode-plugin-tools MCP server.

Skills

  • New lowest-priority scan roots in discoverSkills: the opencode plugin
    cache (~/.cache/opencode/packages/, %LocalAppData%\opencode\cache\packages
    on Windows) and skills//skill/ dirs alongside file-based plugins.
    Handles npm specs (pkg@latest, @scope/pkg@latest) and git specs
    (pkg@git+https:...). Project/global/skills.paths skills always win on
    duplicate ids.
  • The per-turn re-sync also merges opencode's live app.skills inventory at
    the same lowest priority, so skills sourced from places this plugin doesn't
    scan still reach Cursor.
  • No new config surface: folds into forwardSkills; the existing
    skills.include/exclude and permission filtering apply unchanged.

Plugin tools

  • The bridge re-imports each plugin module from the package cache and reads
    its tool map — mirroring the exact closures opencode executes (no
    session-loopback prompt, no LLM turn cost per call).
  • Mirrored tools run inside a loopback-only control server (random port +
    per-session bearer token). A dependency-free stdio JSON-RPC MCP server
    (dist/sidecar/plugin-tools-mcp.js) proxies tools/list / tools/call
    and is merged into the forwarded mcpServers at startup and re-checked
    each turn.
  • Permission gate mirrors opencode semantics: wildcard permission keys,
    evaluation of every pattern a tool's ask requests, last matching rule
    wins, ~/$HOME pattern expansion. ask/unconfigured fails closed with
    a clear message (the interactive prompt can't surface to the Cursor agent).
  • Config: forwardPluginTools: false disables; pluginTools.include/exclude
    filter tool ids (wildcards supported).
{ "permission": { "pty_spawn": "allow", "ctx_*": "allow" } }

Pattern-scoped rules work too (specific patterns after the wildcard they
narrow):

{ "permission": { "pty_spawn": { "*": "ask", "/tmp/*": "allow" } } }

Verification

  • npx tsc --noEmit clean; npm run build clean
  • npx vitest run → 41 files, 607/607 tests (new: skill-discovery plugin
    cases, plugin-tools bridge suite, full-plugin wiring suite)
  • E2E smoke against the built plugin: bridge lands in
    mcpServers["opencode-plugin-tools"]; a tool whose execute calls
    ctx.ask is rejected when unconfigured and runs when allowed — verified
    over a real MCP child process
  • Bun smoke (opencode's runtime): 16 real tools mirrored from
    opencode-pty + context-mode, 0 failures

Docs

  • README: plugin-bundled skills documented under Skills; new "Plugin tools"
    section with permissions, filtering, and limitations
  • docs/superpowers/plans/2026-09-01-plugin-bundled-skills-mirror.md — plan
  • docs/superpowers/plans/2026-09-01-plugin-tools-findings.md — feasibility
    findings + recorded follow-ups (memoize per-turn plugin init, shared
    cache-root helper for version-check, symlink containment, token handoff)

justin-carper and others added 7 commits August 24, 2026 19:51
Mirror plugin-bundled skills into .cursor/skills/ and bridge other
plugins' custom tools to the Cursor agent via a local stdio MCP server.

Skills (folds into forwardSkills):
- Scan the opencode plugin cache (~/.cache/opencode/packages/, Windows
  fallback) and file-plugin sibling skill dirs as lowest-priority roots
  after project/global/skills.paths; first-wins on duplicate ids.
- Merge opencode's live app.skills inventory per turn at the same
  lowest priority, covering sources the filesystem scan can't see.

Plugin tools (new forwardPluginTools option, default on):
- Re-import plugin modules from the package cache and read their tool
  maps — the same closures opencode executes.
- Mirrored tools run behind a loopback, token-authenticated control
  channel; a dependency-free stdio MCP server (opencode-plugin-tools)
  is merged into the forwarded mcpServers at startup and re-checked
  each turn.
- Permission gate mirrors opencode semantics: wildcard keys, per-ask
  pattern evaluation, last match wins, ~/ $HOME pattern expansion,
  ask/unconfigured fails closed with a clear message.
- pluginTools.include/exclude filter mirrored tool ids.
CI's GitHub runners carry real XDG_CONFIG_HOME/XDG_CACHE_HOME; the
discovery code prefers those env vars over the mocked homedir, so the
worker's real config dir leaked into the fakeHome-based expectations.
Point both vars at per-file fake dirs for the worker's lifetime.
opencode registers skills in code (currently `customize-opencode`,
location `<built-in>`) that have no on-disk SKILL.md, so neither the
filesystem scan nor the live merge reached them — the live entry was
dropped because its location isn't resolvable.

Materialise content-only live skills (name + description + content from
`app.skills`) into a per-process scratch dir with generated frontmatter,
so the mirror stamps and copies them like disk-backed skills. The copy
is only rewritten when the content or description changes, keeping
per-turn mtimes (and the skill-set hash) stable.

Verified against a live `opencode debug skill --pure` dump: 23 skills
parsed, `customize-opencode` lands in `.cursor/skills/` with the
sentinel and appears in the `<available_skills>` catalogue.
The V1 SDK typings this repo builds against (1.18.18) predate the
instance route (OpenApi identifier app.skills, path GET /skill), so
client.app.skills was undefined at runtime and the live-skill merge —
including opencode's <built-in> customize-opencode skill — silently
never ran.

Fall back to the hey-api core client underneath the typed groups
(client._client.get({ url: "/skill" })), verified against SDK 1.18.18
and opencode 1.18.21: returns { data: Skill[] } including the built-in
skill with its full 16 KB content.

Live E2E: temp project, branch build via project plugin entry, real
cursor provider (auto-smart) — customize-opencode landed in
.cursor/skills/ mid-session with the generated sentinel, 45 skills
mirrored total.
controlRequest had no abort signal: a stale port (something accepting
but never responding, e.g. a hung opencode holding the port after the
bridge closed) would hang Cursor's MCP discovery on tools/list, and
tools/call could block forever.

tools/list gets 5s (loopback list is instant; discovery degrades to an
empty tool list on timeout) and tools/call gets 5 minutes (plugin tools
can legitimately run long). New test spawns the sidecar against a
never-responding server and asserts tools/list answers [] fast.
Resolve node/bun for the MCP sidecar when execPath is a compiled host,
add hung-control timeouts, and retain the plugin-tools bridge when live
config.get omits plugin entries so chat.params does not tear it down.

Co-authored-by: Cursor <cursoragent@cursor.com>
@justin-carper
justin-carper merged commit c3081bb into main Aug 26, 2026
@justin-carper
justin-carper deleted the allow-the-cursor-agent-to-use-opencode-plugins branch August 26, 2026 15:57
This was referenced Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant