Skip to content

Preserve MCP tools for Claude requests - #19

Merged
dotCipher merged 2 commits into
dotCipher:mainfrom
olosegres:fix-preserve-mcp-tools
Sep 14, 2026
Merged

dotCipher merged 2 commits into
dotCipher:mainfrom
olosegres:fix-preserve-mcp-tools

Conversation

@olosegres

@olosegres olosegres commented May 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Preserve active OpenCode custom/MCP tools when replacing core tool schemas for Claude-compatible requests.
  • Keep MCP-prefixed and exact-name custom tool names from being rewritten to Claude core tools.
  • Update tool-selection tests and docs for active-tool filtering.

Without it MCP list is not visible for models.

Test plan

  • npm test

@olosegres

olosegres commented May 29, 2026 •

Copy link
Copy Markdown
Contributor Author

@dotCipher please take a look, I use it few days and everything works good

@0xsimulacra 0xsimulacra mentioned this pull request Jun 9, 2026
@dotCipher

Copy link
Copy Markdown
Owner

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 OUTBOUND_TOOL_NAME_MAP (history rewriting) vs ACTIVE_TOOL_SCHEMA_NAME_MAP (schema selection) is the right decomposition. Dropping the mcp_* entries is the actual cure. The per-request inbound map via getInboundToolNameMapForActiveOpenCodeTools is wired in correctly, and your test coverage for the nasty edges (MCP-prefixed names, Bash name collisions, schema-less tools, inbound/outbound symmetry) is exactly what I'd want to see.

I pulled this locally and merged it on top of the updated main — code merges clean and all tests pass. One thing before it can land: please rebase on latest main. Since #23 merged, there's now a README.md conflict in the tool-resolution section (it bumped the stub list to 16 tools / 26 total). The resolution is just to keep your rewritten active-filtering description — the prose, not the counts, is what changed.

Two minor, non-blocking notes:

  • getToolInputSchema silently drops tools without a valid input_schema. Defensible (that'd be malformed for a real MCP tool), but a debug log would make "my tool vanished" reports easier to triage.
  • description: "" fallback is fine for the API; just means the model gets no hint for that tool.

Great work — rebase and I think this is ready.

@dotCipher

Copy link
Copy Markdown
Owner

@olosegres

@dotCipher
dotCipher self-requested a review June 22, 2026 22:01

@dotCipher dotCipher left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see notes: #19 (comment)

@dotCipher

Copy link
Copy Markdown
Owner

Re-review update: the MCP/custom-tool preservation direction remains useful, especially the request-scoped inbound name map. This branch is currently conflicted (mergeable_state: dirty) and based on a stale May main, so it cannot be merged or validated as-is.

Please rebase onto current main, resolve the active-tool filtering changes, and rerun CI. The rebase should preserve custom/MCP schemas and avoid rewriting custom tool names that collide with Claude core names.

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.
@olosegres
olosegres force-pushed the fix-preserve-mcp-tools branch from 4b99b98 to 9845075 Compare August 24, 2026 17:20
@olosegres

Copy link
Copy Markdown
Contributor Author

Sorry for the long delay here 🙏

Rebased onto current main (v1.10.12). Resolved the README.md conflict by keeping the active-filtering description and taking the updated 16-stub / 26-tool counts from #23. There was also a small import conflict in index.test.ts from #24's refactor — folded the new exports into the consolidated import block.

Also picked up your non-blocking note: getClaudeToolsForActiveOpenCodeTools now logs a console.error when it drops an active tool with a missing/invalid input_schema, so "my tool vanished" reports are easier to triage.

typecheck, build and the full test suite (116 passing) are green locally. Should be clean to merge now — thanks for the patience!

@olosegres

Copy link
Copy Markdown
Contributor Author

Hi @dotCipher — gentle nudge on this one 🙂

It's rebased on current main (v1.10.12) with no conflicts, and typecheck, build and the full suite (116 tests) are green locally.

The CI run on 9845075 is sitting at action_required — fork PRs need a maintainer to approve the workflow run, so that part is on your side.

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.
@olosegres
olosegres force-pushed the fix-preserve-mcp-tools branch from db1abcb to 8247523 Compare September 14, 2026 11:14

@dotCipher dotCipher left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dotCipher left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

3 participants