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.
Ratel manages three kinds of capabilities: local tools (functions in your code), MCP server tools (from upstream servers), and skills (Markdown playbooks).
They live in two catalogs — ToolCatalog for tools, SkillCatalog for skills — behind one contract. Everything is registered once, ranked per turn by search_capabilities, and loaded on demand. See Architecture for how the pieces fit.
Tools & MCP servers
A local tool is a schema plus a handler you register directly. An upstream MCP server contributes its tools through the same path.
Both land in the same ToolCatalog and rank as one surface: search_capabilities scores them together, and invoke_tool runs whichever wins. The agent never cares where a tool came from.
See Register tools (TypeScript, Python) for the how.
Skills
A skill is a reusable Markdown playbook — a deploy runbook, a debugging checklist — registered with ranked metadata so an agent finds it at the moment it matters. Where a tool is executed, a skill is read, not executed: the body is the payload.
The agent discovers a skill through search_capabilities and pulls the full body into context with get_skill_content — two of the three progressive disclosure tools. Then it follows the instructions.
Skills vs tools
A tool is one operation: the model fills in arguments, the runtime executes. A skill is instructions: a multi-step procedure the agent carries out itself. When the knowledge is "how to do this well" rather than "one operation to run", instructions beat schemas.
ADR-0005 makes two properties load-bearing:
- Read, not executed. Nothing runs when a skill loads.
- Advisory. A task completes without the skill, just worse — so surfacing fails silently and must be measurable. The SDK emits
skill_searchandskill_invoketrace events; see Telemetry.
The advisory property is why there is no standalone skill-search tool. The agent must search for tools anyway, so skills ride in the same search_capabilities response: the tool's necessity carries the skill. ADR-0005 rejected the alternatives — separate skill-search (skippable), one merged ranked list (starvation, incomparable scores), and a related-skills side-channel (a redundant third mechanism).
The skill shape
A skill is { id, name, description, tags, tools, metadata, body }. Only id, name, and description are required; the rest defaults to empty in both SDKs.
| Field | Required | Indexed for ranking | Purpose |
|---|---|---|---|
id | yes | no | lookup key for get_skill_content |
name | yes | yes | ranking signal, matched whole and identifier-split |
description | yes | yes | ranking signal; shown in the skills bucket, compacted to 160 chars |
tags | no, [] | yes | author-declared labels and task phrases ("frontend", "login form") folded into the indexed text |
tools | no, [] | no | ids of tools the playbook calls; pulled into the tools bucket when the skill matches |
metadata | no, {} | no | free-form context for higher layers, e.g. {"stacks": ["react"]}; never matched as query terms |
body | no, "" | no | the Markdown payload returned by get_skill_content |
Worth memorizing: tags are query terms; metadata is carried context, never matched. body is deliberately excluded from the index, so a 15 KB playbook never skews relevance.
A separate corpus, an independent budget
SkillCatalog is the on-demand analog of ToolCatalog: register once, search ranks by relevance, the body is fetched only on load. Its separate corpus ranks name, description, and tags — BM25 by default, semantic or hybrid when enabled.
search_capabilities returns two independently ranked buckets, tools and skills, each with its own top-K budget (topKSkills defaults to 3, topKTools to 5, each capped at 50). Scores are never compared across the two corpora, so a relevant skill can never be crowded out by a pile of matching tools.
Declared tools ride in
When a skill matches, search_capabilities also pulls its declared tools into the tools bucket — playbook and toolkit in one turn. The mechanics:
- Additive: pulled-in tools do not count against
topKTools. - Score 0: a query-matched tool keeps its score; a skill-declared dependency rides in at 0.
- Deduped: a tool that matched both ways appears once.
- Must exist in the
ToolCatalog: unknown declared ids are silently skipped.
Wiring
Pass the SkillCatalog as the second argument to the search factory, and hand the agent get_skill_content built from the same catalog. Without it, the skills bucket is always [].
Register skills before building the search tool: its description advertises the skills bucket and get_skill_content only when the catalog is non-empty at construction time.
from ratel_ai import Skill, SkillCatalog, ToolCatalog, get_skill_content_tool, search_capabilities_tool
catalog = ToolCatalog() # ...register tools...
skills = SkillCatalog()
skills.register(
Skill(
id="vercel-deploy",
name="vercel-deploy",
description="How to deploy to Vercel: env vars, preview vs production, rollbacks.",
tags=["deploy", "ship to production"], # indexed: query terms
tools=["vercel__deploy", "fs__read_file"], # dependency edge, not indexed
metadata={"stacks": ["next", "vercel"]}, # carried context, never matched
body="## Deploying to Vercel\n1. ...",
)
)
search = search_capabilities_tool(catalog, skills) # 2nd argument: skills bucket is live
load = get_skill_content_tool(skills) # id: "get_skill_content"import { SkillCatalog, ToolCatalog, getSkillContentTool, searchCapabilitiesTool } from "@ratel-ai/sdk";
const catalog = new ToolCatalog(); // ...register tools...
const skills = new SkillCatalog();
skills.register({
id: "vercel-deploy",
name: "vercel-deploy",
description: "How to deploy to Vercel: env vars, preview vs production, rollbacks.",
tags: ["deploy", "ship to production"], // indexed: query terms
tools: ["vercel__deploy", "fs__read_file"], // dependency edge, not indexed
metadata: { stacks: ["next", "vercel"] }, // carried context, never matched
body: "## Deploying to Vercel\n1. ...",
});
const search = searchCapabilitiesTool(catalog, skills); // 2nd argument: skills bucket is live
const load = getSkillContentTool(skills); // id: "get_skill_content"Ratel Local loads skills from disk and manages them from the CLI — see Managing skills.
How the agent uses a skill
search_capabilities surfaces the skill
The user says "ship this to production" and no deploy tool is in context. The agent searches; the skill ranks in the skills bucket, and its declared tools join the tools bucket at score 0:
{
"tools": {
"groups": [
{ "server": { "name": "vercel" }, "hits": [{ "toolId": "vercel__deploy", "score": 0, "description": "...", "inputSchema": {} }] },
{ "server": { "name": "fs" }, "hits": [{ "toolId": "fs__read_file", "score": 0, "description": "...", "inputSchema": {} }] }
]
},
"skills": [
{ "skillId": "vercel-deploy", "score": 1.42, "description": "How to deploy to Vercel: env vars, preview vs production, rollbacks." }
]
}get_skill_content pulls the body
get_skill_content({ "skillId": "vercel-deploy" }) returns { "body": "## Deploying to Vercel\n1. ..." }. An unknown id returns a structured { error, isError: true } pointing back to search_capabilities — never a crash.
The agent follows the playbook
The full Markdown is now in context. The agent works through the steps, running the tools via invoke_tool. Loading the skill executed nothing; the model does the work.
Next steps
Register tools (TypeScript)
Wire local functions and MCP servers into the ToolCatalog.
Register tools (Python)
The same registration flow, idiomatic Python.
Progressive disclosure
The full contract of search_capabilities, invoke_tool, and get_skill_content.
Keyword search
How BM25 ranks both catalogs and how to write retrievable definitions.