Progressive disclosure
Start with a minimal tool surface and pull in more on demand: the contracts for search_capabilities, invoke_tool, and get_skill_content.
An agent's direct tool list should carry only what the current turn needs. Everything else stays in the catalog, out of the prompt.
Progressive disclosure is the model's path back to that whole catalog. Three capability tools are the mechanism: search_capabilities finds by intent, invoke_tool runs a hit by id, get_skill_content loads a skill's playbook. They are the escape hatch when the pre-filtered set is not enough; hits are ranked by the catalog's configured retrieval method.
Their descriptions and JSON schemas are a product contract shown to the model, kept verbatim across the Python and TypeScript SDKs. Ratel Local registers these same objects as its MCP tools. Parameter and result keys are camelCase in both languages (toolId, topKTools, inputSchema) — the wire contract is language-neutral.
search_capabilities
One natural-language query returns two independently-ranked buckets: tools (executable, run one via invoke_tool) and skills (reusable playbooks, load one via get_skill_content). Each bucket has its own result budget and corpus, ranked with that catalog's selected method — BM25 by default, semantic and hybrid opt-in. A relevant skill is never crowded out by matching tools, and scores are never compared across the two.
| Parameter | Type | Required | Behavior |
|---|---|---|---|
query | string | yes | describe what you want to do |
topKTools | integer | no | max tools to return, default 5 |
topKSkills | integer | no | max skills to return, default 3 |
Top-K values must be positive integers, capped at 50. Anything else (missing, 0, negative, fractional) falls back to the default — a stray value can never return zero results or an unbounded set.
{
"tools": {
"groups": [
{
// grouped by server: the toolId prefix before the first "__"
"server": { "name": "fs", "description": "filesystem helpers" },
"hits": [
{
"toolId": "fs__read_file",
"score": 1.87, // BM25 score of the query match (illustrative)
"description": "Read a file from local disk.",
"inputSchema": { "properties": { "path": { "type": "string", "description": "path to read" } } }
}
]
}
]
},
"skills": [
{
"skillId": "vercel-deploy",
"score": 1.42,
"description": "How to deploy to Vercel: env vars, preview vs production, rollbacks."
}
]
}- Both keys are always present: no skill catalog wired means
skillsis[]; no tool matches meanstools.groupsis[]. - Hits are grouped by server, in first-hit order. A hit's server is its
toolIdprefix before the first"__"; an id with no separator is its own group. - Each hit carries exactly
toolId,score,description(the raw registered description), andinputSchema. Skill hit descriptions are compacted: whitespace collapsed, clipped to 160 characters on a word boundary. - A matched skill's declared
toolsare pulled into the tools bucket atscore: 0— additive beyondtopKTools, deduped against query hits — so the agent gets a playbook and its toolkit in one turn. See Tools, MCP & Skills.
invoke_tool
Runs a catalog tool by id: catalog.invoke(toolId, args) under the hood.
| Parameter | Type | Required | Behavior |
|---|---|---|---|
toolId | string | yes | id of the tool to invoke |
args | object | yes | the tool's arguments, matching its inputSchema, nested as a single object |
The nested-args contract: arguments go under args, never flattened to the top level. The runtime forgives the common model mistake — when args is missing or null, the remaining top-level keys (minus toolId) become the arguments — though a host that validates the input schema first may reject the flattened form. A non-null args that is not an object is a structured error; nothing is forwarded.
On success you get the tool's raw result back, untouched, no envelope:
// invoke_tool({ "toolId": "fs__read_file", "args": { "path": "/tmp/x" } })
{ "contents": "contents of /tmp/x" }invoke_tool never throws to the caller. Every failure returns { "error": "...", "isError": true }, so a model mistake stays recoverable inside the loop instead of crashing the host:
| Failure | Returned error |
|---|---|
Unknown or missing toolId | unknown toolId: <id>. Use the catalog's search tool to discover available ids. |
Non-object args | invalid args for <id>: `args` must be an object containing the tool's arguments. |
Tool throws UnauthorizedError | needs_auth, always with a re-auth hint; when the upstream server is inferable from the id prefix, an upstream key names it and the hint appends its name |
| Tool throws anything else | tool <id> threw: <message> |
The factory takes an optional callback (onUnauthorized in TypeScript, on_unauthorized in Python, sync or async) invoked with the upstream server name on the needs_auth path.
get_skill_content
The read counterpart to invoke_tool: skills are read, not executed. The flow: search_capabilities surfaces a skill in the skills bucket, get_skill_content pulls its complete playbook into context, the agent follows it. Bundled scripts or files are referenced by absolute path inside the body.
| Parameter | Type | Required | Behavior |
|---|---|---|---|
skillId | string | yes | id of the skill to load |
// get_skill_content({ "skillId": "vercel-deploy" })
{ "body": "## Deploying to Vercel\n1. ..." } // the full SKILL.md Markdown
// unknown or missing skillId: a structured error, never a throw
{ "error": "unknown skillId: <skillId>. Use search_capabilities to discover available ids.", "isError": true }The output schema declares body, error, and isError all optional, deliberately. Both shapes must validate as structured content; requiring body would turn the error branch into a protocol error instead of the declared structured error.
Constructing the capability tools
Each factory wraps your catalog and returns a plain ExecutableTool whose id equals its name, ready to hand to your agent loop:
from ratel_ai import ToolCatalog, SkillCatalog, search_capabilities_tool, invoke_tool_tool, get_skill_content_tool
catalog = ToolCatalog()
skills = SkillCatalog()
# ...register tools and skills...
search = search_capabilities_tool(catalog, skills) # id: "search_capabilities"
invoke = invoke_tool_tool(catalog) # id: "invoke_tool"
load = get_skill_content_tool(skills) # id: "get_skill_content"import { ToolCatalog, SkillCatalog, searchCapabilitiesTool, invokeToolTool, getSkillContentTool } from "@ratel-ai/sdk";
const catalog = new ToolCatalog();
const skills = new SkillCatalog();
// ...register tools and skills...
const search = searchCapabilitiesTool(catalog, skills); // id: "search_capabilities"
const invoke = invokeToolTool(catalog); // id: "invoke_tool"
const load = getSkillContentTool(skills); // id: "get_skill_content"The skill catalog is optional. Pass one with skills registered and the tool's description advertises the skills bucket to the model; without one, skills is always []. Gating is checked at construction time and an empty catalog counts as no skills — build the tool after registering.
The search factory also accepts upstream server info (upstreamServers / upstream_servers), listed in the description so the model knows what the catalog aggregates.
Every search the capability tools run is tagged origin: "agent" in trace events and OTel spans, so agent-initiated discovery stays distinguishable from host-code calls to catalog.search. See Telemetry.
Upgrading from 0.1.x?
search_tools (factories searchToolsTool / search_tools_tool) is the pre-0.2.0 search, still exported as a deprecated compatibility shim with its original behavior: a tools-only { "groups": [...] } result and a single topK input. It is not an alias of the two-bucket tool. Migrate to search_capabilities; registering both serves the old and new names during a migration window.
Next steps
Keyword search
How the default BM25 ranking scores a query — and how to write definitions it can find.
Tools, MCP & Skills
What the catalog holds: executable tools, upstream MCP servers, and skill playbooks.
Register tools (TypeScript)
Fill the catalog the capability tools search: local functions and upstream MCP servers.
Register tools (Python)
The same registration flow, idiomatic Python.
Tools, MCP & Skills
What goes into Ratel's catalogs: local tools, MCP server tools, and skills — registered once, ranked per turn by search_capabilities, loaded on demand.
Keyword search
Ratel's default retrieval: BM25 over a schema-aware text projection of every tool and skill definition — in-process, deterministic, no model, no vector DB, no service.