Skip to content

fix(opencode): make V2 plugin tools directly callable and routing independent of a stray MCP - #1

Closed
qoole wants to merge 3 commits into
Scratchydisk:fix/opencode-v2-plugin-compatfrom
qoole:opencode-v2-direct-tools
Closed

qoole wants to merge 3 commits into
Scratchydisk:fix/opencode-v2-plugin-compatfrom
qoole:opencode-v2-direct-tools

Conversation

@qoole

@qoole qoole commented Sep 18, 2026

Copy link
Copy Markdown

What / Why / How

Follow-up to mksglu#1171 (targets its branch, so it can be folded into that PR rather than compete with it). I ran mksglu#1171 against a live OpenCode 2.0.7 server and it loads and hooks correctly. But three things kept context-mode from being automatic there. Each is one commit with a test that fails without the fix:

  1. The ctx_* tools were not directly callable. In OpenCode 2, ToolEditor.add() defaults to codemode: true, which leaves a tool out of the model's direct tool list: it can only be reached by writing code against the execute tool's catalog. The routing layer, meanwhile, tells the model to call context-mode_ctx_execute, context-mode_ctx_fetch_and_index and context-mode_ctx_search directly, while the V2 registration used the bare ctx_* names. So the redirect named tools that did not exist. The tools are now registered with codemode: false. (4b91426)

  2. Names now follow the router, through OpenCode 2's own namespace. OpenCode 2 builds the name the model sees as <namespace>_<name>. v2ToolIdentity() splits createToolNamer(platform)(tool) into { name, namespace }, so the visible name is exactly what the routing block and the execute.before redirects use, without hardcoding a prefix. If the OpenCode naming later changes (e.g. fix(tool-naming): use native ctx_* names for OpenCode/KiloCode plugin tools mksglu/context-mode#1161's bare ctx_*), registration follows automatically. (468b7eb)

  3. Routing redirects depended on an unrelated process. routing.mjs only redirects curl/wget and large-output commands when isMCPReady() finds a live readiness sentinel, and only the stdio MCP server's main() writes one. With native V2 tools there is no MCP server. So on a machine where some other client happened to have a context-mode MCP server running, curl was redirected; on a machine without one, it ran straight through. The V2 setup() now writes the same sentinel for its own process: PID contents, refreshed every 30s against the reader's 90s window, on an unref'd timer, removed on dispose. (86c15ef)

Verified on OpenCode 2.0.7, as a local plugin on a live server with no context-mode MCP running:

  • all context-mode_ctx_* tools appear as direct tools, and context-mode_ctx_execute runs;
  • curl -s https://example.com | head -c 80 through the shell tool comes back as context-mode: curl/wget redirected. Call context-mode_ctx_execute(...), and the tools it names exist;
  • the routing block is injected into the system prompt by the context hook (4,239 chars, alongside another plugin's context hook);
  • ctx_execute still honours permissions.deny (sudo -n true → Command blocked by security policy).

Two notes, not changed here:

  • The V1 native-tool path appears to have the same sentinel dependency (point 3). I left it alone because I could not test it against a live OpenCode 1.x host.
  • OpenCode 2.0.7 loads a local plugin directory only through <dir>/index.js. It ignores package.json main/exports for local paths, and rejects a file path with configured plugin path must be a directory. That doesn't affect npm installs, but "Option B — manual install" from a clone would silently load nothing on V2 unless the package root has an index.js.

Affected platforms

  • OpenCode

Test plan

  • tests/opencode-plugin.test.ts: runs the real V2 setup() against a minimal fake OpenCode 2 context and asserts:
    • every tool is registered with codemode: false;
    • for every tool, the name OpenCode 2 would show (<namespace>_<name>) equals createToolNamer("opencode")(name);
    • a readiness sentinel exists for the process after setup(), isMCPReady() is true, and the sentinel is removed on dispose.
  • Unit cases for v2ToolIdentity: the prefixed form maps to a namespace; bare names and mcp__… shapes are used as-is.
  • Each new test fails before its fix and passes after.

Checklist

…ed names

OpenCode 2's editor.add() defaults tools to codemode: true, which keeps them
out of the model's direct tool list -- they are only reachable by writing code
against the execute tool's catalog. Routing enforcement, meanwhile, tells the
model to call context-mode_ctx_execute / ctx_fetch_and_index / ctx_search
directly, and the V2 registration used the bare ctx_* names, so the redirect
pointed at tools that did not exist.

Register the V2 tools with codemode: false and under createToolNamer(platform)
names, the same names the routing block and execute.before redirects use.
Observed on OpenCode 2.0.7: before, the model's direct tool list had no ctx_*
tools while curl was redirected to context-mode_ctx_execute; after, all ctx_*
tools are direct and the redirect names match.

Adds a test that runs the real V2 setup() against a fake OpenCode 2 context
and asserts codemode: false plus routed names (red without the fix).
…option

OpenCode 2 composes the model-visible tool name as <namespace>_<name>. Instead
of baking the routed prefix into the name, split toolNamer(name) into
{ name, namespace } (v2ToolIdentity) so the tools use the host's native
namespace field while the visible names stay exactly what the router tells the
model to call. Any namer shape other than a clean <prefix>_<tool> (bare names,
mcp__x__tool) is registered verbatim with no namespace.

Verified on OpenCode 2.0.7: the model sees context-mode_ctx_execute etc., and
context-mode_ctx_execute runs.

Tests: v2ToolIdentity unit cases; the setup() test now asserts, per tool, that
the host-composed visible name equals namer(name) (no prefix-startsWith
tautology), awaits the transform callback, and removes its abort listener.
hooks/core/routing.mjs only redirects curl/wget and large-output commands when
isMCPReady() finds a live readiness sentinel, which the stdio MCP server writes
from its main(). With native V2 tools there is no MCP server, so the redirects
silently depended on an unrelated context-mode MCP process (e.g. another
client's) being alive: on a host with none, curl ran straight through.

The V2 setup() now writes the same sentinel for its own process (PID contents,
30s refresh against the reader's 90s freshness window, unref'd timer) and
removes it on dispose. Observed on OpenCode 2.0.7: curl was redirected on a
machine where a Claude Code context-mode MCP happened to be running and not on
one without; with this change it is redirected on both.

The V1 native-tool path looks to share this dependency; left untouched here as
it was not tested against a live OpenCode 1.x host.
@Scratchydisk

Copy link
Copy Markdown
Owner

Thanks for this — all three commits are now on main, cherry-picked with authorship preserved:

  • 192312d — fix(opencode): register V2 ctx_* tools as direct tools under the routed names (was 4b91426)
  • 2a5f7f3 — refactor(opencode): register V2 tools through OpenCode 2's namespace option (was 468b7eb)
  • 95ea78f — fix(opencode): mark the V2 plugin as a ready tool provider for routing (was 86c15ef)

Verified: tests/opencode-plugin.test.ts 63 passed, npm run typecheck clean, and all three claims re-checked against opencode v2.0.9 (codemode default, <namespace>_<tool> flattening, directory-only plugin loading).

Notes for the record:

  • src/adapters/opencode/plugin.ts on main is now identical to this PR's head.
  • The one remaining test diff (ctx_search vs context-mode_ctx_search) is base divergence from 1378797 bare naming on main, not PR content — this PR never touched that line.
  • Closing as superseded rather than merging, since the target branch fix/opencode-v2-plugin-compat is ~988 lines behind main; merging would drag the stale base back in.

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.

2 participants