@yulimfish/opencode-tool-searchDynamic tool loading for opencode — Kimi K3 tool-search pattern, adapted to opencode's plugin runtime. Collapses noisy tool descriptions into a searchable catalog so the LLM only sees relevant tools per turn.
0
134
近 7 天 7
31.8
生态多维模型
1 个月前
2026-07-20
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@yulimfish/opencode-tool-search@0.1.2"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@yulimfish/opencode-tool-search@0.1.2"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D @yulimfish/opencode-tool-searchopencode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
Dynamic tool loading for opencode, inspired by the Kimi K3 tool-search pattern.
Note on the name: the unscoped
opencode-tool-searchpackage on npm belongs to another author with a different implementation. This project publishes under@yulimfish/opencode-tool-searchto keep the namespaces distinct.
When a session accumulates dozens of tools (built-ins + MCP servers + custom
plugins), every request pays for the full tools[] catalog. The model also
loses accuracy as the option surface grows.
opencode-tool-search collapses the tools you designate as noisy (MCP servers
by default) into short stubs, exposes a single tool_search tool that the model
uses to discover them by keyword, and un-collapses tools once they've been
searched or used this session.
Why not exactly the Kimi API pattern?
Kimi K3 lets clients dynamically inject {role:"system", tools:[…]} messages
mid-conversation. opencode's plugin runtime doesn't (yet) expose the outgoing
tools[] array to plugins, so we can't do the raw protocol thing.
Instead we use two opencode hooks that reach the same goal:
| Goal (Kimi API) | Achieved via |
|---|---|
top-level tools[] stays lean |
tool.definition hook rewrites descriptions to short stubs so the model's attention isn't consumed |
search_tools reveals full definitions |
custom tool_search tool, activated tools stay visible for the rest of the session |
| Activated tools persist across turns | in-memory per-session set, keyed by tool id |
tool_choice: required on the first turn |
not needed — the system-prompt hint tells the model to search first |
Same effect, different plumbing.
Install
npm i -g @yulimfish/opencode-tool-search # or per-project
Add to your ~/.config/opencode/opencode.jsonc:
{
"plugin": ["@yulimfish/opencode-tool-search"]
}
By default, everything under mcp_* / mcp__* gets collapsed. Restart opencode.
Configure
Two ways, use whichever you like — options passed through plugin win over the
JSON file for scalar fields; the tools map is merged.
1) inline in opencode.jsonc
{
"plugin": [
["@yulimfish/opencode-tool-search", {
"hide": ["mcp_*", "figma_*", "yuque_*"],
"keep": ["mcp_critical_tool"],
"topK": 6,
"descriptionLimit": 800,
"injectSystemHint": true
}]
]
}
2) stand-alone ~/.config/opencode/tool-search.json
Handy for large tag catalogs:
{
"hide": ["mcp_*", "yuque_*"],
"topK": 5,
"tools": {
"yuque_yuque_search": {
"tags": ["knowledge", "wiki", "notes", "语雀"]
},
"yuque_yuque_get_doc": {
"tags": ["read", "wiki", "语雀"]
},
"mcp_github_create_issue": {
"tags": ["github", "issue", "create"],
"description": "Create a GitHub issue in the given repo. Args: owner, repo, title, body."
}
}
}
Options reference
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean | true |
Master switch. |
hide |
string[] | ["mcp_*","mcp__*"] |
Wildcard patterns for tools to collapse. * = any run of chars, ? = single, re:... = raw regex. |
keep |
string[] | [] |
Force-visible patterns; wins over hide. (tool_search is always kept.) |
topK |
number | 5 |
Max results per search. |
descriptionLimit |
number | 600 |
Chars per description before truncation. |
injectSystemHint |
boolean | true |
Whether to append a hint paragraph to system prompts telling the model that tool_search exists. |
tools |
object | {} |
Per-tool overrides: { toolId: { tags?: string[], description?: string } }. Tags feed the search index; a custom description replaces the LLM-facing one. |
How the runtime behaves
- Every request — for each tool opencode would send to the LLM, we hit the
tool.definitionhook. If the tool matcheshideand hasn't been activated this session, we replace its description with[hidden — call tool_search(…) to reveal]. Parameters schemas are left intact (so the model can still call the tool without another round trip if it's confident). - Model calls
tool_search— we rank by name/tag/description overlap, return the top K with their real descriptions + parameters, and mark each result as activated for the rest of the session. - Model calls any tool — we auto-activate it via
tool.execute.after, so future turns get the full description without another search. - System prompt — via
experimental.chat.system.transform, we append a short hint listing how many tools are currently collapsed.
Verifying it works
After restart, prompt the model with a request that would hit a hidden MCP tool.
It should call tool_search first. In the log stream:
[tool-search] plugin initialized
[tool-search] tool_search executed { query: 'github issue', hits: [...] }
Or manually:
> use tool_search to find any tool related to "figma"
Design notes
- Zero runtime dependencies. Fuzzy matching is bag-of-words — good enough for a few hundred tools; if your workspace has thousands, open an issue.
- Session-scoped activation only. Restarting opencode resets which tools are "revealed". This is intentional — new sessions should start with the smallest prompt possible.
- Doesn't touch
tools[]on the wire. We can't; that surface isn't exposed. What we can do is make the descriptions worthless-looking so the model won't waste attention on them.
License
MIT © Yulimfish · contact: epeiuss@waterflames.cn