How It Works
When you add a search transform,list_tools() returns just two synthetic tools instead of the full catalog:
search_toolsfinds tools matching a query and returns their full definitionscall_toolexecutes a discovered tool by name
"email" would match a tool named send_email, a tool with “email” in its description, or a tool with an email_address parameter.
Search results are returned in the same JSON format as list_tools, including the full input schema, so the LLM can construct valid calls immediately without a second round-trip.
Search Strategies
FastMCP provides two search transforms, plus an experimental third. They share the same interface — two synthetic tools, same configuration options — but differ in how they match queries to tools.Regex Search
RegexSearchTransform matches tools against a regex pattern using case-insensitive re.search. It has zero overhead and no index to build, making it a good default when the LLM knows roughly what it’s looking for.
search_tools call takes a pattern parameter — a regex string:
BM25 Search
BM25SearchTransform ranks tools by relevance using the BM25 Okapi algorithm. It’s better for natural language queries because it scores each tool based on term frequency and document rarity, returning results ranked by relevance rather than filtering by match/no-match.
search_tools call takes a query parameter — natural language:
Jev Search (Experimental)
JevSearchTransform ranks tools with TypeSafe’s Jev, a model that returns calibrated probabilities over options you define instead of generated text. It reads the query and the tool descriptions for meaning, so "archive last week's invoices" finds archive_invoices without sharing a token with it, and a request that no tool serves comes back empty instead of returning the least-wrong match.
search_tools call takes a natural-language query, the same as BM25:
- Wide pass. One request per chunk of the catalog. The query is the state, each tool name is an option, and its one-line summary is the option’s description. Each chunk’s top
shortlistgoes forward. If more than3 * shortlistcandidates survive, they are ranked again in chunks until the close read fits. - Close read. One request over the candidates with each tool’s full description and parameters. A Choice question decides which candidate fits best and sets the order. One yes/no question per candidate asks whether that tool does what the query asks; candidates below
fit_thresholdare dropped, which is how an off-topic query returns nothing.
3 * shortlist skips the wide pass.
fit_threshold comes from TypeSafe’s skill-suggestion cookbook; the other defaults were chosen for this transform and none were tuned on your catalog. Log what search_tools returns for real queries before relying on the threshold. Tool descriptions are model input: a description written to argue for its own selection can move the ranking, so treat catalogs from third-party servers accordingly.
Which to Choose
Use regex when your LLM is good at constructing targeted patterns and you want deterministic, predictable results. Regex is also simpler to debug — you can see exactly what pattern was sent. Use BM25 when your LLM tends to describe what it needs in natural language, or when your tool catalog has nuanced descriptions where relevance ranking adds value. BM25 handles partial matches and synonyms better because it scores on individual terms rather than requiring a single pattern to match. Use Jev when queries and tool descriptions rarely share vocabulary, when the catalog has lookalike tools that only a full read separates, or when returning nothing for an unserved request matters. It costs a network call per search and needs an API key.Configuration
All search transforms accept the same configuration options.Limiting Results
By default, search returns at most 5 tools. Adjustmax_results based on your catalog size and how much context you want the LLM to receive per search:
Pinning Tools
Some tools should always be visible regardless of search. Usealways_visible to pin them in the listing alongside the synthetic tools:
list_tools so the LLM can call them without searching. They’re excluded from search results to avoid duplication.
Custom Tool Names
The default namessearch_tools and call_tool can be changed to avoid conflicts with real tools:
The call_tool Proxy
The call_tool proxy forwards calls to the real tool. When a client calls call_tool(name="search_database", arguments={...}), the proxy resolves search_database through the server’s normal tool pipeline — including transforms and middleware — and executes it.
The proxy rejects attempts to call the synthetic tools themselves. call_tool(name="call_tool") raises an error rather than recursing.
Tools discovered through search can also be called directly via
client.call_tool("search_database", {...}) without going through the proxy. The proxy exists for LLMs that only know about the tools returned by list_tools and need a way to invoke discovered tools through a tool they can see.Auth and Visibility
Search results respect the full authorization pipeline. Tools filtered by middleware, visibility transforms, or component-level auth checks won’t appear in search results. App-only tools are excluded too. A MCP app can declare backend tools intended for its UI, and hosts use that declaration to omit them from the model’s tool list. A search result is tool output rather than an advertised listing, so the search transform applies that filtering itself. Itscall_tool proxy also excludes app-only tools. App visibility is not a security boundary: clients can still call these tools directly through MCP, subject to the server’s authentication and authorization checks.
The search tool queries list_tools() through the complete pipeline at search time, so the same filtering that controls what a client sees in the listing also controls what they can discover through search.
ctx.disable_components()) are also reflected immediately in search results.
