Skip to content

Commit c3081bb

Browse files
feat: allow the Cursor agent to use opencode plugins (#104)
* feat: let the Cursor agent use opencode plugins 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. * test: isolate XDG env vars in skill-discovery tests 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. * chore: drop implementation plans from tracked docs * feat: mirror opencode built-in skills from the live inventory 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. * fix: fetch live skills via raw GET /skill when SDK lacks app.skills 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. * fix: bound sidecar control-channel fetches with timeouts 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. * fix(plugin-tools): keep bridge alive on live config refresh 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> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 143c189 commit c3081bb

10 files changed

Lines changed: 3145 additions & 82 deletions

‎README.md‎

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ The plugin also registers two **delegation tools**:
158158
> `edit: deny`, `bash: ask`) do **not** apply to them.
159159
>
160160
> Options if you need a permission boundary:
161+
>
161162
> - Set `sandbox: true` in `provider.cursor.options` to run Cursor's tools in Cursor's sandbox.
162163
> - Use `cursor_delegate` instead of the provider path — it is gated by opencode's `permission`
163164
> config.
@@ -302,17 +303,34 @@ The mirror includes:
302303
- **Global skills** from `~/.config/opencode/skills/` and `~/.config/opencode/skill/`,
303304
`~/.claude/skills/`, `~/.agents/skills/`, `~/.opencode/skills/` and `~/.opencode/skill/`.
304305
- **Configured paths** from `config.skills.paths` in your `opencode.json` — additional
305-
directories scanned at the lowest priority (project and standard global locations
306+
directories scanned at low priority (project and standard global locations
306307
win on duplicate ids). `~/` prefixes are expanded to your home directory; relative
307308
paths are resolved against the project directory.
309+
- **Plugin-bundled skills** — skills that ship inside installed opencode plugins,
310+
scanned from the opencode plugin cache (`~/.cache/opencode/packages/` on
311+
macOS/Linux; `%LocalAppData%\opencode\cache\packages\` on Windows) and from
312+
`skills/`/`skill/` dirs alongside file-based plugins
313+
(`~/.config/opencode/plugin/`, `~/.config/opencode/plugins/`, and the project's
314+
`.opencode/plugin/`). Handles npm specs (`pkg@latest`, `@scope/pkg@latest`) and
315+
git specs (`pkg@git+https:...`). Plugin-bundled skills are the **lowest**
316+
priority: a project, global, or `skills.paths` skill with the same id always
317+
wins, so you can shadow a plugin's skill by defining your own.
318+
- **opencode's live skill inventory** — on every turn the mirror also consults
319+
opencode's `app.skills` endpoint (when reachable) and merges any skill it
320+
knows about that the filesystem scan missed, at the same lowest priority.
321+
- **opencode's built-in skills** — skills opencode registers in code rather
322+
than on disk (currently `customize-opencode`, its own config-authoring
323+
guide). They only exist in the live inventory, so the mirror materialises
324+
them from the endpoint's content into `.cursor/skills/` like any other
325+
skill; the materialised copy updates whenever opencode's version changes.
308326
- **Supporting files** alongside each `SKILL.md` (preserving relative paths).
309327
- An `<available_skills>` catalogue appended to the generated system rule,
310328
listing each skill's id and description so the Cursor agent can load them on
311329
demand.
312330

313-
> **Note:** `config.skills.urls` (HTTP skill catalogs) are not yet supported by
314-
> the mirror. If you rely on URL-sourced skills, they will not appear in
315-
> `.cursor/skills/`.
331+
> **Note:** URL-sourced skills (`config.skills.urls`) that are also present in
332+
> opencode's live inventory reach the mirror through that route; the mirror
333+
> does not fetch `skills.urls` catalogs on its own.
316334
317335
### Permission filtering
318336

@@ -346,13 +364,12 @@ user explicitly asked for them). `exclude` always drops the listed skills.
346364

347365
### Limitations
348366

349-
- **Skills bundled inside opencode plugins are not mirrored.** Those ship under
350-
`<opencode-package-cache>/<pkg>/node_modules/<pkg>/skills/` (on macOS/Linux
351-
`~/.cache/opencode/packages/`; on Windows `%LocalAppData%\opencode\cache\packages\`),
352-
which is not a scanned location — `@opencode-ai/sdk` exposes no skills API, so
353-
the mirror resolves skills from the filesystem itself. Skills that reach
354-
opencode only through a plugin will be absent from `.cursor/skills/`. To
355-
mirror one, add its directory to `config.skills.paths`.
367+
- Skills served via `skills.urls` that opencode itself hasn't loaded (the
368+
endpoint is reachable but the catalog wasn't pulled this session) won't
369+
appear until opencode sees them.
370+
- Built-in skills require the live `app.skills` endpoint (i.e. a running
371+
opencode server reachable by this plugin); the filesystem scan can't see
372+
them on its own.
356373
- A user-owned `.cursor/skills/<id>/SKILL.md` (without the `generated:
357374
opencode-cursor` sentinel) is never overwritten or deleted.
358375
- Individual files larger than 1 MB are skipped (the rest of the skill is still
@@ -362,6 +379,88 @@ user explicitly asked for them). `exclude` always drops the listed skills.
362379
any pre-existing `.cursor/skills/` there still loads).
363380
- `cursor_cloud_agent` targets a remote repo and does not inherit skills.
364381

382+
## Plugin tools
383+
384+
Other opencode plugins can register custom tools (e.g. `opencode-pty`'s
385+
`pty_spawn`, `context-mode`'s `ctx_execute`). With `forwardPluginTools: true`
386+
(default), this plugin **mirrors those tools to the Cursor agent** via a local
387+
stdio MCP server (`opencode-plugin-tools`) that is added to the forwarded
388+
`mcpServers`. When the Cursor agent calls one, the call runs through the
389+
plugin's *real* implementation inside opencode's runtime — same code path
390+
opencode itself uses.
391+
392+
How it works:
393+
394+
1. At startup (and re-checked each turn) the plugin reads your `plugin: []`
395+
list, re-imports each plugin from the opencode package cache, and reads its
396+
`tool` map. Plugins that can't be imported are skipped and logged once.
397+
2. A loopback-only HTTP control channel (random port, per-session bearer
398+
token) connects the MCP child process to the host plugin, which owns the
399+
tool closures. Nothing is reachable from outside the machine.
400+
3. The Cursor agent sees the tools through its normal MCP surface and calls
401+
them like any other MCP tool.
402+
403+
### Permissions
404+
405+
Mirrored calls are evaluated against your opencode `permission` config,
406+
keyed by the tool id — with the same semantics opencode itself uses:
407+
permission keys are wildcard-matched, and every pattern a tool's `ask`
408+
requests is matched against the rule's pattern; the **last** matching rule
409+
wins. `~`/`$HOME` prefixes in patterns expand against your home directory.
410+
411+
- **`allow`** → runs without prompting (every requested pattern must allow).
412+
- **`deny`** → rejected.
413+
- **`ask`** (or no rule) → rejected with a clear message. The interactive
414+
prompt is anchored to opencode's session/TUI and can't be surfaced to the
415+
Cursor agent, so ask-permissioned tools are withheld rather than run
416+
unattended. Set the tool to `allow` to use it from Cursor:
417+
418+
```json
419+
{ "permission": { "pty_spawn": "allow", "ctx_*": "allow" } }
420+
```
421+
422+
Pattern-scoped rules work too — e.g. allow spawning ptys only under `/tmp`
423+
(specific patterns must come **after** the wildcard they narrow):
424+
425+
```json
426+
{ "permission": { "pty_spawn": { "*": "ask", "/tmp/*": "allow" } } }
427+
```
428+
429+
If a tool's execution calls `ask` internally and no gate is available, the
430+
call fails closed.
431+
432+
### Filtering
433+
434+
```json
435+
{
436+
"provider": {
437+
"cursor": {
438+
"options": {
439+
"forwardPluginTools": true,
440+
"pluginTools": {
441+
"include": ["pty_*"],
442+
"exclude": ["ctx_execute"]
443+
}
444+
}
445+
}
446+
}
447+
}
448+
```
449+
450+
`include` keeps only matching tool ids (wildcards supported); `exclude` always
451+
drops. `forwardPluginTools: false` disables the bridge entirely.
452+
453+
### Limitations
454+
455+
- Plugins that fail to re-import under the bridge (e.g. native modules that
456+
only load under Bun, or plugins that throw when initialised twice) are
457+
skipped and reported in the opencode log; their tools stay unavailable to
458+
Cursor. Skills and MCP servers from those plugins are unaffected.
459+
- Tool definitions are snapshotted at startup and re-checked per turn; a
460+
plugin installed mid-session is picked up on the next turn.
461+
- `cursor_cloud_agent` targets a remote repo and does not inherit plugin
462+
tools.
463+
365464
## Delegation tools
366465

367466
Both tools resolve the API key from your `opencode auth login` session (or `CURSOR_API_KEY`) and

0 commit comments

Comments
 (0)