feat: allow the Cursor agent to use opencode plugins - #104
Merged
justin-carper merged 7 commits intoAug 26, 2026
Conversation
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
deleted the
allow-the-cursor-agent-to-use-opencode-plugins
branch
August 26, 2026 15:57
This was referenced Aug 26, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Lets the Cursor agent use installed opencode plugins, in two parts:
.cursor/skills/alongsidethe existing skill sources.
opencode-pty'spty_spawn,context-mode'sctx_*) are bridged to the Cursor agent as anopencode-plugin-toolsMCP server.Skills
discoverSkills: the opencode plugincache (
~/.cache/opencode/packages/,%LocalAppData%\opencode\cache\packageson 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.pathsskills always win onduplicate ids.
app.skillsinventory atthe same lowest priority, so skills sourced from places this plugin doesn't
scan still reach Cursor.
forwardSkills; the existingskills.include/excludeand permission filtering apply unchanged.Plugin tools
its
toolmap — mirroring the exact closures opencode executes (nosession-loopback prompt, no LLM turn cost per call).
per-session bearer token). A dependency-free stdio JSON-RPC MCP server
(
dist/sidecar/plugin-tools-mcp.js) proxiestools/list/tools/calland is merged into the forwarded
mcpServersat startup and re-checkedeach turn.
evaluation of every pattern a tool's
askrequests, last matching rulewins,
~/$HOMEpattern expansion.ask/unconfigured fails closed witha clear message (the interactive prompt can't surface to the Cursor agent).
forwardPluginTools: falsedisables;pluginTools.include/excludefilter 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 --noEmitclean;npm run buildcleannpx vitest run→ 41 files, 607/607 tests (new: skill-discovery plugincases, plugin-tools bridge suite, full-plugin wiring suite)
mcpServers["opencode-plugin-tools"]; a tool whoseexecutecallsctx.askis rejected when unconfigured and runs when allowed — verifiedover a real MCP child process
opencode-pty+context-mode, 0 failuresDocs
section with permissions, filtering, and limitations
docs/superpowers/plans/2026-09-01-plugin-bundled-skills-mirror.md— plandocs/superpowers/plans/2026-09-01-plugin-tools-findings.md— feasibilityfindings + recorded follow-ups (memoize per-turn plugin init, shared
cache-root helper for version-check, symlink containment, token handoff)