Ratel Docs
Learn more

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; in TypeScript, one ratel() core holds both — r.tools and r.skills. 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

This section is the concept: reusable playbooks your agent discovers at runtime. Ratel's own audit and engagement skills for coding agents ship with the Skillset.

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_search and skill_invoke trace 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.

FieldRequiredIndexed for rankingPurpose
idyesnolookup key for get_skill_content
nameyesyesranking signal, matched whole and identifier-split
descriptionyesyesranking signal; shown in the skills bucket, compacted to 160 chars
tagsno, []yesauthor-declared labels and task phrases ("frontend", "login form") folded into the indexed text
toolsno, []noids of tools the playbook calls; pulled into the tools bucket when the skill matches
metadatano, {}nofree-form context for higher layers, e.g. {"stacks": ["react"]}; never matched as query terms
bodyno, ""nothe 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

In TypeScript, ratel() wires this for you: register on r.skills, and the search_capabilities and get_skill_content from r.modelTools() share the core's skill catalog — the skills bucket is always advertised, regardless of registration order.

Assembling the funnel from the factories instead (the only path in Python)? 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 [].

On that path, register skills before building the search tool: by default its description advertises the skills bucket and get_skill_content only when the catalog is non-empty at construction time. In TypeScript (0.5.1+), SearchCapabilitiesOptions.advertiseSkills pins the clause on or off explicitly — ratel().modelTools() uses it so the exposed set never depends on registration order.

import asyncio

from ratel_ai import Skill, SkillCatalog, ToolCatalog, get_skill_content_tool, search_capabilities_tool

catalog = ToolCatalog()  # ...register tools...
skills = SkillCatalog()
asyncio.run(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 { ratel } from "@ratel-ai/sdk";

const r = ratel(); // ...register tools on r.tools...
await r.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. ...",
});

// Both tools share the core's skill catalog: the skills bucket is live.
const { search_capabilities, get_skill_content } = r.modelTools();

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

On this page