Skip to Content
DocsToolsTool search

Tool search

When an agent carries dozens of tools — several MCP servers, a big in-process registry — shipping every full JSON schema on every model call costs tens of thousands of tokens and measurably lowers tool-selection accuracy. Seven typical MCP servers weigh in around 67K tokens of tool definitions before the model reads a single word of your prompt.

Tool search is progressive disclosure for tool definitions: ship cheap stubs by default, let the model browse the catalogue with a local search_tools tool, and pay for a tool’s full schema only after the model actually uses it.

from loomflow import Agent, Tuning agent = Agent( "You are an ops assistant.", model="claude-opus-4-7", tools=big_registry, # 50+ tools, MCP servers, ... tuning=Tuning( tool_search=True, tool_search_threshold_tokens=10_000, # default keep_tools=["read", "search"], # always-on core set ), )

Off by default. Tuning(tool_search=False) is the default and the default path is byte-identical to previous behaviour — enabling it is an explicit opt-in per agent. Small tool sets below the threshold stay full even when the flag is on.


The tool-definition block is resent on every turn of the ReAct loop. With a large catalogue that means:

  • Cost — you pay for every schema on every call, even for the 45 tools the model never touches in this session.
  • Accuracy — models pick tools worse when the catalogue is huge; a short list of one-liners is easier to scan than pages of JSON Schema.

A run that uses 4 of 50 tools ships 4 full schemas + 46 one-line stubs instead of 50 full schemas — in fixtures with realistic MCP schemas the tool-block token estimate drops by more than 70%.


How it works

On each turn, when tool_search=True and the estimated token weight of the full definition block exceeds tool_search_threshold_tokens, the ReAct loop rewrites the block:

  1. Stubs. Every tool not in the keep set is reduced to its name, the first sentence of its description plus a hydration hint, and a minimal permissive schema ({"type": "object", "additionalProperties": true}).
  2. search_tools. A local catalogue-search tool is auto-injected (always with its full definition). It runs entirely in-process — no model call, no network — ranking tools by substring + keyword score and returning {name, description} matches.
  3. Hydration on use. The moment the model calls a tool — via direct call or after finding it through search_tools — that tool’s full definition ships on every subsequent turn of the session. Schemas are earned, then kept.
turn 1: [search_tools (full)] [read (full, kept)] [46 stubs...] model: search_tools("database query") turn 2: model: query_db(sql="SELECT ...") ← stub is callable turn 3: [search_tools] [read] [query_db (full, hydrated)] [45 stubs...]

Two properties make stubbing safe:

  • Stubs stay callable. The permissive schema is valid JSON Schema, so any provider accepts a direct call to a stubbed tool. Correctness is preserved because argument validation and coercion always run server-side against the real tool on dispatch — the schema the model saw never weakens what the tool enforces.
  • Safety metadata is preserved. destructive and server pass through stubbing untouched, so the approval-gate backstop and MCP attribution behave identically for stubbed and full tools.

The knobs

All three live on Tuning:

FieldDefaultEffect
tool_searchFalseMaster switch. False = byte-identical to pre-feature behaviour.
tool_search_threshold_tokens10_000Stubbing only kicks in when the estimated token weight (chars/4 over name + description + schema JSON) of the full block exceeds this.
keep_tools()Tool names that always ship full definitions — your agent’s always-on core set (read, search, …). search_tools itself is always kept.

Use keep_tools for the tools that appear in nearly every run: they skip the search round-trip entirely and the model sees their full parameter docs from turn one.

tuning = Tuning( tool_search=True, keep_tools=["read", "edit", "bash"], # hot path, never stubbed )

Dynamic catalogues

The search_tools tool re-lists the live host on every search, so hosts whose catalogue changes mid-run stay searchable — an MCP server that sends listChanged and grows new tools is picked up on the next search without any re-wiring. See MCP resources, prompts + sampling for how the registry refreshes.

Hydration state is session-scoped: it lives on the session’s metadata, so stop-hook re-iterations of the same run keep their earned schemas, while a fresh run starts clean — no leakage across runs or users.


When not to use it

  • Small tool sets. Under the threshold nothing changes anyway; leave the flag off and save the mental overhead.
  • Single-shot structured extraction. If the run is one turn with one known tool, put that tool in keep_tools — or skip tool search and pass only the tools you need.
  • Schema-critical first calls. If a tool has a subtle schema and the model must get the first call right without a discovery turn, keep it in keep_tools.

Relationship to code mode. Tool search shrinks the definition block but the model still emits one tool_use per call, and every tool result lands in context. Code mode attacks the other half of the problem: intermediate results stay out of context because the model calls tools from Python code. The two are complementary.

Last updated on