Repository navigation
Preserve MCP tools for Claude requests - #19
Conversation
|
@dotCipher please take a look, I use it few days and everything works good |
|
This is the highest-impact fix in the current batch, @olosegres — MCP tools being silently rewritten into Claude core tools (and thus invisible) is a real problem, and splitting the one conflated map into I pulled this locally and merged it on top of the updated Two minor, non-blocking notes:
Great work — rebase and I think this is ready. |
|
Re-review update: the MCP/custom-tool preservation direction remains useful, especially the request-scoped inbound name map. This branch is currently conflicted ( Please rebase onto current |
Keep active OpenCode custom/MCP tools in Claude-compatible requests instead of filtering them down to Claude core schemas. Continue replacing active built-in tools with Claude wire schemas, while preserving MCP-prefixed names and exact-name custom tool collisions.
4b99b98 to
9845075
Compare
|
Sorry for the long delay here 🙏 Rebased onto current Also picked up your non-blocking note:
|
|
Hi @dotCipher — gentle nudge on this one 🙂 It's rebased on current The CI run on Is there anything else you need from me to get this merged? |
The plugin failed to initialise at all, so none of its hooks ever ran —
model injection, credential sync and request transformation were all
dead. opencode logged one line and carried on without it:
failed to load plugin modelId.match is not a function
Cause: opencode only recognises a plugin whose default export is an
object shaped { id, server }. A bare function default export drops it
into the legacy loader, which walks every named export of the module and
calls each one as a plugin factory with (input, options). The first
helper it reaches, deriveModelDisplayName, receives an object where it
expects a model id and throws, taking the whole plugin down.
Export the module shape opencode looks for instead. The id is mandatory
for plugins referenced by filesystem path — opencode rejects those
without one.
Guarded by a test on the default export shape. Nothing covered it
before, which is why this went unnoticed.
db1abcb to
8247523
Compare
dotCipher
left a comment
There was a problem hiding this comment.
Re-reviewed the updated head. The request-scoped MCP preservation, explicit plugin module, and regression coverage address the prior blockers. I independently ran
added 3 packages, and audited 4 packages in 485ms
found 0 vulnerabilities,
opencode-claude-bridge@1.10.11 typecheck
tsc --noEmit,
opencode-claude-bridge@1.10.11 test
npm run build && node --test dist/index.test.js
opencode-claude-bridge@1.10.11 build
tsc
▶ thinking injection
✔ does NOT inject thinking for claude-sonnet-4-5 (0.3315ms)
✔ does NOT inject thinking for claude-haiku-4-5-20251001 (0.053417ms)
✔ does NOT inject thinking for an unknown model (0.052708ms)
✔ injects adaptive thinking for claude-sonnet-4-6 (0.314167ms)
✔ injects adaptive thinking for claude-opus-4-5 (0.0535ms)
✔ injects adaptive thinking for claude-opus-4-6 (0.040667ms)
✔ does not overwrite an existing thinking block (0.039542ms)
✔ thinking injection (1.399875ms)
▶ temperature coercion
✔ forces temperature to 1 when adaptive thinking is injected (0.071667ms)
✔ forces temperature to 1 when thinking:enabled is already set (0.349791ms)
✔ does not touch temperature when thinking is not injected (sonnet-4-5) (0.059125ms)
✔ does not touch temperature when it is already 1 (0.040958ms)
✔ does not touch temperature when it is absent (0.036875ms)
✔ temperature coercion (0.671792ms)
▶ OAuth error parsing
✔ extracts nested error messages from token responses (0.062917ms)
✔ falls back to raw response bodies when parsing fails (0.04575ms)
✔ OAuth error parsing (0.143458ms)
▶ Claude assistant prefill stripping
✔ strips the synthetic continue prefill so Anthropic requests end with a user message (0.068541ms)
✔ keeps non-synthetic assistant messages intact (0.032458ms)
✔ Claude assistant prefill stripping (0.130458ms)
▶ system cache control
✔ strips cache_control from system blocks (0.054208ms)
✔ system cache control (0.0755ms)
▶ tool schema selection
✔ uses Claude schemas for native Anthropic requests (0.075875ms)
✔ uses Claude schemas for Claude-family models on OpenRouter (0.022709ms)
✔ keeps default schemas for non-Claude models on OpenRouter (0.022708ms)
✔ keeps default schemas for non-Claude targets (0.021709ms)
✔ injects Claude tool schemas only when active tools are present (0.034125ms)
✔ does not inject Claude tool schemas for tool-less summary or compaction requests (0.025625ms)
✔ filters Claude tool schemas to tools active in the OpenCode request (5.708458ms)
✔ does not advertise WebSearch when only a custom websearch_cited tool is active (0.071875ms)
✔ preserves active MCP and custom OpenCode tools (0.099792ms)
✔ preserves MCP tools that have no description (0.065375ms)
✔ preserves MCP tools whose names look like prefixed core tools (0.166958ms)
✔ preserves custom tools whose names collide with Claude core tool names (0.191833ms)
✔ maps inbound Claude core names only for active OpenCode core tools (0.185292ms)
✔ maps outbound OpenCode core history but preserves MCP-prefixed names (0.038917ms)
✔ does not advertise AskUserQuestion when OpenCode did not enable question (0.061ms)
✔ tool schema selection (7.057542ms)
▶ translateToolArgsJsonString
✔ renames file_path → filePath for Read (0.173333ms)
✔ renames all Edit params (0.03675ms)
✔ renames glob → include for Grep and preserves other keys (0.158875ms)
✔ renames skill → name for Skill and strips args (OpenCode skill has no args param) (0.091666ms)
✔ translates activeForm → priority INSIDE TodoWrite todos[] (0.247ms)
✔ does NOT rewrite activeForm when it appears only inside a string value (Linus case) (0.076583ms)
✔ does NOT rewrite file_path when it appears inside a Bash command string (0.049416ms)
✔ maps Agent subagent_type values (0.047042ms)
✔ leaves an unknown Agent subagent_type untouched (0.030083ms)
✔ strips prompt param for WebFetch and injects default format (0.042083ms)
✔ respects explicit WebFetch format if already set (0.0305ms)
✔ returns input unchanged for malformed JSON (0.038583ms)
✔ returns input unchanged for non-object JSON (array or primitive) (0.031375ms)
✔ passes through unknown tool names without modification (0.032291ms)
✔ strips Claude-only Agent fields (model, run_in_background, isolation) (0.031834ms)
✔ defaults Agent subagent_type to 'general' when missing (OpenCode requires it) (0.025625ms)
✔ strips Claude-only Bash fields (run_in_background, dangerouslyDisableSandbox) (0.028291ms)
✔ strips Claude-only Read field (pages) (0.031292ms)
✔ strips Claude-only Grep fields (0.049ms)
✔ translates AskUserQuestion multiSelect → multiple per question (0.049583ms)
✔ parses stringified AskUserQuestion questions and translates multiSelect (0.051042ms)
✔ translateToolArgsJsonString (1.550416ms)
▶ translateArgsOpencodeToClaude
✔ renames camelCase keys to snake_case for Edit (0.104625ms)
✔ maps OpenCode subagent_type values back to Claude (0.022416ms)
✔ strips Agent OpenCode-only fields (task_id, command) (0.023958ms)
✔ translates AskUserQuestion multiple → multiSelect per question (0.033333ms)
✔ synthesizes a WebFetch prompt from format (markdown) (0.022833ms)
✔ synthesizes a WebFetch prompt from format (text) (0.021083ms)
✔ synthesizes a WebFetch prompt from format (html) (0.032667ms)
✔ strips WebFetch timeout (OpenCode-only) (0.0215ms)
✔ renames priority → activeForm in TodoWrite and collapses cancelled → completed (0.029292ms)
✔ maps plan_enter → EnterPlanMode with empty args (outbound) (0.017708ms)
✔ maps plan_exit → ExitPlanMode preserving allowedPrompts (outbound) (0.023917ms)
✔ is the inverse of translateToolArgsJsonString for the Edit round trip (0.031583ms)
✔ translateArgsOpencodeToClaude (0.479917ms)
▶ Agent type maps
✔ CLAUDE_TO_OPENCODE covers Claude's subagent_type values (0.041ms)
✔ OPENCODE_TO_CLAUDE covers OpenCode's built-in agents (0.017875ms)
✔ Agent type maps (0.079875ms)
▶ SSE processor: tool name mapping
✔ maps Bash → bash on content_block_start (0.349041ms)
✔ maps Read → read on content_block_start (0.048208ms)
✔ maps Glob → glob on content_block_start (0.04525ms)
✔ maps Grep → grep on content_block_start (0.041458ms)
✔ maps Edit → edit on content_block_start (0.0405ms)
✔ maps Write → write on content_block_start (0.049667ms)
✔ maps Agent → task on content_block_start (0.038166ms)
✔ maps WebFetch → webfetch on content_block_start (0.046917ms)
✔ maps TodoWrite → todowrite on content_block_start (0.033292ms)
✔ maps Skill → skill on content_block_start (0.027916ms)
✔ maps AskUserQuestion → question on content_block_start (0.025ms)
✔ maps EnterPlanMode → plan_enter on content_block_start (0.02275ms)
✔ maps ExitPlanMode → plan_exit on content_block_start (0.025ms)
✔ passes through an unknown tool name without modification (0.034833ms)
✔ SSE processor: tool name mapping (0.945083ms)
▶ SSE processor: argument translation
✔ translates file_path → filePath for Read (0.0705ms)
✔ translates all Edit params (0.037625ms)
✔ translates glob → include for Grep, preserves pattern (0.034291ms)
✔ translates activeForm → priority per todo item in TodoWrite (0.046792ms)
✔ translates Agent subagent_type values (0.045459ms)
✔ strips prompt and injects default format for WebFetch (0.03525ms)
✔ translates skill → name and strips args for Skill (0.048041ms)
✔ leaves Bash args untouched (0.031292ms)
✔ maps EnterPlanMode → plan_enter with empty args (0.040833ms)
✔ maps ExitPlanMode → plan_exit preserving allowedPrompts (0.038333ms)
✔ SSE processor: argument translation (0.522625ms)
▶ SSE processor: chunk boundary handling
✔ handles args split across many small fragments (0.064625ms)
✔ handles multiple SSE events concatenated into one chunk (0.051709ms)
✔ handles an SSE event split across two chunks (0.041375ms)
✔ passes through text deltas unchanged (0.0285ms)
✔ does NOT translate tool args inside a text_delta (0.024708ms)
✔ SSE processor: chunk boundary handling (0.260667ms)
▶ SSE processor: interleaved and concurrent tool_use blocks
✔ keeps per-block state isolated across interleaved deltas (0.072083ms)
✔ SSE processor: interleaved and concurrent tool_use blocks (0.085833ms)
▶ SSE processor: error handling
✔ calls debug callback on malformed JSON in an SSE data frame (0.062958ms)
✔ calls debug callback when translateToolArgs throws (0.064292ms)
✔ SSE processor: error handling (0.143625ms)
▶ SSE processor: flush
✔ flush returns any trailing buffered bytes (0.040834ms)
✔ logs a debug warning when a tool_use block is abandoned mid-stream (0.064834ms)
✔ SSE processor: flush (0.125334ms)
▶ SSE processor: pass-through optimization
✔ emits the exact input bytes for ping events (no reserialize) (0.034333ms)
✔ emits the exact input bytes for message_start events (0.022208ms)
✔ emits the exact input bytes for text_delta events (0.028125ms)
✔ still transforms tool_use content_block_start (optimization does not skip interesting events) (0.035084ms)
✔ SSE processor: pass-through optimization (0.152375ms)
▶ parseSseEvent / buildSseEvent round trip
✔ round-trips a simple event (0.031708ms)
✔ returns null for a frame with no data line (0.019875ms)
✔ handles a data-only frame (no event line) (0.021416ms)
✔ parseSseEvent / buildSseEvent round trip (0.09725ms)
▶ deriveModelDisplayName
✔ parses claude-opus-4-7 → Opus 4.7 (0.161209ms)
✔ parses claude-sonnet-4-6 → Sonnet 4.6 (0.044667ms)
✔ ignores a trailing date suffix (claude-haiku-4-5-20251001 → Haiku 4.5) (0.020542ms)
✔ falls back to the raw id when the convention doesn't match (0.024834ms)
✔ deriveModelDisplayName (0.288042ms)
▶ rewriteSystemBlocksForModel
✔ rewrites the identity line to match the requested model (0.105333ms)
✔ leaves blocks untouched when no model id is provided (0.02075ms)
✔ handles a dated variant without re-including the date suffix in the display name (0.027208ms)
✔ preserves non-text blocks and non-matching text (0.027583ms)
✔ rewriteSystemBlocksForModel (0.208541ms)
ℹ tests 116
ℹ suites 19
ℹ pass 116
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 546.576042 (118 passing), and ; all passed.
dotCipher
left a comment
There was a problem hiding this comment.
Re-reviewed the updated head. The request-scoped MCP preservation, explicit plugin module object, and regression coverage address the prior blockers. I independently ran npm ci, npm run typecheck, npm test (118 passing), and git diff --check; all passed.
Summary
Without it MCP list is not visible for models.
Test plan