Back to Cherry Studio

Tool Registry

docs/references/ai/tool-registry.md

2.0.38.8 KB
Original Source

Tool Registry

Model

ts
interface ToolEntry {
  name: string         // wire-name, what the LLM emits in tool_calls
  namespace: string    // ownership key (web, kb, mcp:<serverId>, meta) — never shown to the model
  namespaceLabel?: string // what `tool_search` groups by and shows; defaults to `namespace`
  description: string  // one-line summary for `tool_search`
  defer: 'never' | 'always' | 'auto'
  tool: Tool           // AI SDK Tool (schema + execute + needsApproval + toModelOutput)
  applies?(scope): boolean
}

registry (src/main/ai/tools/adapters/aiSdk/registry.ts) is a process-wide singleton. Tool files register at module-import time; the registry is read at request time by buildAgentParams. The Claude Code runtime has a separate tool system — tools/adapters/claudeCode/agentTools.ts builds its descriptors from MCP servers and built-in descriptors directly; it does not consume this aiSdk ToolRegistry.

Tests construct their own new ToolRegistry() to avoid singleton pollution.

Wire-name convention

Double underscore is the segment separator (so internal single _ stays unambiguous):

SourceName patternExample
Built-infixed wire name (<namespace>_<verb>)web_search, kb_search
MCP (AI SDK)mcp__<server-slug>__<tool-slug>_<identity-digest>mcp__gmail__sendMessage_a1b2c3d4e5f60718293a
Metatool_<verb>tool_search, tool_invoke, tool_inspect (tool_exec is defined but not injected — see below)

The built-in wire names live in @shared/ai/builtinTools (single-underscore, e.g. web_search); they are not derived from a __ segment convention like MCP. The AI SDK MCP digest is derived from the stable server id plus the original protocol tool name. The readable slugs romanize Han characters (tiny-pinyin) so CJK names still produce a meaningful segment; kana and Hangul do not romanize and fall back to server / tool plus the digest. Claude Code keeps its separate runtime naming contract.

Built-in tools

src/main/ai/tools/adapters/aiSdk/builtin/ registers four entries:

  • web_search (WebSearchTool.tscreateWebSearchToolEntry) — namespace web. Talks to the configured web-search provider via the renderer-shared search service.
  • web_fetch (WebFetchTool.tscreateWebFetchToolEntry) — namespace web. Fetches a URL's content.
  • kb_search (KnowledgeSearchTool.ts) — semantic search over the active knowledge base.
  • kb_list (KnowledgeListTool.ts) — enumerate available knowledge bases / documents.

Registration happens in builtin/registerBuiltinTools.ts (registerBuiltinTools). Each tool's applies gates on the relevant assistant.settings.* flag (e.g. enableWebSearch).

MCP tools

src/main/ai/tools/adapters/aiSdk/mcp/:

  • resolveAssistantMcpToolIds — assistant's enabled MCP servers + per-tool disable list → set of tool ids.
  • mcpTools.syncMcpToolsToRegistry({ selectedToolIds }) — scans active servers' cache-only catalogs via McpCatalogService.listTools, matches full tool ids, and registers only exact selections as ToolEntry objects whose tool.execute proxies through the MCP transport. The scan stops early once every selected id has been claimed. Ownership uses the stable namespace: mcp:<serverId>; display names never determine it, and namespaceLabel: mcp:<serverName> is what tool_search groups by and shows the model. Because reads are last-known-good cache snapshots, a transient catalog failure does not evict a still-active server's prior entries.

The sync is idempotent; a stale entry is overwritten on the next sync.

Tool catalog reads never block on MCP

McpCatalogService splits the MCP tool catalog into a read facade and a write/refresh path:

  • listTools(serverId) is cache-only — it returns the shared mcp.tools.<serverId> cache and never connects to the upstream MCP server. Every hot path that builds an agent/chat's tool surface uses it: the Claude Code SDK bridge (createSdkMcpServerInstance), buildMcpToolMetadata, the agent tool-policy (agentTools.listMcpDescriptors), and the two AI-SDK adapters above. A dead or slow server therefore cannot block agent/chat startup (issue #16242).
  • refreshTools(serverId) (and the private listToolsForServer) is the live path that connects, lists, and writes the cache. It is driven entirely by background warmers: prewarmActiveServerTools (at onReady), the onToolListChanged refresh, the renderer's on-demand refreshTools (via useAgentTools), the server-enable toggle, and restartServer.

listTools also fires a single non-blocking refreshTools the first time it sees a never-warmed server (cache undefined, distinct from a warmed-but-empty []), so headless/cron starts self-warm without re-probing dead servers.

Trade-off: tool availability is eventually consistent. A server whose cache is still cold when a session starts contributes no tools to that session and appears on the next one — the Claude Agent SDK snapshots the tool list per session, so this cannot be made live mid-session.

Meta-tools

src/main/ai/tools/adapters/aiSdk/meta/ defines four tools that turn the registry into a search-then-call interface for the model. Only the first three are injected:

ToolInjected?Use
tool_searchyesBrowse the deferred pool by namespace + query, returns brief descriptions
tool_inspectyesEmit a JSDoc stub for one tool — enough to call it correctly
tool_invokeyesInvoke any registry tool by name with a JSON arg blob
tool_execnoSandboxed JS exec with the full registry as a global API (meta/exec/runtime.ts, meta/exec/worker.ts) — defined but intentionally not injected

The injected three are added to the tool set by applyDeferExposition when (and only when) the request actually defers tools. See below.

Defer exposition

src/main/ai/tools/adapters/aiSdk/exposition/:

  • shouldDefer(entries, contextWindow) — returns the set of names to defer. Two gates above the simple threshold:

    • MIN_AUTO_DEFER_COUNT — the auto pool must be large enough that search-then-invoke beats inlining.
    • META_TOOLS_OVERHEAD_TOKENS — estimated savings must exceed the meta-tools' static prompt cost. Without these gates, small tool sets
      • small-context models trigger defer and pay net-negative tokens.
  • applyDeferExposition(tools, registry, contextWindow) — strips the deferred names out of tools, injects tool_search / tool_inspect / tool_invoke, and returns the entries the system-prompt's <DEFERRED_TOOLS> section needs to enumerate (so the model knows what namespaces exist).

Approval-gated tools are never deferred. A force-prompt MCP tool is registered with defer: 'never'mcp/mcpTools.ts reads isMcpToolForcePromptBySource once to drive both defer and needsApproval — so it stays inline and the SDK's native approval gate fires on it. Deferring it would drop it from the SDK tool-set, so the gate would never fire and it would be reachable only through tool_invoke with no approval card. As a runtime backstop the tool_invoke / tool_exec meta-tools also call isApprovalGated at execution time and refuse a gated tool (covering the registry.getByName(any-name) vector), steering the model to call it inline. See Tool Approval.

tool_exec is not injected by applyDeferExposition — there is no metaTools.exec flag. The injection site (applyDeferExposition.ts:50-53) deliberately leaves it out: its worker_threads + new Function sandbox runs model-authored code with full Node privileges, a privilege-escalation surface vs the renderer's prior restrictions. It is meant to be re-enabled behind an explicit Preference key once there is a concrete need.

applies and tool-call repair

  • applies(scope: ToolApplyScope) — per-entry predicate consulted at registry.selectActive. Throws are caught and treated as "inactive" with a warning log.
  • createAiRepair(...) (tools/adapters/aiSdk/repair.ts) — passed to AI SDK as experimental_repairToolCall. When the model emits malformed args (InvalidToolInputError), the repair function gets one chance to fix it via a follow-up LLM call. Other failures (e.g. an unknown tool name) are returned unrepaired.

Where to read more

  • Code: src/main/ai/tools/adapters/aiSdk/ (Claude Code adapter: src/main/ai/tools/adapters/claudeCode/)
  • Tests: tools/adapters/aiSdk/__tests__/, tools/adapters/aiSdk/builtin/__tests__/, tools/adapters/aiSdk/exposition/__tests__/, tools/adapters/aiSdk/mcp/__tests__/, tools/adapters/aiSdk/meta/__tests__/
  • Defer rationale, gate thresholds: tools/adapters/aiSdk/exposition/shouldDefer.ts (header doc + tests)
  • Approval flow: Tool Approval