Use the discovery tools
Wrap the catalog in search_capabilities and invoke_tool, assemble each turn's toolset with a top-K pre-filter, and pick a retrieval method per call.
searchCapabilitiesTool and invokeToolTool wrap a catalog into two tools an agent can call itself. Hand them to your loop and the model gets self-service access to the whole catalog without it living in the prompt. The language-neutral contract (result shapes, defaults, and the error table) lives on Progressive disclosure.
const search = searchCapabilitiesTool(catalog); // id: "search_capabilities" (SEARCH_CAPABILITIES_ID)
const invoke = invokeToolTool(catalog); // id: "invoke_tool" (INVOKE_TOOL_ID)Both return plain ExecutableTool objects ({ id, name, description, inputSchema, outputSchema, execute }). Wrap each one in your framework's tool type — see Framework integrations.
search_capabilities
search_capabilities({ query, topKTools?, topKSkills? }) returns two independently-ranked buckets, so a relevant skill is never crowded out by matching tools:
{
"tools": {
"groups": [
{
"server": { "name": "fs" }, // grouped by server (the id prefix before "__")
"hits": [
{ "toolId": "fs__read_file", "score": 1.42, "description": "...", "inputSchema": {} }
]
}
]
},
"skills": [{ "skillId": "deploy-vercel", "score": 0.9, "description": "..." }]
}The result is typed SearchCapabilitiesResult: CapabilityToolHits grouped per server (CapabilityToolGroup), plus CapabilitySkillHits. topKTools defaults to 5 and topKSkills to 3; a missing, non-integer, or below-1 value falls back to that default, and integers above 50 are clamped to 50.
The skills bucket is always present and stays empty until you pass a SkillCatalog as the second argument. An optional third argument advertises upstream MCP servers in the tool's description — see Register MCP servers.
Upgrading from 0.1.x?
searchToolsTool (id search_tools, constant SEARCH_TOOLS_ID) is still exported as
a deprecated, tools-only shim that keeps its original { groups } result shape and
topK input. Removal is tracked as RAT-250; migrate to searchCapabilitiesTool.
invoke_tool
invoke_tool({ toolId, args }) runs catalog.invoke(toolId, args) and returns the tool's result. Arguments go nested under args; a flattened call with no args is tolerated, with the remaining top-level keys treated as the arguments. On a bad call it returns a structured { error, isError: true } instead of throwing, so a model mistake (an unknown id, a non-object args, a handler that throws) stays recoverable inside the loop rather than crashing the host.
One error is special-cased for MCP auth: when the handler throws an error named UnauthorizedError, the tool returns { error: "needs_auth", isError: true, hint: "call the auth tool to re-authorize <upstream>", upstream? }, inferring the upstream server from the <server>__<tool> id. Pass invokeToolTool(catalog, { onUnauthorized }) (InvokeToolToolOptions) to be notified, sync or async, before that result returns, for example to kick off a re-auth flow.
Skill content
With a SkillCatalog registered, a third capability tool joins the set. It returns a skill's full body on demand:
const search = searchCapabilitiesTool(catalog, skills); // 2nd arg → result gains a populated `skills` bucket
const load = getSkillContentTool(skills); // id: "get_skill_content" (GET_SKILL_CONTENT_ID)Assemble the per-turn toolset
Most agents pair the capability tools with a top-K pre-filter: before each model call, ask the catalog for the few tools most relevant to the user's message and put those in the tool list. The pre-filter covers the common case in the prompt; the capability tools are the escape hatch for everything else.
// Each turn, assemble the tools the model is allowed to see:
function toolsForTurn(userMessage: string): ExecutableTool[] {
const capabilities = [searchCapabilitiesTool(catalog), invokeToolTool(catalog)];
const topK = catalog
.search(userMessage, 3) // BM25: the 3 most relevant tools for this message
.map((hit) => catalog.getExecutable(hit.toolId))
.filter((t): t is ExecutableTool => t !== undefined);
return [...capabilities, ...topK];
}Retrieval methods
Every search ranks with one of three methods, SearchMethod = "bm25" | "semantic" | "hybrid", selected per catalog (the method constructor option) or per call (the fourth search argument, which overrides the catalog default):
// Per catalog: every register eagerly embeds the new tool, so a search
// only ever embeds the query.
const catalog = new ToolCatalog({ method: "hybrid" });
// Per call, on a BM25-default catalog: build the embedding cache first.
const lexical = new ToolCatalog();
// ...register tools...
lexical.buildEmbeddings(); // synchronous, incremental
lexical.search("rotate the api key", 5, "direct", "semantic");Semantic and hybrid rank against a prebuilt embedding cache and throw if it is not built; a search never embeds the corpus. How to build and maintain that cache — eager catalogs vs an explicit buildEmbeddings() — is on Build the embedding cache. search_capabilities passes no method: it always uses the catalog's construction-time default, so switching the agent's engine means constructing the catalog with it.
How each method ranks is covered once, centrally: BM25 and the text projection in Keyword search, the semantic and hybrid engines (and the local embedding model's footprint) in Semantic & hybrid search.