Ratel Docs
TypeScript SDK

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.

Next steps

On this page