feat(mcp): support tool-search-managed MCP servers#12
Open
jms830 wants to merge 4 commits into
Open
Conversation
Add opt-in MCP support directly inside opencode-tool-search, eliminating the need for a separate opencode-mcp-adapter plugin. When config.mcp.servers is set, the plugin connects to each MCP server (local stdio or remote HTTP with auth headers) at startup, lists their tools, and registers each as a native plugin tool. The standard tool.definition deferral logic then applies, including alwaysLoad exemption, without requiring opencode core changes. This solves opencode-tool-search#9: MCP tools previously bypassed the tool.definition hook in opencode core, so descriptions could not be deferred. By wrapping MCP tools as native plugin tools, they pass through the hook naturally. Architecture: - src/mcp/client.ts: per-server connection (stdio + http with headers) - src/mcp/wrap.ts: json-schema -> zod conversion + tool wrapping - src/mcp/index.ts: connectAll orchestrator (parallel, isolated) - src/types.ts: MCPServerConfig + MCPConfig public API - src/plugin.ts: 5-line integration spreading mcpTools into tool map Connection failures are isolated; one bad server does not prevent other servers' tools from being registered. Tests: 11 new tests across connectAll and jsonSchemaToZod (54 total). Build: clean ESM bundle, dist size ~15KB.
…ive OpenCode
Empty-args MCP tools (no `properties` declared) were producing tool
definitions with `properties: {}` that strict providers reject:
- Anthropic: `content.0.tool_use.input: Field required`
- OpenAI Responses: `Missing required parameter: input[N].arguments`
- Vertex Gemini, SGLang, Antigravity: similar empty-schema rejections
The rejection orphaned the recorded `tool_use` block (no `tool_result`
ever produced), cascading into "tool_use ids were found without
tool_result blocks" on every subsequent turn.
Fixes (src/mcp/wrap.ts):
- Inject optional `_placeholder` field when wrapped MCP tool has zero
properties so the rendered JSON Schema always has at least one prop
- Strip `_placeholder` from args before forwarding to MCP server
- Coerce `undefined` args to `{}` (handles Claude no-arg tool calls,
see anomalyco/opencode #9020)
Robustness (src/mcp/client.ts):
- Pass `CallToolResultSchema` to `client.callTool` matching native
OpenCode `convertMcpTool` invocation
- Pass `{ resetTimeoutOnProgress: true, timeout: 60_000 }` so
long-running MCP tools (browser automation, search indexers) keep
the timeout window alive via progress notifications
Tests (tests/mcp.test.ts): +9 cases covering injection, stripping,
real-args preservation, undefined-args handling, error/no-output paths.
Refs: anomalyco/opencode #9020 #8184 #9233 #9131 #15041 #20637
Adds an `experimental.chat.messages.transform` hook that walks the
message history before each request and synthesizes placeholder
`tool_result` blocks for any `tool_use` (Anthropic-shape) or `tool`
(OpenCode-internal shape) parts that lack a matching `tool_result`.
Why:
Once an orphan `tool_use` enters the message history (interrupted
tool call, mid-stream provider error, aborted shell command, model
provider returning before a tool_result lands, etc.), the session
becomes permanently rejected by Anthropic with:
"tool_use ids were found without tool_result blocks immediately
after"
Without this hook the only recovery is `/undo` or starting a fresh
session. The validator heals history in-flight on every request so
corrupted sessions become recoverable: the next turn ships a
synthetic placeholder result for the orphan and the conversation
continues.
Refs: anomalyco/opencode #21326 #21489 #16749 #14367 #17065
Implementation:
- New file: src/hooks/tool-pair-validator.ts (~200 LoC, single dep on
@opencode-ai/sdk types).
- Adapted from oh-my-openagent (MIT, code-yeongyu) — replaced their
shared logger with a no-op default that callers can override via
`setToolPairValidatorLogger`.
- Wired into src/plugin.ts as a second hook entry alongside the
existing `tool.definition` and `experimental.chat.system.transform`
hooks.
- 11 new tests covering: matching pairs (both shapes), missing
results, synthetic user injection, partial matches, dedup,
sessionID preservation, no-op cases, logger override.
Total tests: 74 passing (63 prior + 11 new).
Strip `contentEncoding` and `contentMediaType` keywords from the `inputSchema` before Zod conversion. Anthropic rejects tool schemas that contain these keywords (which appear on MCP tools handling binary blobs, e.g., base64-encoded image params). The strip is recursive and skips key matching when descending through a `properties` map, so a property literally named `contentEncoding` survives. Defensive-only at the moment — Zod's current `toJSONSchema` does not propagate these keywords on output, so users haven't observed the bug yet. Adding the strip futureproofs us against: - Future Zod versions that may preserve the keywords via .meta() - MCP servers we haven't wrapped yet that expose binary params Pattern adapted from oh-my-openagent's `sanitizeJsonSchema` (MIT, code-yeongyu). Tests: +4 cases covering top-level strip, nested object property strip, array item strip, and the "property literally named contentEncoding survives" edge case. Total tests: 78 passing (74 prior + 4 new).
|
Should include updates to the README.md as well? |
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.
Add first-class MCP server support inside
opencode-tool-searchso MCP tools can be discovered, indexed, deferred, and invoked through the same tool-search flow as native OpenCode tools.OpenCode’s native MCP registration path bypasses plugin
tool.definitionhooks, which means large MCP tool sets always enter model context in full. This change lets users configure MCP servers under the tool-search plugin instead, wrapping each MCP tool as a plugin tool so descriptions can be deferred, searched, and loaded on demand.The implementation supports local stdio MCP servers and remote streamable HTTP MCP servers, including optional auth headers. It also aligns MCP tool execution with OpenCode’s native behavior by passing
CallToolResultSchema, enabling progress-aware timeout handling, and normalizing empty-argument tools with an internal_placeholderfield that is stripped before calling the MCP server.This also adds a defensive message-history transform that repairs orphaned assistant
tool_useparts by inserting synthetictool_resultparts. This prevents provider errors when prior tool results are missing after compaction or interrupted tool execution.Finally, MCP schemas are normalized before Zod conversion by recursively stripping Anthropic-incompatible JSON Schema metadata keywords such as
contentEncodingandcontentMediaType, while preserving ordinary properties with those names.Review focus:
_placeholderis acceptable for zero-argument tools, or if there is a cleaner provider-agnostic pattern.tool_userepair hook belongs in this plugin or should be split out.mcp.servers.Validation run locally:
npm run typechecknpm run lintnpm run testnpm run buildnpm run check:exportsLocal integration smoke test connected the plugin to nine MCP servers and discovered 158 plugin-managed tools, including context-mode, qmd, scrapling, jcodemunch, agent-mail, and remote gateway tools.