# Ratel documentation Source: https://docs.ratel.sh/docs Ratel Core is the foundation for context-efficient agents. Ratel Cloud and Ratel Local add companion surfaces for hosted analytics and existing MCP clients. [Start with Ratel Core](/docs/core) Build context-efficient agents with Ratel's SDKs, retrieval primitives, and capability tools. Learn the model, run the quickstart, and explore every package here. Inspect context usage and retrieval performance in a hosted analytics platform. [Ratel Local](/docs/local) Bring capability search to Claude Code, Codex, and other existing MCP clients. --- # What is Ratel? Source: https://docs.ratel.sh/docs/core Keep reading to learn more, or [get started](/docs/core/quickstart) right away. ## Why Ratel? * **Lean context:** keeps every tool schema out of the prompt with [progressive disclosure](/docs/core/progressive-disclosure), and cuts context with [benchmark-backed savings](/docs/core/efficiency). * **Zero setup:** in-process [BM25 keyword search](/docs/core/keyword-search) — no vector database, no API key, no service; [semantic & hybrid ranking](/docs/core/semantic-hybrid-search) when you opt in. * **One catalog:** [tools, MCP servers, and skills](/docs/core/tools-mcp-skills), all searchable and runnable through the same three capability tools. * **Two SDKs:** [TypeScript](/docs/core/typescript/quickstart) and [Python](/docs/core/python/quickstart) over one [Rust engine](/docs/core/architecture), with [Vercel AI SDK](/docs/core/typescript/framework-integrations) and [Pydantic AI](/docs/core/python/framework-integrations) integrations. * **Observable:** the whole search-and-invoke funnel as [local trace events or OpenTelemetry spans](/docs/core/telemetry). * **Open:** [Apache-2.0 engine, MIT everything else](/docs/core/licensing), measured in the open by [ratel-bench](/docs/core/benchmark). ## Getting started Follow the [Quick start](/docs/core/quickstart) — the fastest path lets your coding agent integrate Ratel for you — or [install](/docs/core/install) the SDK and jump straight to the [TypeScript](/docs/core/typescript/quickstart) or [Python](/docs/core/python/quickstart) quickstart. [Features](/docs/core/features) lists everything Ratel Core does, [What's new](/docs/core/whats-new) tracks releases, and the [FAQ](/docs/core/faq) answers common questions. Using Claude Code or Codex? [Ratel Local](/docs/local/quickstart) puts the same capability contract in front of your existing MCP servers. ## Support and issues For bugs and questions, open an issue on [github.com/ratel-ai/ratel](https://github.com/ratel-ai/ratel/issues). ## Community Join our brand-new [Discord](https://discord.gg/75vAPdjYqT) to ask questions and follow development. ## How it works Curious? Read [Architecture](/docs/core/architecture) for the turn-by-turn flow and [Efficiency](/docs/core/efficiency) for exactly what changes in the model's context. --- # Features Source: https://docs.ratel.sh/docs/core/features Each feature has its own short page; two are marked coming soon. [Tools, MCP & Skills](/docs/core/tools-mcp-skills) What lives in the catalog: tools, MCP servers, and Markdown skill playbooks, all searchable the same way. [Progressive disclosure](/docs/core/progressive-disclosure) The capability tools — `search_capabilities`, `invoke_tool`, `get_skill_content` — that let the model pull capabilities on demand instead of front-loading the prompt. [Keyword search](/docs/core/keyword-search) The zero-setup default: BM25 ranking over a schema-aware text projection of every definition. [Semantic & hybrid search](/docs/core/semantic-hybrid-search) Opt-in embedding-based and hybrid ranking for intent queries that keywords miss. **Adaptive ranking (coming soon)** Retrieval that learns from your agent's real usage — which results actually get invoked. **Compaction (coming soon)** Keeps long-running agent context lean without losing what matters. [Telemetry](/docs/core/telemetry) The search-and-invoke funnel as local trace events, or exported as OpenTelemetry spans. [SDKs](/docs/core/sdks) Two first-party SDKs, `@ratel-ai/sdk` (TypeScript) and `ratel-ai` (Python), over one Rust core. [Related projects](/docs/core/related-projects) The Ratel ecosystem around the Core SDKs: the core repo, Ratel Local, the benchmark, the skills suite, and Ratel Cloud. --- # Quick start Source: https://docs.ratel.sh/docs/core/quickstart The fastest way to adopt Ratel is to not write the integration yourself. Ratel ships five open-source skills for coding agents — Claude Code, Cursor, Codex, and 40+ others — that audit your agent codebase, plan its observability, integrate Ratel, and fix what the audit finds. Prefer to write the code by hand? Jump to [the SDK quickstarts](#write-the-code-yourself). > This page covers the **skills suite**, Ratel's installable skills for coding agents; for the skills *concept*, reusable playbooks registered in a `SkillCatalog`, see [Tools, MCP & Skills](/docs/core/tools-mcp-skills). ## Install the skills **npm** ```bash npx skills add ratel-ai/skills -y --all ``` **pnpm** ```bash pnpm dlx skills add ratel-ai/skills -y --all ``` **yarn** ```bash yarn dlx skills add ratel-ai/skills -y --all ``` **bun** ```bash bunx skills add ratel-ai/skills -y --all ``` That installs all five skills into the current project (`./.claude/skills/` for Claude Code, the equivalent location for other supported agents). The `-y` flag accepts all prompts; `--all` installs all skills into all agents; add `-g` to install globally (`~/.claude/skills/`). Per-skill install: **npm** ```bash npx skills add ratel-ai/skills --skill ratel-assessment ``` **pnpm** ```bash pnpm dlx skills add ratel-ai/skills --skill ratel-assessment ``` **yarn** ```bash yarn dlx skills add ratel-ai/skills --skill ratel-assessment ``` **bun** ```bash bunx skills add ratel-ai/skills --skill ratel-assessment ``` The `skills` CLI is [Vercel Labs' open agent skills tool](https://github.com/vercel-labs/skills) — compatible with Claude Code, Cursor, Codex, OpenCode, Gemini CLI, and 40+ other coding agents. The suite itself is [MIT-licensed](https://github.com/ratel-ai/skills/blob/main/LICENSE) at [`ratel-ai/skills`](https://github.com/ratel-ai/skills). ## Paste this into your coding agent Want a read on your agent first? The assessment is free and static — no setup: ```text Run npx skills add ratel-ai/skills --all and use the skills to assess the agents in this codebase ``` Ready to wire Ratel in? Go straight to the integration plan: ```text Run npx skills add ratel-ai/skills --all and use the skills to integrate Ratel in this project. ``` Either entry point pulls in the whole suite. The assessment writes a report to `.ratel/ratel-assessment-.md`, plus a branded, scored HTML version alongside it. The report ends with "Recommended next steps": every finding routes to the right follow-up skill, so you don't have to know the arc up front. ## What's inside Five skills. The first three run the engagement arc in order; the last two are the fix-skills the assessment routes to when it finds a long system prompt or weak tool/skill definitions. | Skill | What it does | When to fire | | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | [`ratel-assessment`](https://github.com/ratel-ai/skills/blob/main/skills/ratel-assessment/SKILL.md) | Static read of the agent codebase (TypeScript or Python). Writes a 12-dimension scorecard with evidence-backed findings to `/.ratel/` — markdown plus a scored HTML version. | First touch. Zero setup. | | [`ratel-observability-assessment`](https://github.com/ratel-ai/skills/blob/main/skills/ratel-observability-assessment/SKILL.md) | Detects the OpenTelemetry backend you run, plans turning on Ratel's native OTLP telemetry, and proposes the dashboards that prove value. | After assessment flags Observability as Weak / Missing. | | [`ratel-integrate`](https://github.com/ratel-ai/skills/blob/main/skills/ratel-integrate/SKILL.md) | Plans the rollout: integration mode (direct SDK / Ratel Local / hybrid), pilot scope, and an A/B test tied to Ratel's native-telemetry metrics. Covers both SDKs. | After observability is in. | | [`ratel-decompose-prompt`](https://github.com/ratel-ai/skills/blob/main/skills/ratel-decompose-prompt/SKILL.md) | Breaks a monolithic system prompt into a lean core plus extractable skills registered in a `SkillCatalog`. | When assessment flags Prompt decomposition as Weak / Missing. | | [`ratel-tune-definitions`](https://github.com/ratel-ai/skills/blob/main/skills/ratel-tune-definitions/SKILL.md) | Rewrites tool and skill definitions — descriptions, names, schemas, tags — for retrievability and model usability. Writes a before/after plan. | When assessment flags Definition quality as Weak / Missing. | ## How the skills chain ```text ratel-assessment → "here's what's weak; here's where Ratel fits" ↓ (Observability Weak/Missing) ratel-observability-assessment → "turn on Ratel's native OTLP telemetry" ↓ ratel-integrate → "here's how to roll Ratel out + A/B it" ratel-assessment also branches to two fix-skills, conditional on findings: ├─ Prompt decomposition weak → ratel-decompose-prompt └─ Definition quality weak → ratel-tune-definitions ``` The arc isn't a forced sequence — each report names which sibling to run next based on what it found. All reports land under `.ratel/` in your repo, so they accumulate and stay diff-able. ## Observability Ratel telemetry **is** OpenTelemetry: the SDKs emit the retrieval-and-tool funnel natively as `gen_ai.*` spans plus a `ratel.*` overlay, exported as stock OTLP. Point those spans at the backend you already run — Langfuse, LangSmith, your own collector, or [Ratel Cloud](/docs/cloud), coming soon. `ratel-observability-assessment` plans that setup — greenfield or dual-exported into an existing provider — and the dashboards that prove value. ## Write the code yourself Everything the skills plan and apply, you can also write by hand: register your tools and prompt playbooks in Ratel catalogs, then hand your agent the three capability tools — `search_capabilities`, `invoke_tool`, `get_skill_content`. The per-language quickstarts cover both starting from scratch and migrating an existing agent: [TypeScript quickstart](/docs/core/typescript/quickstart) Install @ratel-ai/sdk, build the catalogs, and run the discovery loop — no framework required. [Python quickstart](/docs/core/python/quickstart) The same path with ratel-ai, idiomatic Python. > **Using Claude Code or Codex as your agent runtime?** > > [Ratel Local](/docs/local/quickstart) puts the same capability contract in front of your > existing MCP servers — no code at all. --- # What's new Source: https://docs.ratel.sh/docs/core/whats-new Ratel Core versions track **`ratel-ai-core`**, the Rust engine both SDKs bundle. Its changelog is below. Each published package also keeps its own changelog; those are [listed at the end](#looking-for-a-specific-package). See [Changelog](/docs/core/packages/rust/ratel-ai-core/changelog). ## Looking for a specific package? The SDK and telemetry packages version independently. Their changelogs live in the Reference section: * **Rust** — [`ratel-ai-core`](/docs/core/packages/rust/ratel-ai-core/changelog) · [`ratel-ai-telemetry`](/docs/core/packages/rust/ratel-ai-telemetry/changelog) * **TypeScript** — [`@ratel-ai/sdk`](/docs/core/packages/typescript/sdk/changelog) · [`@ratel-ai/telemetry`](/docs/core/packages/typescript/telemetry/changelog) · [`@ratel-ai/telemetry-otlp`](/docs/core/packages/typescript/telemetry-otlp/changelog) * **Python** — [`ratel-ai`](/docs/core/packages/python/ratel-ai/changelog) · [`ratel-ai-telemetry`](/docs/core/packages/python/ratel-ai-telemetry/changelog) Each package page next to them carries the raw generated API reference — start at the [package list](/docs/core/packages). --- # FAQ Source: https://docs.ratel.sh/docs/core/faq ## Basics ### Do I need a vector database or an API key? No. The default is [BM25 keyword search](/docs/core/keyword-search) running in your process — no model, no vector database, no service, no API key. [Semantic and hybrid ranking](/docs/core/semantic-hybrid-search) are opt-in and still local: a small embedding model downloads once on first use and runs CPU-only. ### How is this different from passing all my tools to the model? Without Ratel, every schema is sent on every turn, so prompt cost scales with catalog size. With Ratel, the model reads only the top-K matches plus three capability tools, however large the catalog grows. [Efficiency](/docs/core/efficiency) shows the before/after; [progressive disclosure](/docs/core/progressive-disclosure) explains the mechanism. ### Does hiding tools behind search hurt accuracy? The [benchmark](/docs/core/benchmark) says no: on the BFCL suite, 86–91% token savings come with accuracy at or near baseline, and smaller models actually gain. See [Efficiency](/docs/core/efficiency) for the full table and [benchmark.ratel.sh](https://benchmark.ratel.sh) for live results. ### What's the difference between a skill and a tool? A tool is one operation the runtime executes. A skill is a Markdown playbook the agent reads — instructions for multi-step work, ranked in its own corpus so tools never crowd out a relevant skill. See [Tools, MCP & Skills](/docs/core/tools-mcp-skills). ## Integration ### Which languages and frameworks does Ratel support? Two first-party SDKs — [`@ratel-ai/sdk`](/docs/core/typescript/quickstart) for TypeScript and [`ratel-ai`](/docs/core/python/quickstart) for Python — with mirrored APIs over one Rust core. Framework wiring is a small adapter, documented for the [Vercel AI SDK](/docs/core/typescript/framework-integrations) and [Pydantic AI](/docs/core/python/framework-integrations). ### Can I use Ratel with Claude Code, Codex, or Cursor without writing code? Yes — [Ratel Local](/docs/local) is one MCP server that fronts your whole tool estate and exposes just the capability tools; configuration is the only integration work. Claude Code and Codex can follow the [Ratel Local quickstart](/docs/local/quickstart). Cursor uses [one manual MCP entry](/docs/local/cursor); version 0.4.0 has no Cursor import or link adapter. ### Should I use Ratel Local or the SDK? Pick [Ratel Local](/docs/local) when your tools already live behind MCP servers and you don't want to change agent code. Pick an [SDK](/docs/core/sdks) when you own the agent loop and want in-process control. Both expose the same capability tools. ### Do my existing MCP servers work with Ratel? Yes. Upstream MCP tools land in the same catalog as your local functions and rank as one surface. See [Tools, MCP & Skills](/docs/core/tools-mcp-skills) and Register MCP servers ([TypeScript](/docs/core/typescript/register-mcps), [Python](/docs/core/python/register-mcps)). ### Does retrieval bypass my tools' permissions or auth? No. Permissions, authentication, and side effects still belong to the tool and its host — Ratel only changes which definitions reach the model. See [Architecture](/docs/core/architecture). ## Search quality ### When should I switch from BM25 to semantic or hybrid search? Stay on BM25 until you see missed recall — the first fix is usually [better definitions](/docs/core/keyword-search), not a different engine. If you do opt in, hybrid is usually the safer choice. [Semantic & hybrid search](/docs/core/semantic-hybrid-search) has the decision guide. ### My tool never ranks for the queries I expect. How do I fix it? Your vocabulary is the recall mechanism: BM25 indexes names, descriptions, and schema property names, so write "what it does" plus a "use when" clause in the caller's words. The checklist is on [Keyword search](/docs/core/keyword-search). ## Privacy & performance ### Does any of my data leave my machine? Not by default: retrieval runs in-process, telemetry is off until you opt in, and message/tool content never leaves the process unless you set a content-capture mode. See [Telemetry](/docs/core/telemetry). ### Does Ratel work offline or air-gapped? BM25 needs nothing — no model, no service. Semantic and hybrid download an embedding model once, then load offline; a mirror endpoint covers air-gapped setups. Details on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ### What runtime overhead does Ratel add? BM25 retrieval runs deterministically in your process with no model or service call. Semantic search adds a one-time model download and CPU-only inference; telemetry is a no-op until you opt in. See [Telemetry](/docs/core/telemetry). ### I already run OpenTelemetry — will Ratel's telemetry conflict? No: your existing provider already receives Ratel's `ratel.*` / `gen_ai.*` spans. Skip `configureTelemetry`, and add the Ratel span processor only if you also want to dual-export. See [Telemetry](/docs/core/telemetry). ## Project ### How is Ratel licensed? Can I use it commercially? The `ratel-ai-core` engine is Apache-2.0; the SDKs, Ratel Local, and everything else are MIT. Both permit commercial use, modification, and redistribution — see [Licensing](/docs/core/licensing). ### Where do the token-savings numbers come from? From the open harness [`ratel-ai/ratel-bench`](https://github.com/ratel-ai/ratel-bench), which runs every scenario as baseline, oracle, and ratel-full. [Benchmark](/docs/core/benchmark) covers the methodology; [benchmark.ratel.sh](https://benchmark.ratel.sh) has the live results. ### What's on the roadmap? Adaptive ranking and compaction are marked coming soon on [Features](/docs/core/features). [Ratel Cloud](/docs/cloud) — hosted context analytics — is also coming soon. ### Can my agent read these docs? Yes — these docs publish [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt), regenerated from the authored content on every build. --- # Install Source: https://docs.ratel.sh/docs/core/install Ratel ships as [two SDKs over one Rust core](/docs/core/sdks), plus [Ratel Local](/docs/local) for MCP clients such as Claude Code, Codex, and Cursor. No hosted Ratel service or API key is required. ## SDK **TypeScript** ```bash npm install @ratel-ai/sdk@0.4.0 ``` ESM, **Node.js 20 or newer**. Full guide — native targets, optional packages, embedding model: [TypeScript install](/docs/core/typescript/install). Then the [TypeScript quickstart](/docs/core/typescript/quickstart). **Python** ```bash pip install "ratel-ai==0.4.0" # extras: pip install 'ratel-ai[mcp]' # register upstream MCP servers pip install 'ratel-ai[otlp]' # OTLP telemetry export ``` **Python 3.10 or newer** for these guides; the base SDK supports 3.9, the `mcp` extra needs 3.10+. Full guide — prebuilt wheels, extras, embedding model: [Python install](/docs/core/python/install). Then the [Python quickstart](/docs/core/python/quickstart). The docs pin the version they document; drop the pin to track the latest release. ## Requirements * Prebuilt native targets: macOS (`darwin-arm64`, `darwin-x64`), Linux glibc (`linux-x64-gnu`, `linux-arm64-gnu`), Windows (`win32-x64-msvc`). * On those targets nothing compiles at install time — no Rust toolchain. * [Semantic & hybrid search](/docs/core/semantic-hybrid-search) is opt-in and downloads a local embedding model on first use; BM25 needs nothing. ## Ratel Local For MCP clients, install the gateway instead of an SDK: ```bash npm install --global @ratel-ai/mcp-server@0.4.0 # binary: ratel-mcp, Node.js 20+ # or, as a Claude Code plugin: claude plugin marketplace add ratel-ai/ratel-local claude plugin install ratel-mcp@ratel ``` Claude Code and Codex wiring is covered in the [Ratel Local quickstart](/docs/local/quickstart), including import and plugin paths. Cursor uses [manual `mcp.json` setup](/docs/local/cursor). The Rust crate [`ratel-ai-core`](/docs/core/packages/rust/ratel-ai-core) underlies both SDKs; it is a reference, not an install target. ## Next steps [Quick start](/docs/core/quickstart) The fastest path in: your coding agent integrates Ratel with the skills suite. [TypeScript quickstart](/docs/core/typescript/quickstart) Register a tool and a skill with @ratel-ai/sdk, let the agent find them. [Python quickstart](/docs/core/python/quickstart) The same flow with ratel-ai. --- # Architecture Source: https://docs.ratel.sh/docs/core/architecture Ratel separates **what your agent can do** from **what the model must read right now**. Your Ratel Core application owns the full catalog. The model gets a small view selected for the current request and a controlled way to reach the rest. ## The pieces Five layers, all in your process: * **Rust engine.** [`ratel-ai-core`](/docs/core/packages/rust/ratel-ai-core) owns the registries (`ToolRegistry`, `SkillRegistry`), the three retrieval engines (BM25, semantic over a local `bge-small-en-v1.5`, hybrid fused with Reciprocal Rank Fusion), and the local trace stream. * **SDK bindings.** [`@ratel-ai/sdk` (napi) and `ratel-ai` (PyO3)](/docs/core/sdks) are thin, mirrored bindings over that same engine — in-process, no server, no API key. * **Catalogs.** [`ToolCatalog` and `SkillCatalog`](/docs/core/tools-mcp-skills) pair metadata with executable handlers and hold local tools, MCP server tools, and skills as separate corpora, so tool and skill rankings never crowd each other out. * **Retrieval pipeline.** [BM25](/docs/core/keyword-search) is the deterministic default. [Semantic and hybrid](/docs/core/semantic-hybrid-search) are opt-in per catalog or per call, still against a local index. * **Telemetry stream.** Every search, expansion, and invocation emits a trace event into a sink you configure — off by default, like [OpenTelemetry export](/docs/core/telemetry), which speaks the [`ratel.*` span vocabulary](/docs/core/packages/rust/ratel-ai-telemetry). ## What happens on a turn ```text Ratel Core SDK user request → local retrieval → top-K tools + capability tools → model ↕ ToolCatalog ``` [Ratel Local](/docs/local) exposes the same catalog semantics and wire contract over MCP, either as its CLI-managed gateway or [embedded in a Node process](/docs/local/library). ### Register once A tool enters the catalog with an id, a retrievable name and description, JSON input and output schemas, and an executor. MCP imports produce the same shape automatically. Skill metadata lives in the separate `SkillCatalog`. ### Retrieve locally For the incoming request, Ratel ranks the catalog in-process. BM25 is the deterministic default. Semantic and hybrid retrieval are opt-in and still use a local index rather than a hosted vector database. ### Give the model a small starting set An SDK integration can inject the top-K matches as normal framework tools. The capability tools stay available alongside them. A Ratel Local integration begins with the capability tools because the upstream catalog remains behind the MCP boundary. ### Expand only when needed If the starting set is insufficient, the model calls `search_capabilities` and gets separately ranked tool and skill hits, each with the id and schema needed for the next call. A matched skill exposes its body only through `get_skill_content`. See [Progressive disclosure](/docs/core/progressive-disclosure) for the exact contracts. ### Invoke by id The model calls `invoke_tool` with the selected `toolId` and nested `args`. Ratel dispatches to the local handler or upstream MCP server and returns the result to the normal agent loop. Tool permissions, authentication, and side effects still belong to the tool and its host; retrieval does not bypass them. ## What stays out of the prompt * Definitions for tools that did not match the current request. * Full skill bodies until the agent chooses to load one. * Upstream MCP inventories when Ratel Local is the only server exposed to the host. * Retrieval infrastructure: the default index runs locally with no Ratel service call. What remains is ordinary agent behavior: your model provider still handles the model request, and invoked tools keep the permissions you configured. ## Keep building with Ratel Core [Build the SDK path](/docs/core/quickstart) Let a coding agent integrate Ratel, or follow the TypeScript and Python quickstarts. [What this saves](/docs/core/efficiency) The token and accuracy cost of oversized tool lists — and what this architecture buys back. > **Using Claude Code, Codex, or Cursor?** > > Claude Code and Codex can [import or link](/docs/local/import-or-link) through Ratel Local. > Cursor uses [manual MCP configuration](/docs/local/cursor). Both paths end at the same > capability tools. --- # Efficiency Source: https://docs.ratel.sh/docs/core/efficiency Ratel's value is measured in what the model reads: fewer tokens per turn, with tool selection as good or better. ## What changes in the model's context Ratel does not change your model or your prompts. It changes which tool and skill definitions reach the model on each turn: | Without Ratel | With Ratel | | ------------------------------------------------- | -------------------------------------------------------- | | Every tool schema is sent up front | Only the relevant top-K and capability tools are sent | | The model must choose from one crowded list | Local retrieval ranks candidates for the current request | | Each integration expands the model's tool surface | One catalog stays searchable behind capability tools | | Runbooks compete with tool definitions for tokens | Skill bodies load only after the model selects one | ## Why it compounds Without Ratel, prompt cost scales with catalog size: the benchmark sweeps pools from 30 to 600 tools, and the baseline pays for all of them every turn. With Ratel, the model reads the top-K hits plus three capability tools no matter how large the catalog grows. That is the mechanism behind [progressive disclosure](/docs/core/progressive-disclosure). ## Benchmark-backed numbers On the BFCL tools suite (Ratel 0.4.0, dense retrieval, `ratel-full` arm vs `control-baseline`), token savings grow with catalog size while accuracy holds at or near baseline — only the smallest local model gives up a few points: | Model | Mean total tokens | Accuracy (multiple / simple split) | | ----------------- | ----------------- | ---------------------------------- | | Claude Haiku 4.5 | −86% | +26.0 / +6.5 pt | | Claude Sonnet 4.6 | −87% | +0.5 / +0.5 pt | | GPT 5.4 Mini | −89% | +1.0 / −1.0 pt | | Qwen3 4b | −91% | −5.5 / −3.3 pt | Smaller models gain the most: Haiku 4.5 jumps 26 points on the multiple split once the crowded list is gone. [Benchmark](/docs/core/benchmark) covers the methodology; the live results at [benchmark.ratel.sh](https://benchmark.ratel.sh) let you switch model, version, and retrieval method. --- # Benchmark Source: https://docs.ratel.sh/docs/core/benchmark Ratel's numbers come from an open harness, [`ratel-ai/ratel-bench`](https://github.com/ratel-ai/ratel-bench), and the published results live at [benchmark.ratel.sh](https://benchmark.ratel.sh). ## Three arms Every scenario runs three ways on the same model: * **control-baseline** — the entire candidate tool pool is exposed to the agent on every turn. * **control-oracle** — only the gold tool is exposed; the accuracy upper bound. * **ratel-full** — the model sees a handful of targeted tools plus the capability tools, regardless of how large the underlying pool is. The gap between baseline and ratel-full is the token saving; the gap to oracle is the accuracy headroom. ## Two suites * **BFCL** — the [Berkeley Function-Calling Leaderboard](https://gorilla.cs.berkeley.edu/leaderboard.html), simple and multiple splits, with pool sizes swept across realistic MCP setups (30 → 600 tools). * **Skills suite** — agent tasks over six datasets (bigcodebench, champ, logicbench, medcalcbench, theoremqa, toolqa), measuring retrieval and completion on multi-step work. ## What gets measured Per model, arm, and split: task completion, tool selection accuracy, recall, mean total tokens, and latency. A separate retriever evaluation sweeps the pool sizes (precision, recall, MRR, nDCG). ## Where results live [benchmark.ratel.sh](https://benchmark.ratel.sh) renders the reports as interactive charts and tables, methodology included. The site regenerates at build time from ratel-bench's published reports, so it always tracks the source of truth. For the headline numbers and what they mean in your agent, see [Efficiency](/docs/core/efficiency). --- # Licensing Source: https://docs.ratel.sh/docs/core/licensing Ratel uses two permissive licenses: the `ratel-ai-core` Rust crate — the engine — is **Apache-2.0**, and everything else is **MIT**. The split is decided in [ADR 0009](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0009-licensing.md). | Component | License | Why | | ------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------- | | `ratel-ai-core` (the engine) | Apache-2.0 | Explicit patent grant on the code others embed | | SDKs (`@ratel-ai/sdk`, `ratel-ai`), native bindings, telemetry packages, examples | MIT | Matches ecosystem norms | | Ratel Local (`@ratel-ai/mcp-server`, [`ratel-ai/ratel-local`](https://github.com/ratel-ai/ratel-local)) | MIT | Same | | Skills ([`ratel-ai/skills`](https://github.com/ratel-ai/skills)) | MIT | Same | ## What this means for you * **Embed and redistribute freely.** Both licenses permit commercial use, modification, and redistribution. * **Patent protection.** The engine's Apache-2.0 license includes an explicit patent grant. * **Mixing is standard.** MIT code depending on Apache-2.0 code is a common, one-way-compatible combination — no extra obligations for consumers. * **Old versions keep their terms.** Versions published before the relicense stay under the license they shipped with. There is no AGPL, SSPL, BSL, or any other source-available or copyleft license anywhere in the repo — a decision recorded in ADR 0009. ## License texts * [LICENSE-APACHE](https://github.com/ratel-ai/ratel/blob/main/LICENSE-APACHE) — Apache-2.0, for the engine * [LICENSE.md](https://github.com/ratel-ai/ratel/blob/main/LICENSE.md) — MIT, for everything else * [NOTICE](https://github.com/ratel-ai/ratel/blob/main/NOTICE) — per-component breakdown --- # Tools, MCP & Skills Source: https://docs.ratel.sh/docs/core/tools-mcp-skills 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](/docs/core/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](/docs/core/typescript/register-tools), [Python](/docs/core/python/register-tools)) 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 [Quick start](/docs/core/quickstart). 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](/docs/core/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](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0005-first-class-skills.md) 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](/docs/core/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](/docs/core/keyword-search) by default, [semantic or hybrid](/docs/core/semantic-hybrid-search) 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. **Python** ```python 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" ``` **TypeScript** ```ts 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](/docs/local/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: ```jsonc { "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)](/docs/core/typescript/register-tools) Wire local functions and MCP servers into the `ToolCatalog`. [Register tools (Python)](/docs/core/python/register-tools) The same registration flow, idiomatic Python. [Progressive disclosure](/docs/core/progressive-disclosure) The full contract of `search_capabilities`, `invoke_tool`, and `get_skill_content`. [Keyword search](/docs/core/keyword-search) How BM25 ranks both catalogs and how to write retrievable definitions. --- # Progressive disclosure Source: https://docs.ratel.sh/docs/core/progressive-disclosure 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](/docs/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](/docs/core/keyword-search), [semantic and hybrid](/docs/core/semantic-hybrid-search) 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. ```jsonc { "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 `skills` is `[]`; no tool matches means `tools.groups` is `[]`. * Hits are grouped by server, in first-hit order. A hit's server is its `toolId` prefix before the first `"__"`; an id with no separator is its own group. * Each hit carries exactly `toolId`, `score`, `description` (the raw registered description), and `inputSchema`. Skill hit descriptions are compacted: whitespace collapsed, clipped to 160 characters on a word boundary. * A matched skill's declared `tools` are pulled into the tools bucket at `score: 0` — additive beyond `topKTools`, deduped against query hits — so the agent gets a playbook and its toolkit in one turn. See [Tools, MCP & Skills](/docs/core/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: ```jsonc // 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: . Use the catalog's search tool to discover available ids.` | | Non-object `args` | ``invalid args for : `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 threw: ` | 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 | ```jsonc // 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: . 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: **Python** ```python 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" ``` **TypeScript** ```ts 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](/docs/core/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](/docs/core/keyword-search) How the default BM25 ranking scores a query — and how to write definitions it can find. [Tools, MCP & Skills](/docs/core/tools-mcp-skills) What the catalog holds: executable tools, upstream MCP servers, and skill playbooks. [Register tools (TypeScript)](/docs/core/typescript/register-tools) Fill the catalog the capability tools search: local functions and upstream MCP servers. [Register tools (Python)](/docs/core/python/register-tools) The same registration flow, idiomatic Python. --- # Keyword search Source: https://docs.ratel.sh/docs/core/keyword-search Every registered tool and skill definition competes for the model's attention. Stack up a few dozen and each turn opens with the model reading definitions it will never call, paying in tokens and accuracy. Ratel keeps the catalog outside the prompt and ranks it per query. The default engine is **BM25** over a schema-aware text projection of each definition: it runs in your process, deterministically — no model, no vector DB, no service. ## Two ways to consume the ranking Both SDKs expose one retrieval engine through two patterns, and most agents run both at once: * **Pre-filter (top-K).** Before each model call, `catalog.search(query, top_k)` returns the few tools most relevant to the user's message. Only those enter the tool list. * **Dynamic capability search.** The agent carries `search_capabilities` and `invoke_tool` and reaches the rest of the catalog itself when the pre-filtered set is not enough. See [Progressive disclosure](/docs/core/progressive-disclosure). Whichever path a query arrives through, the ranking below is the same. ## BM25 over a schema-aware projection BM25 is the lexical algorithm behind most search engines. Ratel runs it in-process: the index is rebuilt from the registered definitions at each search call, and an empty catalog simply returns no hits. The scorer is tuned for short documents like tool definitions (`k1 = 0.9`, `b = 0.4`; fixed, not a knob). Ties break by tool id, so the same query over the same catalog returns the same hits in the same order on any machine. ### What gets indexed BM25 does not rank raw JSON. Each tool is flattened into a **schema-aware text projection** ([ADR 0004](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0004-retrieval-and-tool-selection.md)): 1. The tool **name**, whole and identifier-split: `search_files` also indexes "search files". Hyphens are not split, so prefer `snake_case` or `camelCase` over kebab-case ids. 2. The **description**, verbatim. 3. Every property of the input and output schemas: the property **name** (whole and split), its **description**, and each **string** value of its **enum**, recursing through nested objects and array items. Non-string enum values are skipped. For each [skill](/docs/core/tools-mcp-skills), the projection is the `name`, each **tag**, and the `description`. The body is excluded by design — a 15 KB runbook would drown the description's term weights. Structural JSON Schema tokens (`type`, `required`, braces) are never indexed. The emitted text is tokenized at index and query time (lowercased, stemmed, stop words removed), so matching is not verbatim. The consequence is the point: **your vocabulary is the recall mechanism.** The projection is a contract — every retrieval method ranks exactly this text, and changing the flattening is treated as a breaking change. ## Write definitions the index can find Every definition serves two readers. The **index** decides whether it surfaces; the **model** reads the description to decide *when* to call and the schema to decide *how*. * **Say what it does and when to use it.** One sentence for the what, then `Use when ...`. The "when" clause is what separates a tool from its sibling in a shared result set. * **Use the caller's vocabulary.** Stemming matches "refunded" to "refund"; nothing maps "monetary adjustments" to "money back". Add distinct trigger phrasings, not repetitions — term-frequency scoring saturates fast. * **Avoid near-duplicate descriptions.** Two tools described in the same words split the score and the model's confidence. Merge them, or rewrite each "when to use" until they no longer overlap. * **Name parameters and enum values like they will be searched.** `arg1` is invisible to the index; a `status` enum of `["pending", "shipped", "refunded"]` adds query terms *and* constrains the call. * **Keep schemas tight for the model.** Structure is not indexed, but it stops malformed calls: type every property, list `required`, set `additionalProperties: false`. * **Tag your skills.** The body is not indexed, so tags and the description are where an intent prompt has to land. Fold in category labels ("billing") and raw task phrases ("issue a refund"). Anti-patterns: * **Paraphrase chains.** "Searches files. Finds files. Locates files." Saturated terms, still no "when to use". * **Marketing tone.** No caller types "seamless" or "powerful". Concrete verbs and domain nouns only. * **One-line anemia.** "Refund tool." Nothing to match, nothing for the model to decide with. * **Structural noise.** Restating the schema in prose, or documenting internals (transport, timeouts, id formats). Spot-check with `catalog.search` using realistic queries — the user's phrasing, not the tool's name. The intended tool should top each phrasing you expect to serve. **Python** ```python from ratel_ai import ToolCatalog catalog = ToolCatalog() # ...register the exact tools your agent registers in production. for q in ["refund a customer order", "customer wants money back"]: hits = catalog.search(q, top_k=5) print(q, "->", [(h.tool_id, round(h.score, 2)) for h in hits]) ``` **TypeScript** ```ts import { ToolCatalog } from "@ratel-ai/sdk"; const catalog = new ToolCatalog(); // ...register the exact tools your agent registers in production. for (const q of ["refund a customer order", "customer wants money back"]) { const hits = catalog.search(q, 5); console.log(q, "->", hits.map((h) => `${h.toolId}: ${h.score.toFixed(2)}`)); } ``` `SkillCatalog.search` works the same way for skills (hits carry `skillId` in TypeScript, `skill_id` in Python). > The `ratel-tune-definitions` skill in the [skills suite](/docs/core/quickstart) > automates this audit: it inventories every definition, flags these failure modes, > and writes a prioritized plan of before/after rewrites. ## When keyword search misses BM25 misses the query that shares no words with the right definition: "summarize this contract" will not surface `extract_document_text` unless the overlap is written in. The first fix is better definitions. When paraphrase queries keep missing, opt into [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Progressive disclosure](/docs/core/progressive-disclosure) The agent-facing side of retrieval: search\_capabilities, invoke\_tool, and get\_skill\_content. [Semantic & hybrid search](/docs/core/semantic-hybrid-search) Opt-in embedding and hybrid ranking for the paraphrase queries BM25 cannot match. [Register tools (TypeScript)](/docs/core/typescript/register-tools) Build the catalog the ranking runs over, from local functions and upstream MCP servers. [Register tools (Python)](/docs/core/python/register-tools) The same registration flow, idiomatic Python. --- # Semantic & hybrid search Source: https://docs.ratel.sh/docs/core/semantic-hybrid-search BM25 misses the query that shares no words with the right definition. "Summarize this contract" will not surface `extract_document_text` unless someone wrote the overlap in. Two opt-in methods close that gap ([ADR 0011](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0011-selectable-retrieval-methods.md)). The default engine and the text they all rank is covered in [Keyword search](/docs/core/keyword-search). ## The two methods * **`"semantic"`** embeds the query with a local model and cosine-ranks it against pre-built per-tool vectors. * **`"hybrid"`** runs both the BM25 and semantic arms, each to a depth of at least 100, and fuses them with Reciprocal Rank Fusion (`k = 60`, no reranker). RRF fuses rank positions, not raw scores. BM25's unbounded scores and cosine's `[-1, 1]` never need reconciling — which also means scores are not comparable across methods. Treat each as the engine's raw score for that query. All three methods rank the same [indexed projection](/docs/core/keyword-search); embeddings are computed over that text, not raw JSON. ## Enable it per catalog or per call The method is a string — `"bm25"` (default), `"semantic"`, or `"hybrid"` — chosen at catalog construction or overridden on a single search. The per-call value wins. **Python** ```python from ratel_ai import ToolCatalog # Per catalog: every search defaults to hybrid. # register() embeds each new tool eagerly; a model-load # failure raises at register (fail-fast). catalog = ToolCatalog(method="hybrid") # Per call: override the catalog default for one search. hits = catalog.search("summarize this contract", 5, method="semantic") ``` **TypeScript** ```ts import { ToolCatalog } from "@ratel-ai/sdk"; // Per catalog: every search defaults to hybrid. // register() embeds each new tool eagerly; a model-load // failure throws at register (fail-fast). const catalog = new ToolCatalog({ method: "hybrid" }); // Per call: override the catalog default for one search. // method is the 4th argument, after origin. const hits = catalog.search("summarize this contract", 5, "direct", "semantic"); ``` `SkillCatalog` takes the same `method` option and per-call override; [skills](/docs/core/tools-mcp-skills) rank in their own corpus with the same engines. One consumer never overrides: `search_capabilities` always searches with the catalog's construction-time default, and [Ratel Local](/docs/local) inherits it the same way. To change the engine your agent uses, change the catalog. ## Build embeddings first Semantic and hybrid rank against a pre-built embedding cache. A search only ever embeds the query, never the corpus. A catalog constructed as semantic or hybrid keeps the cache current automatically: every `register` embeds the new tool. A BM25-default catalog that wants per-call semantic or hybrid searches must build the cache explicitly: **Python** ```python from ratel_ai import ToolCatalog catalog = ToolCatalog() # method defaults to "bm25" # ...register tools... catalog.build_embeddings() # embed anything not yet embedded hits = catalog.search("summarize this contract", 5, method="hybrid") ``` **TypeScript** ```ts import { ToolCatalog } from "@ratel-ai/sdk"; const catalog = new ToolCatalog(); // method defaults to "bm25" // ...register tools... catalog.buildEmbeddings(); // embed anything not yet embedded const hits = catalog.search("summarize this contract", 5, "direct", "hybrid"); ``` The cache is incremental and keyed by id. Adding or re-registering one tool costs one embedding, not N; a call with nothing left to embed is a no-op. The per-language cache walk-throughs — including MCP-ingested tools and the skill corpus — live on Register tools ([TypeScript](/docs/core/typescript/register-tools#build-the-embedding-cache), [Python](/docs/core/python/register-tools#build-the-embedding-cache)). Re-registering a tool invalidates its cached vector. On a BM25-default catalog, run `build_embeddings` again before the next semantic search (an eager catalog rebuilds at register). Both methods are synchronous and fail if the model cannot load: TypeScript throws an `Error`, Python raises `RuntimeError`. > A semantic or hybrid search on a catalog whose cache does not cover every tool fails fast, with the message `embeddings are not computed for semantic search (hint: construct the catalog with method="semantic"/"hybrid", or build its embeddings first)`. The guard loads no model, a search never silently embeds the corpus, and BM25 searches on the same catalog keep working. ## The local embedding model Opting in loads **`BAAI/bge-small-en-v1.5`** (384 dimensions) in the SDK's own process: * **Pure-Rust Candle inference, CPU-only.** No service, no API key, no GPU. * **Pinned HuggingFace revision**, so embeddings are reproducible across machines and over time. * **Downloaded on first use** into the shared HuggingFace cache (`~/.cache/huggingface`), then loaded offline. Nothing downloads at install time or inside a search. * **Roughly 130 MB resident** while loaded. * `HF_HOME` moves the cache; `HF_ENDPOINT` points the download at a mirror for proxied or air-gapped environments. * A cold load slower than 5 s emits an `embedder_load` [trace event](/docs/core/telemetry) with status `slow` — a hint the machine may be underpowered for local inference. `RATEL_EMBED_SLOW_MS` overrides the threshold. ## Choosing a method * **Stay on BM25** (the default) until you see missed recall. When it misses, the first fix is usually [better definitions](/docs/core/keyword-search), not a different engine. * **Pick semantic** when your queries genuinely paraphrase: query and tool share no vocabulary, and rewording the definitions cannot close the gap. * **Pick hybrid** to keep BM25's exact-identifier precision and add semantic's paraphrase recall. If you opt in at all, hybrid is usually the safer choice. * Semantic and hybrid pay the model's costs: a one-time download, resident memory, embedding work at register time, and possible load failures. BM25 pays none of that. ## Next steps [Keyword search](/docs/core/keyword-search) The BM25 default, the indexed projection, and how to write definitions that rank. [Progressive disclosure](/docs/core/progressive-disclosure) The agent-facing side of retrieval: search\_capabilities, invoke\_tool, and get\_skill\_content. [Telemetry](/docs/core/telemetry) Trace events for every search, including the embedder\_load hint. --- # Adaptive ranking Source: https://docs.ratel.sh/docs/core/adaptive-ranking **Coming soon.** Adaptive ranking will feed your agent's real usage — which retrieved capabilities actually get invoked — back into the ranking. Until then, ranking is deterministic: see [Keyword search](/docs/core/keyword-search) and [Semantic & hybrid search](/docs/core/semantic-hybrid-search). --- # Compaction Source: https://docs.ratel.sh/docs/core/compaction **Coming soon.** Compaction will keep long-running agent context lean — trimming what the model no longer needs while keeping it retrievable. Until then, the biggest context lever is retrieval itself: start with [Keyword search](/docs/core/keyword-search). --- # Telemetry Source: https://docs.ratel.sh/docs/core/telemetry Ratel emits telemetry on **two deliberately separate streams** ([ADR 0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md)): * **Local trace events**, owned by the Rust core. Structured events (`search`, `invoke_start`, `upstream_error`, ...) recorded into a sink you configure; this stream feeds the offline inspector, [Ratel Local statusline and savings reporting](/docs/local/statusline-telemetry), and future rerankers. Its reliability contract is query-log semantics — best-effort, sampleable, lossy on backpressure: losing an event is acceptable, corrupting a catalog is not. * **OpenTelemetry spans.** Remote telemetry *is* OpenTelemetry: `gen_ai.*` spans per a pinned semconv baseline plus a `ratel.*` overlay, exported as stock OTLP (`http/protobuf` + Bearer auth). This is the stream for Ratel Cloud, Langfuse, or your own collector. Both are off by default: the local sink is `noop` until you pass one, and spans are no-ops until an OpenTelemetry provider exists. Pick your path: * **Local visibility, no network** → [configure a trace sink](#local-trace-sinks). * **No OpenTelemetry in your stack yet** → [call `configure_telemetry` / `configureTelemetry`](#greenfield-ship-spans-to-ratel-cloud) once at startup and spans flow to Ratel Cloud. * **Already running OpenTelemetry** (Langfuse, the Vercel AI SDK, your own collector) → skip `configureTelemetry`; your provider already receives the spans. [Add the Ratel span processor](#already-on-opentelemetry-dual-export) to dual-export the Ratel cut. ## Local trace sinks Pass `trace` to the `ToolCatalog` (or `SkillCatalog`) constructor and the core records every search, invoke, [capability-tool](/docs/core/progressive-disclosure), upstream-MCP, and auth event into the sink. **Python** ```python from ratel_ai import ToolCatalog, TraceSinkConfig catalog = ToolCatalog( trace=TraceSinkConfig(kind="jsonl", session_id="session-1", path="/tmp/ratel.jsonl"), ) # Every catalog.invoke, capability-tool call, and register_mcp_server call now # appends one JSON line per event to /tmp/ratel.jsonl. ``` `TraceSinkConfig` is a dataclass: `kind` is `"noop" | "memory" | "jsonl"`; `session_id` is required for memory and jsonl, `path` for jsonl. Misconfiguration raises `ValueError` (for example `"jsonl sink requires path"`). **TypeScript** ```ts import { ToolCatalog } from "@ratel-ai/sdk"; const catalog = new ToolCatalog({ trace: { kind: "jsonl", sessionId: "session-1", path: "/tmp/ratel.jsonl" }, }); // Every catalog.invoke, capability-tool call, and registerMcpServer call now appends one JSON line per event to /tmp/ratel.jsonl. ``` `TraceSinkConfig` is a discriminated union: `{ kind: "noop" }`, `{ kind: "memory"; sessionId }`, or `{ kind: "jsonl"; sessionId; path }`. Misconfiguration throws (for example `"jsonl sink requires path"`). | Kind | Requires | Behavior | | -------- | ----------------------- | -------------------------------------------------------------- | | `noop` | — | Drop everything. The default when no `trace` is passed. | | `memory` | a session id | Keep events in memory; drain via the catalog. Useful in tests. | | `jsonl` | a session id and a path | Append one JSON line per event to the file. | Every recorded event lands wrapped in an envelope: `{ "v": 1, "ts": , "session_id": "...", "type": "search", ...event fields }`, with the event type tagged in snake\_case. * **Draining the memory sink.** `catalog.drain_trace_events()` (Python) / `catalog.drainTraceEvents()` (TypeScript) returns the buffered events and removes them. It returns `[]` unless the active sink is `memory`; the `jsonl` and `noop` sinks have no drain, read the file instead. * **JSONL file behavior.** The sink creates missing parent directories, opens the file append-only with mode `0600` on Unix, and flushes after every line. Writes are best-effort: a serialization or write failure is swallowed rather than crashing the agent loop. * **Origin tagging.** Searches the model makes through `search_capabilities` are tagged `origin: "agent"`; direct `catalog.search(query, k)` calls default to `"direct"`, overridable with a third argument. ### Event types The event set is defined once, in the Rust core, so every SDK emits the same shapes. The full `type` vocabulary: | `type` | Recorded on | Emitted by | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `search` | every `ToolCatalog.search`, including the one inside `search_capabilities` | SDK | | `index_churn` | a tool added to or removed from the index | SDK | | `skill_search` | every `SkillCatalog.search`, including the one inside `search_capabilities` | SDK | | `skill_churn` | a skill added to the index | SDK | | `skill_invoke` | every `SkillCatalog.invoke`, including via `get_skill_content` | SDK | | `invoke_start`, `invoke_end`, `invoke_error` | around every `ToolCatalog.invoke`: start, then end on success or error on failure (the error is rethrown after recording) | SDK | | `gateway_search` | every `search_capabilities` call, recorded at the capability-tool layer | SDK | | `gateway_invoke`, `gateway_error` | an `invoke_tool` call succeeding or failing (unknown id, unauthorized upstream, executor error); `gateway_error` also covers `get_skill_content` with an unknown skill id | SDK | | `upstream_register` | every `registerMcpServer` / `register_mcp_server` | SDK | | `upstream_invoke`, `upstream_error` | a proxied upstream-MCP tool call succeeding or failing | SDK | | `auth_refresh` | an upstream token refresh attempt | Ratel Local | | `auth_needs` | an upstream MCP server needing (re)authorization | Ratel Local | | `auth_flow_start`, `auth_flow_end` | the OAuth flow for an upstream starting and finishing | Ratel Local | | `embedder_load` | once, on the first (cold) load of the embedding model | SDK | A single agent call often records several of these: `search_capabilities` produces both a core-level `search` (or `skill_search`) and a `gateway_search`, and `invoke_tool` produces `invoke_start` / `invoke_end` (or `invoke_error`) plus `gateway_invoke` / `gateway_error`, with `upstream_invoke` / `upstream_error` on top for proxied MCP tools. The four `auth_*` events come only from [Ratel Local OAuth upstreams](/docs/local/oauth), which embeds the SDK and records into the same stream; the SDKs never emit them. ## The OpenTelemetry span vocabulary Independently of the local sink, the SDK emits OpenTelemetry spans around the same funnel: `gen_ai.*` attributes pinned to semconv v1.42.0, plus the Ratel-owned `ratel.*` overlay. The local event and the span are two independent channels around the same call. | Span | Emitted around | Attributes | | ------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ratel.search` | every `ToolCatalog.search` / `SkillCatalog.search`, including the one inside `search_capabilities` | `ratel.search.target` (`tool` \| `skill`), `ratel.origin`, `ratel.search.top_k`, `ratel.search.hit_count`; `ratel.search.query` only when [content capture](#content-capture) selects a span mode | | `execute_tool ` | every `ToolCatalog.invoke`, including via `invoke_tool` | `gen_ai.operation.name = "execute_tool"`, `gen_ai.tool.name`, `ratel.tool.args_size_bytes`; gated: `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` | | `ratel.skill.load` | `SkillCatalog.invoke`, including via `get_skill_content` | `ratel.skill.id` | | `ratel.upstream.register` | `registerMcpServer` / `register_mcp_server` (connect + list + ingest) | `ratel.upstream.server`, `ratel.upstream.transport`, `ratel.upstream.tool_count` | | `ratel.auth.flow` | `invoke_tool` hitting an unauthorized (401) upstream | `ratel.auth.outcome = "needs_auth"`, plus `ratel.upstream.server` when known | Tool invocations deliberately reuse `execute_tool`, the standard `gen_ai.operation.name` value, instead of a bespoke `ratel.invoke` span, so a generic OTel backend already understands them. Errors use the standard span status `ERROR` plus the exception event, not a bespoke attribute. The SDK does **not** emit the LLM-call spans (`chat `): those come from your LLM instrumentation, and Ratel's spans join the same trace next to them. ### No-op until a provider Instrumentation is free until you opt in, in both languages: * **TypeScript:** the SDK's only OpenTelemetry dependency is `@opentelemetry/api`, never the OTel SDK. Until a provider is registered, every span is a no-op `NonRecordingSpan`, and the local trace stream is untouched. * **Python:** the base `ratel-ai` install has zero dependencies. If `opentelemetry` or `ratel_ai_telemetry` is missing, every telemetry helper is a straight pass-through, zero overhead, no spans. With them installed, spans go to whatever provider is registered. ### Greenfield: ship spans to Ratel Cloud No OpenTelemetry in your stack yet? One call at startup registers a global tracer provider with a batch OTLP exporter pointed at Ratel Cloud. **Python** The exporter ships behind the `[otlp]` extra (without it, `configure_telemetry` raises `ModuleNotFoundError` with this install hint): ```bash pip install 'ratel-ai[otlp]' ``` ```python import os from ratel_ai import configure_telemetry handle = configure_telemetry( endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"), api_key=os.environ["RATEL_API_KEY"], ) # ... run your agent: every ratel.* / execute_tool span now exports ... handle.shutdown() # flush the exporter on exit (handle.force_flush() mid-run) ``` `configure_telemetry(*, api_key=None, endpoint=None, headers=None, service_name=None, capture_content=None, include_span_and_events=None)` returns a per-call shutdown handle (`handle.shutdown()` / `handle.force_flush()`). **TypeScript** The exporter is the optional peer `@ratel-ai/telemetry-otlp` (without it, `configureTelemetry` throws with this install hint): ```bash npm install @ratel-ai/telemetry-otlp ``` ```ts import { configureTelemetry } from "@ratel-ai/sdk"; const handle = await configureTelemetry({ endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces", apiKey: process.env.RATEL_API_KEY, }); // ... run your agent: every ratel.* / execute_tool span now exports ... await handle.shutdown(); // flushes and stops the exporter ``` `configureTelemetry({ serviceName?, apiKey?, endpoint?, headers?, captureContent?, includeSpanAndEvents? })` resolves to a `TelemetryHandle` with a `shutdown()` method. Endpoint and auth resolve identically in both languages: * The `endpoint` option wins; otherwise the `RATEL_URL` env var. Either way it is the **full OTLP traces URL including `/v1/traces`**, for example `https://cloud.ratel.sh/v1/traces`. With neither set, the call fails before anything registers. * The API key is sent as `Authorization: Bearer ` on top of any extra `headers`. When omitted it falls back to the `RATEL_API_KEY` env var (unless you already set an `Authorization` header explicitly). * `service_name` / `serviceName` defaults to `"ratel"`. Runnable examples live in the repo: [`examples/telemetry-ts`](https://github.com/ratel-ai/ratel/tree/main/examples/telemetry-ts) and [`examples/telemetry-python`](https://github.com/ratel-ai/ratel/tree/main/examples/telemetry-python) emit a sample `ratel.search` + `execute_tool` trace to a console exporter, no collector or API key needed, and switch to a real OTLP export when `RATEL_URL` is set. > `configureTelemetry` owns the global provider. If one is already registered it fails loudly, pointing at the span processor, rather than silently no-op'ing. That error means you are on the wrong path: dual-export instead. ### Already on OpenTelemetry: dual-export OpenTelemetry's coexistence model is one provider, many span processors. If Langfuse, the Vercel AI SDK, or your own collector already owns the provider, the SDK's spans are already flowing to it, so skip `configureTelemetry` and add the Ratel span processor to send a copy to Ratel Cloud: **Python** ```python import os from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from ratel_ai_telemetry.otlp import ratel_span_processor provider = trace.get_tracer_provider() # the provider your existing setup registered if not isinstance(provider, TracerProvider): raise RuntimeError("register your OpenTelemetry SDK provider before adding processors") provider.add_span_processor( ratel_span_processor( endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"), api_key=os.environ["RATEL_API_KEY"], ) ) ``` `ratel_span_processor(*, api_key=None, endpoint=None, headers=None, span_filter=None, enabled=True)`; `ratel_span_exporter(...)` is the bare OTLP exporter if you wire your own processor. **TypeScript** ```ts import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { LangfuseSpanProcessor } from "@langfuse/otel"; import { ratelSpanProcessor } from "@ratel-ai/telemetry-otlp"; const provider = new NodeTracerProvider({ spanProcessors: [ new LangfuseSpanProcessor(), // Langfuse keeps every span ratelSpanProcessor({ // Ratel takes gen_ai.*/ratel.* only endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces", apiKey: process.env.RATEL_API_KEY, }), ], }); provider.register(); ``` `ratelSpanProcessor(opts)` accepts the endpoint/auth options (`endpoint`, `apiKey`, `headers`) plus `spanFilter` and `enabled`; `ratelTraceExporter(opts)` is the bare OTLP exporter if you wire your own processor. The processor batches spans to the Ratel OTLP exporter and forwards only those passing its filter: * The default filter forwards a span if its name starts with `ratel.` or any attribute key starts with `gen_ai.` or `ratel.`, so the framework's `ai.*` wrapper noise stays out. * Pass `spanFilter: () => true` (TypeScript) / `span_filter=lambda _s: True` (Python) to forward everything. Do that, or tail-sample, when full-trace fidelity matters — per-span filtering can orphan the AI SDK's `ai.*` wrapper span from its `gen_ai.*` child. * The exporter carries no resource; your provider keeps ownership of `service.name`. ## Content capture Message and tool content never leaves the process by default. Set the mode in code — `configure_telemetry(capture_content=...)` / `configureTelemetry({ captureContent })` — or through the ecosystem instrumentation flag `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`, the fallback when nothing is set in code; parsing is trimmed and case-insensitive. | Value | Mode | | -------------------------------------- | -------------------------------------------------- | | unset / empty / `NO_CONTENT` | no content (the default) | | `SPAN_ONLY` | content on span attributes | | `EVENT_ONLY` | content on span events only, never span attributes | | `SPAN_AND_EVENT` | both | | `TRUE` / `1` (legacy boolean) | `SPAN_AND_EVENT` | | anything else (including `FALSE`, `0`) | no content | The gated payloads in the SDK are `ratel.search.query` on `ratel.search` and `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` on `execute_tool`. They appear only when the mode is a span mode (`SPAN_ONLY` or `SPAN_AND_EVENT`). **`EVENT_ONLY` does not put content on spans**, and since the 0.4.0 SDKs emit no content-bearing span events, it captures nothing from the SDK today: the `gen_ai.client.inference.operation.details` event is the convention for message content on your LLM instrumentation's spans, not something the Ratel SDK emits. ## Next steps [Telemetry (TypeScript)](/docs/core/typescript/telemetry) Set up this trace stream in a Node agent. [Telemetry (Python)](/docs/core/python/telemetry) The same setup, idiomatic Python. [Framework integrations](/docs/core/typescript/framework-integrations) Wire the capability tools into the agent framework you already run. TypeScript shown; the Python twin mirrors it. [Ratel Local statusline](/docs/local/statusline-telemetry) Operate the local JSONL stream and Claude Code statusline. --- # SDKs Source: https://docs.ratel.sh/docs/core/sdks Ratel ships two first-party SDKs that bundle the same Rust core ([`ratel-ai-core`](https://github.com/ratel-ai/ratel)), **[`@ratel-ai/sdk`](/docs/core/typescript/quickstart)** for TypeScript / Node and **[`ratel-ai`](/docs/core/python/quickstart)** for Python. The API is mirrored across both. Everything runs **in-process**: no external vector DB or embedding service, no API key, nothing to deploy. BM25 needs no embeddings; semantic and hybrid retrieval opt into a local embedding model, downloaded once on first use and cached. The wedge today is **tool selection**. Register your tools (or ingest an upstream MCP server) into a `ToolCatalog`; a BM25 index ranks them so the model sees the handful that matter for the current turn, reached through two self-service tools, `search_capabilities` and `invoke_tool`, instead of the full list. Use the toggle to switch languages; your choice follows you across every page. ## Install `npm install @ratel-ai/sdk` or `pip install ratel-ai` — versions, extras, native targets, and Ratel Local live on the [Install](/docs/core/install) page. ## Register a catalog, hand the agent two tools `ToolCatalog` pairs each tool's metadata with an executable handler. `searchCapabilitiesTool` / `invokeToolTool` (snake\_case in Python) are the capability tools: they let your agent search the catalog and invoke anything in it. Wire them into any framework. **Python** ```python from ratel_ai import ToolCatalog, ExecutableTool, search_capabilities_tool, invoke_tool_tool catalog = ToolCatalog() catalog.register( ExecutableTool( id="read_file", name="read_file", description="Read a file from local disk.", input_schema={"properties": {"path": {"type": "string"}}}, output_schema={"properties": {"contents": {"type": "string"}}}, execute=lambda args: {"contents": open(args["path"]).read()}, ) ) # The full catalog stays out of context; the agent reaches it via these two. search = search_capabilities_tool(catalog) # id == "search_capabilities" invoke = invoke_tool_tool(catalog) # id == "invoke_tool" ``` **TypeScript** ```typescript import { ToolCatalog, searchCapabilitiesTool, invokeToolTool } from "@ratel-ai/sdk"; import { readFile } from "node:fs/promises"; const catalog = new ToolCatalog(); catalog.register({ id: "read_file", name: "read_file", description: "Read a file from local disk.", inputSchema: { properties: { path: { type: "string" } } }, outputSchema: { properties: { contents: { type: "string" } } }, execute: async ({ path }) => ({ contents: await readFile(path, "utf8") }), }); // The full catalog stays out of context; the agent reaches it via these two. const search = searchCapabilitiesTool(catalog); // id === "search_capabilities" const invoke = invokeToolTool(catalog); // id === "invoke_tool" ``` Need only ranking, no execution? Use `ToolRegistry` (metadata-only BM25 index) and dispatch calls yourself. Both layers are covered in the language references below. ## Full reference The per-language pages document the full shipped API of [`ratel-ai/ratel`](https://github.com/ratel-ai/ratel): every class, capability tool, and telemetry hook, with mirrored snippets in both languages. [TypeScript SDK](/docs/core/typescript/quickstart) `@ratel-ai/sdk`: ToolRegistry, ToolCatalog, capability tools, MCP ingestion, telemetry. [Python SDK](/docs/core/python/quickstart) `ratel-ai`: the same API, idiomatic Python. ## End-to-end examples Runnable agents that wire a catalog into a real framework, with a BM25 pre-filter plus the `search_capabilities` / `invoke_tool` capability tools: * [`examples/ai-sdk`](https://github.com/ratel-ai/ratel/tree/main/examples/ai-sdk), Vercel AI SDK (`ToolLoopAgent`) * [`examples/pydantic-ai`](https://github.com/ratel-ai/ratel/tree/main/examples/pydantic-ai), Pydantic AI * [`examples/mcp-chat`](https://github.com/ratel-ai/ratel/tree/main/examples/mcp-chat), REPL ingesting an upstream MCP server via `registerMcpServer` --- # Related projects Source: https://docs.ratel.sh/docs/core/related-projects Ratel Core is one piece of a small open ecosystem. Everything lives under [github.com/ratel-ai](https://github.com/ratel-ai). ## ratel The core repo: [`ratel-ai/ratel`](https://github.com/ratel-ai/ratel). One Rust core, two first-party SDKs — [`@ratel-ai/sdk`](/docs/core/typescript/quickstart) for TypeScript and [`ratel-ai`](/docs/core/python/quickstart) for Python. Start at the [SDK overview](/docs/core/sdks). ## Ratel Local One MCP server that fronts your whole tool estate. Claude Code, Codex, and Cursor get `search_capabilities`, `invoke_tool`, and `get_skill_content` without a line of SDK code. Ships as [`@ratel-ai/mcp-server`](https://github.com/ratel-ai/ratel-local) on npm; the CLI is `ratel-mcp`. Start with the [Claude Code and Codex quickstart](/docs/local/quickstart) or [manual Cursor setup](/docs/local/cursor), then use [Troubleshooting](/docs/local/troubleshooting) when something breaks. ## ratel-bench The benchmark harness behind Ratel's numbers: [`ratel-ai/ratel-bench`](https://github.com/ratel-ai/ratel-bench). Published results live at [benchmark.ratel.sh](https://benchmark.ratel.sh). ## Skills suite Five installable audit and engagement skills for coding agents: assess an agent codebase, plan its observability, integrate Ratel, and fix what the audit finds. Repo: [`ratel-ai/skills`](https://github.com/ratel-ai/skills). Install and usage: [Quick start](/docs/core/quickstart). ## Ratel Cloud Hosted context analytics: inspect context usage and retrieval performance across your agents. Coming soon — see [Ratel Cloud](/docs/cloud). --- # Install Source: https://docs.ratel.sh/docs/core/typescript/install One package, no service: ```bash npm install @ratel-ai/sdk@0.4.0 ``` The docs pin the version they document; drop the pin to track the latest release. The package is ESM (`type: module`) and needs **Node.js 20 or newer**. ## Prebuilt native targets The SDK bundles Ratel's Rust core as a native module. Prebuilt bindings ship for: * macOS: `darwin-arm64`, `darwin-x64` * Linux (glibc): `linux-x64-gnu`, `linux-arm64-gnu` * Windows: `win32-x64-msvc` On these targets nothing compiles at install time and no Rust toolchain is needed. ## What else you might install * **MCP ingestion** — [`registerMcpServer`](/docs/core/typescript/register-mcps) works out of the box; the MCP client (`@modelcontextprotocol/sdk`) ships as a dependency. * **Telemetry export** — [`@ratel-ai/telemetry-otlp`](/docs/core/packages/typescript/telemetry-otlp) is an optional peer; add it only to ship spans over OTLP. See [Telemetry & OTel](/docs/core/typescript/telemetry). * **Semantic & hybrid retrieval** — nothing to install. Opting in downloads the local embedding model on first use; see [Build the embedding cache](/docs/core/typescript/register-tools#build-the-embedding-cache) and [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Quickstart](/docs/core/typescript/quickstart) Register a tool and let an agent discover it through the capability tools. [Packages](/docs/core/typescript/packages) The four npm packages behind the TypeScript surface. --- # Quickstart Source: https://docs.ratel.sh/docs/core/typescript/quickstart This is the TypeScript path. Python reader? Start at the [Python quickstart](/docs/core/python/quickstart). Rather not write the integration yourself? Point your coding agent at the [skills suite](/docs/core/quickstart). Ratel is framework-agnostic: everything the SDK hands you is a plain executable tool — metadata plus an `execute` handler — that plugs into any agent loop. This page shows the two ways in: [starting from scratch](#start-from-scratch) and [migrating an existing agent](#migrate-an-existing-agent). > **Before you start** > > Use Node.js 20 or newer. Nothing on this page calls a model, so no API key is needed. ## How it works Everything starts with a **`ToolCatalog`**: register each of your tools once, pairing its metadata (id, description, JSON schemas) with the handler that runs it. A **`SkillCatalog`** holds Markdown playbooks the same way, ranked in their own corpus. The agent reaches both through the three capability tools — `search_capabilities` (find tools and skills by description), `invoke_tool` (run a tool by id), and `get_skill_content` (load a skill's body) — so the full catalog never enters the prompt. How the pieces fit per turn is covered in [Architecture](/docs/core/architecture). ## Start from scratch ### Create a project and install the SDK ```bash mkdir ratel-quickstart && cd ratel-quickstart npm init -y npm pkg set type=module npm install @ratel-ai/sdk@0.4.0 npm install --save-dev tsx ``` Requirements, prebuilt targets, and optional packages are on [Install](/docs/core/typescript/install). ### Register a tool and a skill Create `index.ts`. Tools carry their metadata and handler; skills carry a description and a Markdown body: ```typescript import { SkillCatalog, ToolCatalog, getSkillContentTool, invokeToolTool, searchCapabilitiesTool, } from "@ratel-ai/sdk"; const catalog = new ToolCatalog(); catalog.register({ id: "get_weather", name: "get_weather", description: "Get the current weather for a city.", inputSchema: { type: "object", properties: { city: { type: "string", description: "City name" }, }, required: ["city"], }, outputSchema: { type: "object", properties: { city: { type: "string" }, temperatureC: { type: "number" }, condition: { type: "string" }, }, required: ["city", "temperatureC", "condition"], }, execute: async ({ city }) => ({ city, temperatureC: 24, condition: "sunny", }), }); const skills = new SkillCatalog(); skills.register({ id: "weather-briefing", name: "weather-briefing", description: "How to report the weather: temperature first, then condition.", body: "## Weather briefing\nAnswer in one sentence: temperature first, then the condition. No filler.", }); // The three capability tools are the only tools your agent needs to see. // Construct them after registering, so the skills bucket is advertised. const search = searchCapabilitiesTool(catalog, skills); // id: "search_capabilities" const invoke = invokeToolTool(catalog); // id: "invoke_tool" const load = getSkillContentTool(skills); // id: "get_skill_content" ``` ### Run the discovery loop by hand Append the exact calls a model makes at runtime, then run the file: ```typescript console.log(await search.execute({ query: "weather in Rome" })); console.log(await invoke.execute({ toolId: "get_weather", args: { city: "Rome" } })); console.log(await load.execute({ skillId: "weather-briefing" })); ``` ```bash npx tsx index.ts ``` Three results, no model key involved: * `search_capabilities` returns two independently-ranked buckets: `get_weather` under `tools`, `weather-briefing` under `skills`. Neither schema nor body entered any prompt to get there. * `invoke_tool` runs the handler and returns its result untouched: `{ city: "Rome", temperatureC: 24, condition: "sunny" }`. * `get_skill_content` returns the playbook: `{ body: "## Weather briefing\n…" }`. The full result shapes and error contract are on [Progressive disclosure](/docs/core/progressive-disclosure). ### Hand the loop to your agent Each capability tool is a plain `ExecutableTool` — `{ id, name, description, inputSchema, outputSchema, execute }`. Map those fields onto your framework's tool type (a few lines anywhere), add the three tools to the agent's tool list, and tell it to search before answering: ```text Find tools and playbooks with search_capabilities. Run tools with invoke_tool. When a skill matches, load it with get_skill_content and follow it. ``` Ready-made wiring — the Vercel AI SDK adapter and per-turn assembly — is on [Framework integrations](/docs/core/typescript/framework-integrations). Production agents usually add a top-K pre-filter next to the capability tools; that pattern is on [Use the discovery tools](/docs/core/typescript/discovery-tools). ## Migrate an existing agent A migration changes where capabilities live, not your model or business logic. Start with one workflow, prove it still behaves the same, then repeat. | Existing asset | After migration | | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Tool IDs, schemas, and handlers | Register once in `ToolCatalog`; keep authorization and side effects in the handler. | | Conditional runbooks and examples | Register in `SkillCatalog`; load only when relevant. | | Identity, safety, universal policy, output rules, and runtime context | Keep in the core prompt. | | Model and agent loop | Keep both; reuse the loop's prompt and tool seams. | ### Find the integration points Find where your process creates long-lived dependencies and where each request builds the model's tool list. Create the catalogs at startup; change the model-facing list at the request boundary. ### Register an existing tool Tool definitions leave the repeated model request, not your code. Preserve the ID, schema contract, handler, authorization, permissions, and side effects. Convert framework-native schemas to JSON Schema at this boundary. ```typescript // Before: the schema is sent to the model on every request const toolsForEveryRequest = { issue_refund: existingRefundTool }; // After: the same contract is registered once at startup const catalog = new ToolCatalog(); catalog.register({ id: "issue_refund", name: "issue_refund", description: "Issue an approved refund for an order.", inputSchema: issueRefundInputSchema, outputSchema: issueRefundOutputSchema, execute: issueRefund, // the existing handler }); ``` Register each tool in this workflow the same way. During a gradual migration, leave unmigrated tools in the old list and remove each migrated definition from it. ### Extract an on-demand skill Keep instructions and runtime context that must govern every turn in the core prompt. Move only conditional guidance, such as the refund workflow, into a self-contained skill: ```typescript // Before: the refund workflow is sent on every turn const instructions = [ CORE_PROMPT, SAFETY_POLICY, OUTPUT_RULES, RUNTIME_CONTEXT, REFUND_RUNBOOK, ].join("\n\n"); // After: everything except the runbook stays in the core prompt const coreInstructions = [ CORE_PROMPT, SAFETY_POLICY, OUTPUT_RULES, RUNTIME_CONTEXT, ].join("\n\n"); const skills = new SkillCatalog(); skills.register({ id: "refund-runbook", name: "refund-runbook", description: "Check refund eligibility, obtain approval, then issue the refund.", tools: ["issue_refund"], body: REFUND_RUNBOOK, }); ``` The description says when to load the skill. Its `tools` list surfaces `issue_refund` beside the matched playbook. ### Rewire and verify the agent Add the discovery policy to the core prompt. After both catalogs are populated, build the three Ratel capability tools and adapt them at the request boundary: ```typescript const discoveryInstructions = ` Find tools and playbooks with search_capabilities. Run tools with invoke_tool. When a skill matches, load it with get_skill_content and follow it. `.trim(); const instructions = [coreInstructions, discoveryInstructions].join("\n\n"); const capabilityTools = [ searchCapabilitiesTool(catalog, skills), invokeToolTool(catalog), getSkillContentTool(skills), ]; ``` The capability tools are plain `ExecutableTool` values. [Framework integrations](/docs/core/typescript/framework-integrations) shows the adapter and the production top-K turn assembler. A refund request should now follow this path: 1. `search_capabilities` returns `refund-runbook` and `issue_refund`. 2. `get_skill_content` loads the eligibility and approval steps. 3. `invoke_tool` calls the unchanged `issueRefund` handler. 4. The agent answers from the tool result. Before migrating the next workflow, verify the skill ranks, its declared tool appears, the handler receives the same arguments, and the initial request excludes the full catalog and refund runbook. > **Let a coding agent do the migration** > > The [skills suite](/docs/core/quickstart) ships skills for Claude Code, Cursor, Codex, and > similar: `ratel-integrate` plans this migration for your codebase, and > `ratel-decompose-prompt` splits the system prompt into skills. ## Next steps [Framework integrations](/docs/core/typescript/framework-integrations) Ready-made wiring for the Vercel AI SDK — and the recipe for any other loop. [Skills suite](/docs/core/quickstart) Coding-agent skills that assess your codebase and apply this integration for you. [Register tools](/docs/core/typescript/register-tools) The anatomy of an executable tool, the full ToolCatalog surface, and ranking without execution. [Register skills](/docs/core/typescript/register-skills) The full SkillCatalog surface: tags, declared tools, and how skills rank. --- # Register tools Source: https://docs.ratel.sh/docs/core/typescript/register-tools Everything the model can reach lives in a **`ToolCatalog`**: local functions, upstream MCP servers' tools, or both, ranked as one surface. This page covers local tools; MCP ingestion has [its own page](/docs/core/typescript/register-mcps). ## The anatomy of an executable tool An executable tool pairs the metadata Ratel indexes with the handler that runs it. Registering a local tool is one `register` call: ```ts import { ToolCatalog } from "@ratel-ai/sdk"; import { readFile } from "node:fs/promises"; const catalog = new ToolCatalog(); catalog.register({ id: "read_file", name: "read_file", description: "Read a file from local disk and return its textual contents.", inputSchema: { type: "object", properties: { path: { type: "string", description: "absolute path to the file" } }, required: ["path"], }, outputSchema: { type: "object", properties: { contents: { type: "string" } } }, execute: async ({ path }) => ({ contents: await readFile(path, "utf8") }), }); ``` What each field is for: * **`id`** — the stable handle everything else uses: `catalog.invoke(id, args)`, the `toolId` the model passes to `invoke_tool`, and the `toolId` on every search hit. Unique per catalog; [MCP-ingested tools](/docs/core/typescript/register-mcps) get a server-namespaced id automatically. * **`name`** — the tool's name where it appears as a direct tool definition, e.g. when you pre-filter top-K hits into the model's tool list. * **`description`** — what the model reads to pick the tool, and the main text BM25 ranks against. This is the field that decides whether the tool is ever found; see the definition-writing guidance in [Keyword search](/docs/core/keyword-search). * **`inputSchema`** / **`outputSchema`** — JSON Schema for the arguments and the result. The input schema rides every `search_capabilities` hit, so the model can call the tool without a second lookup. * **`execute`** — the handler, sync or async. `catalog.invoke` awaits it either way and rethrows whatever it throws (after recording an `invoke_error` trace event), so failures surface where you call it. The `ExecutableTool` type requires it. ## The `ToolCatalog` surface The catalog is the registry plus an executor per tool. The methods you will use: ```ts const catalog = new ToolCatalog(); // ToolCatalogOptions: { trace?, method? } catalog.register(tool); // ExecutableTool: metadata + execute() catalog.search(query, topK); // → SearchHit[] ({ toolId, score }) catalog.buildEmbeddings(); // pre-compute the semantic/hybrid embedding cache catalog.has(toolId); // → boolean catalog.get(toolId); // → Tool | undefined (metadata only) catalog.getExecutable(toolId); // → ExecutableTool | undefined (metadata + execute) await catalog.invoke(toolId, args); // run the handler, return its result catalog.recordEvent(event); // inject a trace event into the active sink catalog.drainTraceEvents(); // → unknown[]; memory sink only ``` The constructor takes `ToolCatalogOptions = { trace?: TraceSinkConfig; method?: SearchMethod }`: `trace` wires a local [trace sink](/docs/core/typescript/telemetry) (default: none), and `method` sets the catalog's default retrieval method (default `"bm25"`; see [Use the discovery tools](/docs/core/typescript/discovery-tools)). Two behaviors worth knowing: * **`invoke`** throws `unknown toolId: ` for an id that was never registered. It awaits sync and async executors alike (`Executor = (input: any) => Promise | unknown`) and rethrows whatever the handler throws (after recording an `invoke_error` trace event), so failures surface where you call it. * **`search`**'s full signature is `search(query, topK, origin = "direct", method?)`. Pass `"agent"` to tag a search as model-initiated in telemetry, and a `method` to override the catalog default for one call. Unlike the capability tool, this direct path applies no top-K guard, so pass a positive integer. ## Build the embedding cache BM25 needs no preparation. Semantic and hybrid retrieval rank against a **pre-built embedding cache** — a search embeds only the query, never the corpus — so the cache must cover every tool before the first such search. Two ways to keep it current: ```ts // Eager: a semantic- or hybrid-default catalog embeds every tool as it is // registered. Nothing else to call; a model-load failure throws at register. const eager = new ToolCatalog({ method: "hybrid" }); // Explicit: a BM25-default catalog embeds nothing until you ask. Build the // cache after registering, then opt in per call. const lexical = new ToolCatalog(); // ...register tools... lexical.buildEmbeddings(); // synchronous; throws if the model fails to load lexical.search("rotate the api key", 5, "direct", "semantic"); ``` `buildEmbeddings()` is incremental and keyed by id: it embeds only what is missing, so adding or re-registering one tool costs one embedding, not N, and a call with nothing left to embed is a no-op. Re-registering a tool invalidates its cached vector — on an explicit catalog, build again before the next semantic or hybrid search. A search over an incomplete cache throws instead of silently embedding the corpus. The first embedding loads the bundled local model (`BAAI/bge-small-en-v1.5`) in-process, downloading it into the HuggingFace cache on first use — no service, no API key. The model's footprint, offline use, and when the engines are worth it are on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## `ToolRegistry`: ranking without execution Need only the ranking, and you will dispatch tool calls yourself? `ToolRegistry` is the metadata-only BM25 index underneath `ToolCatalog`, with no executors and no capability tools. Register the same definitions minus `execute`, search, and route the winning ids through your own dispatcher. ```ts import { ToolRegistry, type Tool } from "@ratel-ai/sdk"; const registry = new ToolRegistry(); registry.register({ id: "read_file", name: "read_file", description: "Read a file from local disk and return its textual contents.", inputSchema: { properties: { path: { type: "string" } } }, outputSchema: { properties: { contents: { type: "string" } } }, }); registry.search("read a text file", 5); // → [{ toolId: "read_file", score: 1.42 }, ...] ``` The registry exposes the full ranking and tracing surface, minus executors: ```ts registry.search(query, topK); // BM25, origin "direct" registry.searchWithOrigin(query, topK, "agent"); // unknown origin strings fall back to "direct" registry.searchWithMethod(query, topK, "direct", "hybrid"); // throws if embeddings are not built registry.buildEmbeddings(); // incremental; throws if the model fails to load registry.setTraceSink({ kind: "memory", sessionId: "s1" }); registry.recordEvent(event); registry.drainTraceEvents(); // → unknown[]; memory sink only ``` There is no construction-time method default at this level: pick a method per call with `searchWithMethod`, or use the catalog wrappers. `SkillRegistry` is also exported and mirrors the same surface over `Skill` / `SkillHit`, the metadata-only index underneath `SkillCatalog`. ## Spot-check what you registered Verify a registration the way the model will find it: run a direct search and check that the expected ids surface. Local and upstream tools rank together. ```ts const hits = catalog.search("read a text file from disk", 5); console.log(hits.map((h) => `${h.toolId} ${h.score}`)); // → ["read_file 2.1", "fs__read_file 1.8"] ``` This is the same ranking `search_capabilities` runs on the model's behalf — a direct `catalog.search` is tagged `origin: "direct"` in telemetry, the capability tool's searches `"agent"`. If a tool you expect near the top doesn't surface, the fix is almost always its wording, not the ranking. ## Next steps [Register MCP servers](/docs/core/typescript/register-mcps) Ingest an upstream server's tools into the same catalog with registerMcpServer. [Use the discovery tools](/docs/core/typescript/discovery-tools) Hand the model search\_capabilities and invoke\_tool, plus a top-K pre-filter. [Keyword search](/docs/core/keyword-search) How BM25 ranks the catalog, and how to write descriptions and schemas it finds. --- # Register MCP servers Source: https://docs.ratel.sh/docs/core/typescript/register-mcps Hand the catalog a connected MCP transport and Ratel calls `tools/list`, registers each upstream tool under a server-namespaced id (`__`), and wires its executor to `tools/call` over the same connection. The upstream tools rank and run alongside your local ones. ## Ingest an MCP server's tools The SDK owns the MCP client: hand it a transport and it connects, reads the server's instructions, and lists the tools itself. For a remote server, that transport is Streamable HTTP: ```ts import { ToolCatalog, registerMcpServer } from "@ratel-ai/sdk"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const catalog = new ToolCatalog(); const handle = await registerMcpServer(catalog, { name: "issues", transport: new StreamableHTTPClientTransport( new URL("https://example.com/mcp"), ), }); // handle.toolIds → ["issues__create_issue", "issues__search_issues", ...] // handle.serverInstructions → the upstream's instructions, if any // catalog.search / catalog.invoke now rank and run the upstream tools alongside local ones. await handle.close(); // disconnect on shutdown ``` A local server spawned as a subprocess uses the stdio transport instead — everything else stays the same: ```ts import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const handle = await registerMcpServer(catalog, { name: "fs", transport: new StdioClientTransport({ command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"], }), }); ``` Any [transport from `@modelcontextprotocol/sdk`](https://modelcontextprotocol.io) works — Streamable HTTP, stdio, or SSE. A failure during connect or `tools/list` rejects the returned promise. The options object is `RegisterMcpServerOptions = { name, transport }`; the promise resolves to an `McpServerHandle = { toolIds, serverInstructions, close }`. Registration wraps connect, `tools/list`, and ingest in one `ratel.upstream.register` span and records an `upstream_register` trace event. ## Namespaced ids and the handle Each upstream tool registers under **`__`** — `fs__read_file`, `github__create_issue`. The prefix keeps ids collision-free across servers, and it is how `search_capabilities` groups results by server (everything before the first `__`). An upstream tool with no description registers with `""`, leaving only its name and schema text to rank on — worth fixing upstream or documenting around. The returned handle carries three things: * `toolIds` — the namespaced ids that were registered. * `serverInstructions` — the upstream's instructions, read from the live handshake. * `close()` — closes the SDK-owned MCP client; call it when your process winds down. ## Error propagation An upstream `tools/call` failure records an `upstream_error` trace event and rethrows, so it propagates as a rejected promise from `catalog.invoke` — the same handling path as a local executor that throws. When the model reaches the tool through `invoke_tool`, that failure comes back as a structured `{ "error": "tool threw: ...", "isError": true }` instead, so the loop stays recoverable. ## Many servers, one ranked surface Call `registerMcpServer` once per server and keep registering local tools into the same catalog: the model sees a single ranked surface through the same [capability tools](/docs/core/progressive-disclosure). To make the aggregation visible, pass the upstream list to the `search_capabilities` factory: ```ts const search = searchCapabilitiesTool(catalog, skills, { upstreamServers: [{ name: "gmail", description: "Send and search email", toolCount: 12 }], }); // UpstreamServerInfo = { name, description?, instructions?, toolCount?, needsAuth? } ``` The second argument is an optional [`SkillCatalog`](/docs/core/typescript/register-skills). Each entry renders one line in the tool's description, via the exported `formatUpstreamLine` helper, as `- ( tools) (auth required)` with every part after the name conditional. The matching entry also fills a result group's `server.description` and `server.instructions`. ## Embeddings for upstream tools Upstream tools enter the same corpus as local ones, so the [embedding cache](/docs/core/typescript/register-tools#build-the-embedding-cache) treats them identically. On a semantic- or hybrid-default catalog, `registerMcpServer` embeds every ingested tool as part of registration — budget a model load plus one embedding per tool on the first ingest. On a BM25-default catalog, call `catalog.buildEmbeddings()` once after the handle resolves, before the first semantic or hybrid search. A re-sync that re-registers an upstream tool invalidates its cached vector: eager catalogs re-embed at register, explicit ones need another `buildEmbeddings()`. What the engines cost is on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Register skills](/docs/core/typescript/register-skills) Add Markdown playbooks that rank in their own corpus, next to the tools. [Use the discovery tools](/docs/core/typescript/discovery-tools) How search\_capabilities groups results per server and invoke\_tool handles upstream auth. [Ratel Local](/docs/local/mcp-servers) Aggregate upstream servers with Ratel Local instead of in your own code. --- # Register skills Source: https://docs.ratel.sh/docs/core/typescript/register-skills Skills are Markdown playbooks (a deploy runbook, a debugging checklist) ranked in a *separate* corpus from tools, so a relevant skill is never crowded out by matching tools. The concept is covered in [Tools, MCP servers & skills](/docs/core/tools-mcp-skills); Ratel's own coding-agent skills — including `ratel-decompose-prompt`, which extracts skills from a long system prompt — are on the [Quick start](/docs/core/quickstart). ## Register a skill A skill can declare the `tools` its instructions call: when the skill matches a query, those tools are pulled into the `tools` bucket (additively, deduped) so the agent gets the playbook *and* the tools it needs in one turn instead of a second search. ```ts import { SkillCatalog, searchCapabilitiesTool, getSkillContentTool, type Skill, } from "@ratel-ai/sdk"; 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 for ranking tools: ["vercel__deploy", "fs__read_file"], // surfaced alongside the skill when it matches metadata: { stacks: ["next", "vercel"] }, // non-indexed context for higher-layer ranking body: "## Deploying to Vercel\n1. ...", // returned by getSkillContentTool }); const search = searchCapabilitiesTool(catalog, skills); // 2nd arg → result gains a populated `skills` bucket const load = getSkillContentTool(skills); // id: "get_skill_content" ``` Only `id`, `name`, and `description` are required; `tags`, `tools`, `metadata`, and `body` are optional (parity with the Python SDK). Ranking indexes a skill's name, description, and tags; the body never enters the index, so a long playbook cannot drown out its own description. ## How skills surface Pass the `SkillCatalog` as the second argument to `searchCapabilitiesTool` and search returns the `skills` bucket alongside `tools`, each with its own result budget. The agent pulls a skill's full body into context on demand via `getSkillContentTool` (id `get_skill_content`, constant `GET_SKILL_CONTENT_ID`). `get_skill_content({ skillId })` returns `{ body }`, or `{ error, isError: true }` for an unknown id. ## The `SkillCatalog` surface `SkillCatalog` takes the same constructor options as `ToolCatalog` (`SkillCatalogOptions = { trace?, method? }`) and mirrors its surface: ```ts skills.search(query, topK); // → SkillHit[] ({ skillId, score }); same (query, topK, origin?, method?) signature skills.buildEmbeddings(); // same semantics as ToolCatalog.buildEmbeddings skills.has(skillId); // → boolean skills.get(skillId); // → Skill | undefined skills.size(); // → number of registered skills skills.invoke(skillId); // → string: the Markdown body; throws on unknown id skills.recordEvent(event); // inject a trace event into the active sink skills.drainTraceEvents(); // → unknown[]; memory sink only ``` `invoke` returns the body directly (`body ?? ""`), recording a `skill_invoke` trace event, and throws `unknown skillId: ` for an unknown id; `getSkillContentTool` is the boundary that translates that throw into the structured `{ error, isError: true }` the agent sees. ## Embeddings for skills The skill corpus has its own embedding cache, maintained exactly like the tool one ([Build the embedding cache](/docs/core/typescript/register-tools#build-the-embedding-cache)): construct `SkillCatalog` with `method: "semantic"` or `"hybrid"` to embed eagerly at register, or keep the BM25 default and call `skills.buildEmbeddings()` before opting in per call. The two caches are independent — skills can rank hybrid while tools stay on BM25. Embeddings cover the same projection the ranking indexes — name, description, tags — never the body, so a long playbook costs one small embedding. When the engines are worth it is on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Use the discovery tools](/docs/core/typescript/discovery-tools) The skills bucket in search\_capabilities results, and loading a body via get\_skill\_content. [Skills suite](/docs/core/quickstart) Coding-agent skills that extract, tune, and maintain the skills your agents load. [Tools, MCP servers & skills](/docs/core/tools-mcp-skills) Where skills sit next to tools and MCP servers in the Ratel model. --- # Use the discovery tools Source: https://docs.ratel.sh/docs/core/typescript/discovery-tools `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](/docs/core/progressive-disclosure). ```ts 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](/docs/core/typescript/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: ```jsonc { "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`: `CapabilityToolHit`s grouped per server (`CapabilityToolGroup`), plus `CapabilitySkillHit`s. `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`](/docs/core/typescript/register-skills) as the second argument. An optional third argument advertises upstream MCP servers in the tool's description — see [Register MCP servers](/docs/core/typescript/register-mcps). > **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? }`, inferring the upstream server from the `__` 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: ```ts 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. ```ts // 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): ```ts // 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](/docs/core/typescript/register-tools#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](/docs/core/keyword-search), the semantic and hybrid engines (and the local embedding model's footprint) in [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Framework integrations](/docs/core/typescript/framework-integrations) Wire the capability tools and the pre-filter into the Vercel AI SDK. [Progressive disclosure](/docs/core/progressive-disclosure) The full contract search\_capabilities, invoke\_tool, and get\_skill\_content expose to the model. [Semantic & hybrid search](/docs/core/semantic-hybrid-search) When to opt in to embedding-based retrieval, and what it costs. --- # Framework integrations Source: https://docs.ratel.sh/docs/core/typescript/framework-integrations Ratel ships no framework plugins because it needs none. Everything the SDK hands you — your registered tools and the capability tools (`search_capabilities` / `invoke_tool`; `get_skill_content` joins the set when a `SkillCatalog` is registered) — is a plain executable tool: metadata plus an `execute` handler. Wiring a framework is two small pieces: 1. **An adapter** from that shape to the framework's tool type. 2. **A turn assembler** that builds each turn's tool list: the always-present capability tools plus the top-K catalog hits for the user's message ([ADR 0004](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0004-retrieval-and-tool-selection.md) replace-mode pre-filtering). This page follows the shipped [`examples/ai-sdk`](https://github.com/ratel-ai/ratel/blob/main/examples/ai-sdk/README.md) example. Using Pydantic AI? See the [Python framework integrations](/docs/core/python/framework-integrations). ## Vercel AI SDK The example pins `ai` `^6.0.0` and `@ai-sdk/openai` `^3.0.0` alongside `@ratel-ai/sdk`; the loop is AI SDK v6's `ToolLoopAgent`. ### The adapter AI SDK tools are built with `tool()`, wrapping the input schema in `jsonSchema()`, so the schema the model sees is the same one Ratel ranks: ```ts import { type ExecutableTool } from "@ratel-ai/sdk"; import { jsonSchema, tool } from "ai"; export function toAISDKTool(executable: ExecutableTool) { return tool({ description: executable.description, inputSchema: jsonSchema(executable.inputSchema), execute: executable.execute, }); } ``` That is the entire adapter. It passes `execute` straight through, which is fine here because every executor in the example is async; note that direct top-K calls then bypass `catalog.invoke` and its `invoke_*` trace events. ### Assemble the turn and run the loop Each run puts the two capability tools in the list unconditionally, then adds the top-K BM25 hits for the prompt with their full schemas: ```ts import { invokeToolTool, searchCapabilitiesTool, type ToolCatalog } from "@ratel-ai/sdk"; import { type LanguageModel, stepCountIs, type Tool, ToolLoopAgent } from "ai"; import { toAISDKTool } from "./tools.js"; export async function runAgent(args: { prompt: string; model: LanguageModel; catalog: ToolCatalog; initialTopK?: number; maxSteps?: number; }): Promise { const { prompt, model, catalog } = args; const initialTopK = args.initialTopK ?? 3; const maxSteps = args.maxSteps ?? 8; const tools: Record = { search_capabilities: toAISDKTool(searchCapabilitiesTool(catalog)), invoke_tool: toAISDKTool(invokeToolTool(catalog)), }; for (const hit of catalog.search(prompt, initialTopK)) { const exec = catalog.getExecutable(hit.toolId); if (exec) tools[exec.id] = toAISDKTool(exec); } const agent = new ToolLoopAgent({ model, tools, toolChoice: "auto", stopWhen: stepCountIs(maxSteps), }); const result = await agent.generate({ prompt }); return result.text; } ``` `ToolLoopAgent` multi-steps inside one `.generate()` call, chaining `search_capabilities`, then `invoke_tool`, then the final answer when the pre-filtered set falls short. ### Run it Nothing in the wiring is OpenAI-specific: `runAgent` accepts any `LanguageModel`, so swapping providers is one import edit (`@ai-sdk/anthropic` instead of `@ai-sdk/openai`). ```ts import { openai } from "@ai-sdk/openai"; const text = await runAgent({ prompt: "read the files and find every TODO comment under src/", model: openai(process.env.AI_MODEL ?? "gpt-5-mini"), catalog, // your populated ToolCatalog }); ``` To run the shipped example from a checkout of the `ratel` repo (it is a private workspace package, so it rebuilds the SDK first): ```bash export OPENAI_API_KEY=sk-... pnpm install pnpm -F @ratel-ai/example-ai-sdk start "send an email to alice@example.com saying ship it" ``` Leave `OPENAI_API_KEY` unset and the example runs in diagnostic mode: it prints the BM25 top-3 for the prompt and exits without calling a model. > Direct top-K tools get the AI SDK's strict schema validation at the LLM boundary. Calls routed through `invoke_tool` do not: the model reads each tool's `inputSchema` from `search_capabilities` results and serializes the arguments itself, so validation happens only at the catalog's own `execute`. ## The same pattern over an MCP server [`examples/mcp-chat`](https://github.com/ratel-ai/ratel/blob/main/examples/mcp-chat/README.md) applies the identical wiring to tools it does not own: it ingests an upstream MCP server with [`registerMcpServer`](/docs/core/typescript/register-mcps), then rebuilds the tool list every turn. With 13 upstream tools and a top-K of 3, the model sees five tool definitions per turn; the rest stay reachable through `search_capabilities` / `invoke_tool` without occupying context. ## Any other framework LangChain, OpenAI Agents, a hand-rolled loop: the recipe does not change, because `ToolCatalog` entries and the capability-tool factories all return the same plain executable shape. Write one adapter and one turn assembler. The [Progressive disclosure](/docs/core/progressive-disclosure) page covers the contract the capability tools expose to the model. ## Next steps [Register tools](/docs/core/typescript/register-tools) Build the ToolCatalog these examples pre-filter. [Telemetry & OTel](/docs/core/typescript/telemetry) See the invoke and gateway events these integrations emit, locally and over OpenTelemetry. --- # Telemetry & OTel Source: https://docs.ratel.sh/docs/core/typescript/telemetry Ratel emits telemetry on two deliberately separate streams ([ADR 0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md)): **local trace events** recorded into a sink you configure, and **OpenTelemetry spans** exported as stock OTLP. Both are off by default. This page is the TypeScript wiring. The full event-type vocabulary and span table live on [Telemetry](/docs/core/telemetry). ## Local trace sinks Pass `trace` to the `ToolCatalog` (or `SkillCatalog`) constructor and the core records every search, invoke, capability-tool, upstream-MCP, and auth event into the sink. ```ts import { ToolCatalog } from "@ratel-ai/sdk"; const catalog = new ToolCatalog({ trace: { kind: "jsonl", sessionId: "session-1", path: "/tmp/ratel.jsonl" }, }); // Every catalog.invoke, capability-tool call, and registerMcpServer call now appends one JSON line per event to /tmp/ratel.jsonl. ``` `TraceSinkConfig` is a discriminated union: `{ kind: "noop" }`, `{ kind: "memory"; sessionId }`, or `{ kind: "jsonl"; sessionId; path }`. Misconfiguration throws (for example `"jsonl sink requires path"`). * `{ kind: "noop" }` — drop everything (the default when no `trace` is passed). * `{ kind: "memory"; sessionId }` — keep events in memory; drain via `catalog.drainTraceEvents()`. Useful in tests. * `{ kind: "jsonl"; sessionId; path }` — append one JSON line per event to `path` (mode `0600` on Unix). Best-effort, lossy on backpressure. Both catalogs also expose the sink directly. `recordEvent(event)` injects an event into the active sink, validating it against the core trace schema: an unrecognized shape throws `invalid trace event: ...`. `drainTraceEvents()` removes and returns the captured envelopes, `[]` unless the active sink is `memory`. `searchCapabilitiesTool` records its own `gateway_search` event and tags the underlying catalog search's `search` event with `origin: "agent"`; direct callers (`catalog.search(query, k)`) default to `"direct"`. Override per call via `catalog.search(query, k, "agent")`. The full event set is in [Telemetry](/docs/core/telemetry). ## OpenTelemetry export Independently of the local sink, the SDK emits OpenTelemetry spans for the same funnel — `execute_tool`, `ratel.search`, `ratel.skill.load`, `ratel.upstream.register`, `ratel.auth.flow` (the `gen_ai.*` / `ratel.*` vocabulary, catalogued in [Telemetry](/docs/core/telemetry)). The SDK's only OpenTelemetry dependency is `@opentelemetry/api`, never the OTel SDK. Until a provider is registered, every span is a no-op `NonRecordingSpan`. The `ratel.*` semantic-convention vocabulary itself ships as the OTel-free package [`@ratel-ai/telemetry`](/docs/core/packages/typescript/telemetry). ### Greenfield: ship spans to Ratel Cloud No OpenTelemetry in your stack yet? One call at startup registers a global tracer provider with a batch OTLP exporter. The exporter is the optional peer [`@ratel-ai/telemetry-otlp`](/docs/core/packages/typescript/telemetry-otlp) (without it, `configureTelemetry` throws with this install hint): ```bash npm install @ratel-ai/telemetry-otlp ``` ```ts import { configureTelemetry } from "@ratel-ai/sdk"; const handle = await configureTelemetry({ endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces", apiKey: process.env.RATEL_API_KEY, }); // ... run your agent: every ratel.* / execute_tool span now exports ... await handle.shutdown(); // flushes and stops the exporter ``` `configureTelemetry(options?: ConfigureTelemetryOptions): Promise` — `ConfigureTelemetryOptions` extends `InitOptions` with `captureContent` / `includeSpanAndEvents`, in-code overrides of the [content-capture](#content-capture) gate. All optional: * `endpoint` — the full OTLP traces URL including `/v1/traces`; falls back to the `RATEL_URL` env var. With neither set, the call fails before anything registers. * `apiKey` — sent as `Authorization: Bearer `; when omitted it falls back to the `RATEL_API_KEY` env var (unless you already set an `Authorization` header explicitly). * `headers` — merged in before the Bearer header. * `serviceName` — defaults to `"ratel"`. * `captureContent` / `includeSpanAndEvents` — set the content-capture mode in code, winning over the env var. > `configureTelemetry` owns the global provider. If one is already registered it fails loudly, pointing at the span processor, rather than silently no-op'ing. That error means you are on the wrong path: dual-export instead. ### Already on OpenTelemetry: dual-export If Langfuse, the Vercel AI SDK, or your own collector already owns the provider, the SDK's spans are already flowing to it. Skip `configureTelemetry` and add the Ratel span processor to send a copy to Ratel Cloud: ```ts import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { LangfuseSpanProcessor } from "@langfuse/otel"; import { ratelSpanProcessor } from "@ratel-ai/telemetry-otlp"; const provider = new NodeTracerProvider({ spanProcessors: [ new LangfuseSpanProcessor(), // Langfuse keeps every span ratelSpanProcessor({ // Ratel takes gen_ai.*/ratel.* only endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces", apiKey: process.env.RATEL_API_KEY, }), ], }); provider.register(); ``` `ratelSpanProcessor(opts)` accepts the endpoint/auth options (`endpoint`, `apiKey`, `headers`) plus `spanFilter` and `enabled`; `ratelTraceExporter(opts)` is the bare OTLP exporter if you wire your own processor. The default filter forwards a span if its name starts with `ratel.` or any attribute key starts with `gen_ai.` or `ratel.`; pass `spanFilter: () => true` to forward everything. ### Content capture Message and tool content never leaves the process by default. The gated payloads (`ratel.search.query`, `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`) ride span attributes only when the capture gate selects a span mode (`SPAN_ONLY` or `SPAN_AND_EVENT`); `EVENT_ONLY` does not put content on spans. Set the gate in code via `configureTelemetry({ captureContent })`; it wins over the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` env var, the fallback when nothing is set in code. The full mode table is on [Telemetry](/docs/core/telemetry). ## Next steps [Telemetry](/docs/core/telemetry) The full event-type vocabulary, span table, and content-capture modes. [Packages](/docs/core/typescript/packages) Raw API references and changelogs for @ratel-ai/telemetry and @ratel-ai/telemetry-otlp. --- # Packages Source: https://docs.ratel.sh/docs/core/typescript/packages Ratel's TypeScript surface is four npm packages: | Package | What it is | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | [`@ratel-ai/sdk`](/docs/core/packages/typescript/sdk) | The TypeScript SDK — catalogs, capability tools, MCP ingestion | | [`@ratel-ai/telemetry`](/docs/core/packages/typescript/telemetry) | OTel-free `ratel.*` semantic-convention vocabulary | | [`@ratel-ai/telemetry-otlp`](/docs/core/packages/typescript/telemetry-otlp) | One-call OTLP exporter wiring (`init()`, `ratelSpanProcessor`) | | [`@ratel-ai/mcp-server`](/docs/core/packages/typescript/mcp-server) | The package that powers [Ratel Local](/docs/local) — MCP serving, CLI configuration, and library embedding | Each Core package page carries the raw API reference generated from source doc comments, plus the package's changelog. The `@ratel-ai/mcp-server` page points to its separately maintained source and product documentation. For the cross-language map — the Rust core and the Python packages — see [Packages](/docs/core/packages). --- # Install Source: https://docs.ratel.sh/docs/core/python/install One package, no service: ```bash pip install "ratel-ai==0.4.0" ``` The docs pin the version they document; drop the pin to track the latest release. The base SDK needs **Python 3.10 or newer** for the examples in these guides and has no runtime dependencies. ## Prebuilt wheels The SDK bundles Ratel's Rust core as a native module. Prebuilt `abi3` wheels ship for: * macOS: `darwin-arm64`, `darwin-x64` * Linux (glibc): `linux-x64-gnu`, `linux-arm64-gnu` * Windows: `win32-x64-msvc` On these targets nothing compiles at install time and no Rust toolchain is needed. ## Extras * **`mcp`** — `pip install 'ratel-ai[mcp]'` pulls the official MCP client for [`register_mcp_server`](/docs/core/python/register-mcps). Lazily imported: the base SDK installs and runs without it. * **`otlp`** — `pip install 'ratel-ai[otlp]'` pulls the `ratel.*` span vocabulary and the OTLP exporter behind `configure_telemetry()`. See [Telemetry & OTel](/docs/core/python/telemetry). * **Semantic & hybrid retrieval** — not an extra. Opting in downloads the local embedding model on first use; see [Build the embedding cache](/docs/core/python/register-tools#build-the-embedding-cache) and [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Quickstart](/docs/core/python/quickstart) Register a tool and let an agent discover it through the capability tools. [Packages](/docs/core/python/packages) The two PyPI packages behind the Python surface. --- # Quickstart Source: https://docs.ratel.sh/docs/core/python/quickstart This is the Python path. TypeScript reader? Start at the [TypeScript quickstart](/docs/core/typescript/quickstart). Rather not write the integration yourself? Point your coding agent at the [skills suite](/docs/core/quickstart). Ratel is framework-agnostic: everything the SDK hands you is a plain executable tool — metadata plus an `execute` handler — that plugs into any agent loop. This page shows the two ways in: [starting from scratch](#start-from-scratch) and [migrating an existing agent](#migrate-an-existing-agent). > **Before you start** > > Use Python 3.10 or newer. Nothing on this page calls a model, so no API key is needed. ## How it works Everything starts with a **`ToolCatalog`**: register each of your tools once, pairing its metadata (id, description, JSON schemas) with the handler that runs it. A **`SkillCatalog`** holds Markdown playbooks the same way, ranked in their own corpus. The agent reaches both through the three capability tools — `search_capabilities` (find tools and skills by description), `invoke_tool` (run a tool by id), and `get_skill_content` (load a skill's body) — so the full catalog never enters the prompt. How the pieces fit per turn is covered in [Architecture](/docs/core/architecture). ## Start from scratch ### Create a project and install the SDK ```bash mkdir ratel-quickstart && cd ratel-quickstart python -m venv .venv source .venv/bin/activate python -m pip install "ratel-ai==0.4.0" ``` Requirements, prebuilt wheels, and the `mcp` / `otlp` extras are on [Install](/docs/core/python/install). ### Register a tool and a skill Create `main.py`. Tools carry their metadata and handler; skills carry a description and a Markdown body: ```python import asyncio from ratel_ai import ( ExecutableTool, Skill, SkillCatalog, ToolCatalog, get_skill_content_tool, invoke_tool_tool, search_capabilities_tool, ) catalog = ToolCatalog() catalog.register( ExecutableTool( id="get_weather", name="get_weather", description="Get the current weather for a city.", input_schema={ "type": "object", "properties": { "city": {"type": "string", "description": "City name"}, }, "required": ["city"], }, output_schema={ "type": "object", "properties": { "city": {"type": "string"}, "temperatureC": {"type": "number"}, "condition": {"type": "string"}, }, "required": ["city", "temperatureC", "condition"], }, execute=lambda args: { "city": args["city"], "temperatureC": 24, "condition": "sunny", }, ) ) skills = SkillCatalog() skills.register( Skill( id="weather-briefing", name="weather-briefing", description="How to report the weather: temperature first, then condition.", body="## Weather briefing\nAnswer in one sentence: temperature first, then the condition. No filler.", ) ) # The three capability tools are the only tools your agent needs to see. # Construct them after registering, so the skills bucket is advertised. 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" ``` ### Run the discovery loop by hand Append the exact calls a model makes at runtime — capability tools own `async` handlers, so await `execute` directly — then run the file: ```python async def main() -> None: print(await search.execute({"query": "weather in Rome"})) print(await invoke.execute({"toolId": "get_weather", "args": {"city": "Rome"}})) print(await load.execute({"skillId": "weather-briefing"})) asyncio.run(main()) ``` ```bash python main.py ``` Three results, no model key involved: * `search_capabilities` returns two independently-ranked buckets: `get_weather` under `tools`, `weather-briefing` under `skills`. Neither schema nor body entered any prompt to get there. * `invoke_tool` runs the handler and returns its result untouched: `{"city": "Rome", "temperatureC": 24, "condition": "sunny"}`. * `get_skill_content` returns the playbook: `{"body": "## Weather briefing\n…"}`. The full result shapes and error contract are on [Progressive disclosure](/docs/core/progressive-disclosure). ### Hand the loop to your agent Each capability tool is a plain `ExecutableTool` — `id`, `name`, `description`, `input_schema`, `output_schema`, `execute`. Map those fields onto your framework's tool type (a few lines anywhere), add the three tools to the agent's tool list, and tell it to search before answering: ```text Find tools and playbooks with search_capabilities. Run tools with invoke_tool. When a skill matches, load it with get_skill_content and follow it. ``` Ready-made wiring — the Pydantic AI adapter and per-turn assembly — is on [Framework integrations](/docs/core/python/framework-integrations). Production agents usually add a top-K pre-filter next to the capability tools; that pattern (and the sync-vs-async dispatch rules) is on [Use the discovery tools](/docs/core/python/discovery-tools). ## Migrate an existing agent A migration changes where capabilities live, not your model or business logic. Start with one workflow, prove it still behaves the same, then repeat. | Existing asset | After migration | | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Tool IDs, schemas, and handlers | Register once in `ToolCatalog`; keep authorization and side effects in the handler. | | Conditional runbooks and examples | Register in `SkillCatalog`; load only when relevant. | | Identity, safety, universal policy, output rules, and runtime context | Keep in the core prompt. | | Model and agent loop | Keep both; reuse the loop's prompt and tool seams. | ### Find the integration points Find where your process creates long-lived dependencies and where each request builds the model's tool list. Create the catalogs at startup; change the model-facing list at the request boundary. ### Register an existing tool Tool definitions leave the repeated model request, not your code. Preserve the ID, schema contract, handler, authorization, permissions, and side effects. Convert framework-native schemas to JSON Schema at this boundary. ```python # Before: the schema is sent to the model on every request tools_for_every_request = [existing_refund_tool] # After: the same contract is registered once at startup catalog = ToolCatalog() catalog.register( ExecutableTool( id="issue_refund", name="issue_refund", description="Issue an approved refund for an order.", input_schema=issue_refund_input_schema, output_schema=issue_refund_output_schema, execute=issue_refund, # the existing handler ) ) ``` Register each tool in this workflow the same way. During a gradual migration, leave unmigrated tools in the old list and remove each migrated definition from it. ### Extract an on-demand skill Keep instructions and runtime context that must govern every turn in the core prompt. Move only conditional guidance, such as the refund workflow, into a self-contained skill: ```python # Before: the refund workflow is sent on every turn instructions = "\n\n".join( [CORE_PROMPT, SAFETY_POLICY, OUTPUT_RULES, RUNTIME_CONTEXT, REFUND_RUNBOOK] ) # After: everything except the runbook stays in the core prompt core_instructions = "\n\n".join( [CORE_PROMPT, SAFETY_POLICY, OUTPUT_RULES, RUNTIME_CONTEXT] ) skills = SkillCatalog() skills.register( Skill( id="refund-runbook", name="refund-runbook", description="Check refund eligibility, obtain approval, then issue the refund.", tools=["issue_refund"], body=REFUND_RUNBOOK, ) ) ``` The description says when to load the skill. Its `tools` list surfaces `issue_refund` beside the matched playbook. ### Rewire and verify the agent Add the discovery policy to the core prompt. After both catalogs are populated, build the three Ratel capability tools and adapt them at the request boundary: ```python discovery_instructions = """ Find tools and playbooks with search_capabilities. Run tools with invoke_tool. When a skill matches, load it with get_skill_content and follow it. """.strip() instructions = "\n\n".join([core_instructions, discovery_instructions]) capability_tools = [ search_capabilities_tool(catalog, skills), invoke_tool_tool(catalog), get_skill_content_tool(skills), ] ``` The capability tools are plain `ExecutableTool` values. [Framework integrations](/docs/core/python/framework-integrations) shows the adapter and the production top-K turn assembler. A refund request should now follow this path: 1. `search_capabilities` returns `refund-runbook` and `issue_refund`. 2. `get_skill_content` loads the eligibility and approval steps. 3. `invoke_tool` calls the unchanged `issue_refund` handler. 4. The agent answers from the tool result. Before migrating the next workflow, verify the skill ranks, its declared tool appears, the handler receives the same arguments, and the initial request excludes the full catalog and refund runbook. > **Let a coding agent do the migration** > > The [skills suite](/docs/core/quickstart) ships skills for Claude Code, Cursor, Codex, and > similar: `ratel-integrate` plans this migration for your codebase, and > `ratel-decompose-prompt` splits the system prompt into skills. ## Next steps [Framework integrations](/docs/core/python/framework-integrations) Ready-made wiring for Pydantic AI — and the recipe for any other loop. [Skills suite](/docs/core/quickstart) Coding-agent skills that assess your codebase and apply this integration for you. [Register tools](/docs/core/python/register-tools) The anatomy of an executable tool, the full ToolCatalog surface, and ranking without execution. [Register skills](/docs/core/python/register-skills) The full SkillCatalog surface: tags, declared tools, and how skills rank. --- # Register tools Source: https://docs.ratel.sh/docs/core/python/register-tools Everything the model can reach lives in a **`ToolCatalog`**: local functions, upstream MCP servers' tools, or both, ranked as one surface. This page covers local tools; upstream servers are on [Register MCP servers](/docs/core/python/register-mcps). ## The anatomy of an executable tool An executable tool pairs the metadata Ratel indexes with the handler that runs it. Registering a local tool is one `register` call: ```python from ratel_ai import ExecutableTool, ToolCatalog catalog = ToolCatalog() catalog.register( ExecutableTool( id="read_file", name="read_file", description="Read a file from local disk and return its textual contents.", input_schema={ "type": "object", "properties": {"path": {"type": "string", "description": "absolute path to the file"}}, "required": ["path"], }, output_schema={"type": "object", "properties": {"contents": {"type": "string"}}}, execute=lambda args: {"contents": open(args["path"]).read()}, ) ) ``` What each field is for: * **`id`** — the stable handle everything else uses: `catalog.invoke(id, args)`, the `toolId` the model passes to `invoke_tool`, and the `toolId` on every search hit. Unique per catalog; [MCP-ingested tools](/docs/core/python/register-mcps) get a server-namespaced id automatically. * **`name`** — the tool's name where it appears as a direct tool definition, e.g. when you pre-filter top-K hits into the model's tool list. * **`description`** — what the model reads to pick the tool, and the main text BM25 ranks against. This is the field that decides whether the tool is ever found; see the definition-writing guidance in [Keyword search](/docs/core/keyword-search). * **`input_schema`** / **`output_schema`** — JSON Schema for the arguments and the result. The input schema rides every `search_capabilities` hit, so the model can call the tool without a second lookup. Both default to `{}`. * **`execute`** — the handler, sync or async. `catalog.invoke` awaits it either way and rethrows whatever it throws (after recording an `invoke_error` trace event), so failures surface where you call it. `register` raises `ValueError` if `execute` is `None`. ## The `ToolCatalog` surface The catalog is the registry plus an executor per tool. The methods you will use: ```python import asyncio from ratel_ai import ExecutableTool, ToolCatalog async def main() -> None: catalog = ToolCatalog() # ToolCatalog(trace=None, method="bm25") catalog.register( # metadata + execute ExecutableTool( id="echo", name="echo", description="Repeat a message back to the caller.", input_schema={"properties": {"text": {"type": "string"}}}, output_schema={"properties": {"text": {"type": "string"}}}, execute=lambda args: {"text": args["text"]}, ) ) hits = catalog.search("repeat a message", 5) # → list[SearchHit] assert hits[0].tool_id == "echo" catalog.build_embeddings() # pre-compute semantic/hybrid embeddings assert catalog.has("echo") # → bool assert catalog.get("echo") is not None # → Tool | None assert catalog.get_executable("echo") is not None print(await catalog.invoke("echo", {"text": "hello"})) catalog.drain_trace_events() # → list[dict]; memory sink only asyncio.run(main()) ``` `invoke` calls the handler, awaits it only if it returned a coroutine (so sync and async executors both work), and re-raises whatever it throws after recording an `invoke_error` trace event. `search` defaults to `origin="direct"`; pass `catalog.search(query, k, "agent")` to tag a search as model-initiated in telemetry. Two constructor keywords: `trace=` wires a local [trace sink](/docs/core/python/telemetry) (default: none), and `method=` sets the catalog's default retrieval method (default `"bm25"`). The per-call override lives on [Use the discovery tools](/docs/core/python/discovery-tools); how the engines rank is on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Build the embedding cache BM25 needs no preparation. Semantic and hybrid retrieval rank against a **pre-built embedding cache** — a search embeds only the query, never the corpus — so the cache must cover every tool before the first such search. Two ways to keep it current: ```python # Eager: a semantic- or hybrid-default catalog embeds every tool as it is # registered. Nothing else to call; a model-load failure raises at register. eager = ToolCatalog(method="hybrid") # Explicit: a BM25-default catalog embeds nothing until you ask. Build the # cache after registering, then opt in per call. lexical = ToolCatalog() # ...register tools... lexical.build_embeddings() # synchronous; raises RuntimeError if the model fails to load lexical.search("rotate the api key", 5, method="semantic") ``` `build_embeddings()` is incremental and keyed by id: it embeds only what is missing, so adding or re-registering one tool costs one embedding, not N, and a call with nothing left to embed is a no-op. Re-registering a tool invalidates its cached vector — on an explicit catalog, build again before the next semantic or hybrid search. A search over an incomplete cache raises instead of silently embedding the corpus. The first embedding loads the bundled local model (`BAAI/bge-small-en-v1.5`) in-process, downloading it into the HuggingFace cache on first use — no service, no API key. The model's footprint, offline use, and when the engines are worth it are on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## `ToolRegistry`: ranking without execution Need only the ranking, and you will dispatch tool calls yourself? `ToolRegistry` is the metadata-only BM25 index underneath `ToolCatalog`, with no executors and no capability tools. It takes positional metadata rather than a dataclass: ```python from ratel_ai import ToolRegistry registry = ToolRegistry() registry.register( "read_file", "read_file", "Read a file from local disk and return its textual contents.", {"properties": {"path": {"type": "string"}}}, {"properties": {"contents": {"type": "string"}}}, ) registry.search("read a text file", 5) # → [SearchHit(tool_id="read_file", score=1.42), ...] ``` `SkillRegistry` is the same executor-free index over skills: `register(id, name, description, tags, tools, metadata, body)`, with hits as `SkillHit` (`.skill_id`, `.score`). Both registries also expose `search_with_origin`, `search_with_method`, `build_embeddings`, and the trace-sink plumbing (`set_trace_sink`, `record_event`, `drain_trace_events`) that the catalogs build on. ## Spot-check what you registered Verify a registration the way the model will find it: run a direct search and check that the expected ids surface. Local and upstream tools rank together. ```python hits = catalog.search("read a text file from disk", 5) for hit in hits: print(hit.tool_id, hit.score) # read_file 2.1 # fs__read_file 1.8 ``` This is the same ranking `search_capabilities` runs on the model's behalf — a direct `catalog.search` is tagged `origin: "direct"` in telemetry, the capability tool's searches `"agent"`. If a tool you expect near the top doesn't surface, the fix is almost always its wording, not the ranking. ## Next steps [Register MCP servers](/docs/core/python/register-mcps) Ingest an upstream server's tools into the same ranked surface. [Use the discovery tools](/docs/core/python/discovery-tools) Expose the catalog to the model through search\_capabilities and invoke\_tool. [Keyword search](/docs/core/keyword-search) How BM25 ranks the catalog, and how to write descriptions it finds. --- # Register MCP servers Source: https://docs.ratel.sh/docs/core/python/register-mcps `register_mcp_server` lists an upstream server's tools and registers each one into the catalog, wiring its executor to the upstream `tools/call` over the same connection. It ships behind the `mcp` extra (Python 3.10+): ```bash pip install 'ratel-ai[mcp]' ``` ## Ingest an MCP server's tools You own the `ClientSession` lifecycle — set up the transport and session with `async with`, then pass the initialized session in. For a remote server, that transport is Streamable HTTP: ```python import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from ratel_ai import ToolCatalog, register_mcp_server async def main() -> None: catalog = ToolCatalog() async with streamablehttp_client("https://example.com/mcp") as (read, write, _): async with ClientSession(read, write) as session: init = await session.initialize() handle = await register_mcp_server( catalog, name="issues", session=session, transport_label="streamable-http", # recorded on upstream_register instructions=init.instructions, # echoed as server_instructions ) print(handle.tool_ids) # ["issues__create_issue", ...] asyncio.run(main()) ``` A local server spawned as a subprocess uses the stdio transport instead — the session handling stays the same: ```python from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client params = StdioServerParameters( command="npx", args=["-y", "@modelcontextprotocol/server-everything"] ) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: init = await session.initialize() handle = await register_mcp_server( catalog, name="fs", session=session, transport_label="stdio", instructions=init.instructions, ) # handle.tool_ids → ["fs__echo", "fs__add", ...] ``` `catalog.search` and `catalog.invoke` now include the upstream tools. The third transport, `sse_client` (SSE), comes from the same `mcp` package. Because the caller owns the session, `transport_label` and `instructions` are caller-supplied — they default to `"unknown"` and `None`. > The caller-owns-the-session split is the one deliberate divergence from the TypeScript SDK, whose `registerMcpServer` reads the server instructions and transport label from the live handshake itself. Pass `instructions=` and `transport_label=` from your `await session.initialize()` result to make the emitted `upstream_register` event byte-identical across the two SDKs. ## Namespaced ids and the handle Each upstream tool registers under **`__`** — `fs__read_file`, `github__create_issue`. The prefix keeps ids collision-free across servers, and it is how `search_capabilities` groups results by server (everything before the first `__`). An upstream tool with no description registers with `""`, leaving only its name and schema text to rank on — worth fixing upstream or documenting around. The returned handle carries three things: * `tool_ids` — the namespaced ids that were registered. * `server_instructions` — echoes whatever you passed as `instructions=`. * `close()` — an awaitable no-op by default (your `async with` blocks already own teardown); pass `on_close=` (an async callable) if the handle should tear something down itself. ## Error propagation An upstream `tools/call` failure records an `upstream_error` trace event and rethrows, so it propagates as a raised exception from `catalog.invoke` — the same handling path as a local executor that throws. When the model reaches the tool through `invoke_tool`, that failure comes back as a structured `{ "error": "tool threw: ...", "isError": true }` instead, so the loop stays recoverable. ## Many servers, one ranked surface Call `register_mcp_server` once per server and keep registering local tools into the same catalog: the model sees a single ranked surface through the same [capability tools](/docs/core/progressive-disclosure). To make the aggregation visible, pass the upstream list to the `search_capabilities` factory with the keyword-only `upstream_servers=`: a sequence of `UpstreamServerInfo(name, description=None, instructions=None, tool_count=None, needs_auth=False)`. The tool's description then names each aggregated server, and any result group whose server name matches gains that server's `description` and `instructions`. ## Embeddings for upstream tools Upstream tools enter the same corpus as local ones, so the [embedding cache](/docs/core/python/register-tools#build-the-embedding-cache) treats them identically. On a semantic- or hybrid-default catalog, `register_mcp_server` embeds every ingested tool as part of registration — budget a model load plus one embedding per tool on the first ingest. On a BM25-default catalog, call `catalog.build_embeddings()` once after the handle returns, before the first semantic or hybrid search. A re-sync that re-registers an upstream tool invalidates its cached vector: eager catalogs re-embed at register, explicit ones need another `build_embeddings()`. What the engines cost is on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Register skills](/docs/core/python/register-skills) Add Markdown playbooks that rank in their own corpus, next to the tools. [Use the discovery tools](/docs/core/python/discovery-tools) Expose the aggregated catalog to the model, including auth-expiry handling. [Ratel Local](/docs/local/mcp-servers) Aggregate upstream servers with Ratel Local instead of in your own code. --- # Register skills Source: https://docs.ratel.sh/docs/core/python/register-skills Skills are Markdown playbooks (a deploy runbook, a debugging checklist) ranked by a *separate* BM25 corpus from tools. What a skill is and when to reach for one is on [Tools vs MCP vs skills](/docs/core/tools-mcp-skills); Ratel's own coding-agent skills — including `ratel-decompose-prompt`, which extracts skills from a long system prompt — are on the [Quick start](/docs/core/quickstart). ## Register a skill ```python from ratel_ai import Skill, SkillCatalog, get_skill_content_tool, search_capabilities_tool 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 for ranking tools=["vercel__deploy", "fs__read_file"], # surfaced alongside the skill when it matches metadata={"stacks": ["next", "vercel"]}, # non-indexed context for higher-layer ranking body="## Deploying to Vercel\n1. ...", # returned by get_skill_content_tool ) ) search = search_capabilities_tool(catalog, skills) # 2nd arg → result gains a populated `skills` bucket load = get_skill_content_tool(skills) # id == "get_skill_content" ``` Only `id`, `name`, and `description` are required; `tags`, `tools`, `metadata`, and `body` are optional. `get_skill_content({skillId})` returns `{"body": ...}`, or `{"error": ..., "isError": True}` for an unknown id. ## How skills surface Pass a `SkillCatalog` as the second argument to `search_capabilities_tool` and search returns the `skills` bucket alongside `tools`, each with its own result budget, so a relevant skill is never starved by matching tools. The agent pulls a skill's full body into context on demand via `get_skill_content_tool` (id `get_skill_content`, constant `GET_SKILL_CONTENT_ID`). A skill can also declare the `tools` its instructions call: when the skill matches a query, those tools are pulled into the `tools` bucket (additively, deduped) so the agent gets the playbook *and* the tools it needs in one turn instead of a second search. ## The `SkillCatalog` surface `SkillCatalog(trace=None, method="bm25")` takes the same constructor keywords as `ToolCatalog`, and its method surface mirrors it over the skill corpus: ```python skills.search(query, top_k) # → list[SkillHit] (.skill_id, .score); origin=, method= as on ToolCatalog skills.has(skill_id) # → bool skills.get(skill_id) # → Skill | None skills.size() # → int skills.invoke(skill_id) # → str: the body; raises ValueError on an unknown id ``` `build_embeddings()`, `record_event()`, and `drain_trace_events()` work exactly as on `ToolCatalog`. Note that `invoke` is synchronous here, no `await`: loading a skill body is a lookup, not an executor call. It records a `skill_invoke` trace event. ## Embeddings for skills The skill corpus has its own embedding cache, maintained exactly like the tool one ([Build the embedding cache](/docs/core/python/register-tools#build-the-embedding-cache)): construct `SkillCatalog` with `method="semantic"` or `"hybrid"` to embed eagerly at register, or keep the BM25 default and call `skills.build_embeddings()` before opting in per call. The two caches are independent — skills can rank hybrid while tools stay on BM25. Embeddings cover the same projection the ranking indexes — name, description, tags — never the body, so a long playbook costs one small embedding. When the engines are worth it is on [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Use the discovery tools](/docs/core/python/discovery-tools) Hand the agent search\_capabilities, invoke\_tool, and get\_skill\_content. [Tools vs MCP vs skills](/docs/core/tools-mcp-skills) When a capability should be a tool, an MCP server, or a skill. [Skills suite](/docs/core/quickstart) Coding-agent skills that extract, tune, and maintain the skills your catalog serves. --- # Use the discovery tools Source: https://docs.ratel.sh/docs/core/python/discovery-tools The capability tools wrap a catalog into tools the agent can call itself: 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](/docs/core/progressive-disclosure). ```python search = search_capabilities_tool(catalog) # id == "search_capabilities" invoke = invoke_tool_tool(catalog) # id == "invoke_tool" ``` `search_capabilities_tool` and `invoke_tool_tool` return plain `ExecutableTool` objects (`id`, `name`, `description`, `input_schema`, `output_schema`, `execute`). Wrap each one in your framework's tool type and run your normal loop. ## `search_capabilities` **`search_capabilities({query, topKTools?, topKSkills?})`** returns two independently-ranked buckets, so a relevant skill is never crowded out by matching tools (result keys are camelCase, a wire contract shared with the TypeScript SDK and MCP): ```jsonc { "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": "..."}] } ``` `topKTools` defaults to 5 and `topKSkills` to 3: integers above 50 clamp down to 50; a missing, `0`, negative, `bool`, or non-integer value falls back to the default rather than erroring. The `skills` bucket is always present and stays empty until you pass a `SkillCatalog` — see [Register skills](/docs/core/python/register-skills). The keyword-only `upstream_servers=` advertises upstream MCP servers in the tool's description — see [Register MCP servers](/docs/core/python/register-mcps). > **Upgrading from 0.1.x?** > > `search_tools_tool` (id `search_tools`, constant `SEARCH_TOOLS_ID`) is still exported as a deprecated, tools-only shim that keeps its original `{"groups": ...}` result shape. Migrate to `search_capabilities_tool`. ## `invoke_tool` **`invoke_tool({toolId, args})`** runs `catalog.invoke(tool_id, args)` and returns the tool's result. Arguments go *nested* under `args`, but a flattened call still works: when `args` is missing or `None`, every top-level key except `toolId` and `args` is forwarded to the tool. On a bad call it returns a structured `{"error": ..., "isError": True}` instead of raising, so a model mistake (unknown id, malformed args, a handler that throws) stays recoverable inside the loop rather than crashing the host. Expired upstream credentials get the same treatment. When an executor raises an exception whose type is named `UnauthorizedError` (duck-typed, so any library's class qualifies), `invoke_tool` returns `{"error": "needs_auth", "isError": True, "hint": "call the auth tool to re-authorize ", "upstream": ""}` — the upstream inferred from the id prefix before `__`. Pass `on_unauthorized=` to `invoke_tool_tool` (an `OnUnauthorized` callback, sync or async, called with the upstream name) to trigger your re-auth flow at that moment. ## Skill content With a `SkillCatalog` registered, a third capability tool joins the set. It returns a skill's full body on demand: ```python search = search_capabilities_tool(catalog, skills) # 2nd arg → result gains a populated `skills` bucket load = get_skill_content_tool(skills) # id == "get_skill_content" ``` ## Assemble the per-turn toolset The capability tools are the escape hatch; the pre-filter covers the common case. Each turn, put the two capability tools in the list unconditionally, then add the top-K catalog hits for the user's message. The full catalog never enters the prompt: ```python def tools_for_turn(user_message: str) -> list[ExecutableTool]: capabilities = [search_capabilities_tool(catalog), invoke_tool_tool(catalog)] top_k = [ executable for hit in catalog.search(user_message, 3) # BM25: the 3 most relevant tools if (executable := catalog.get_executable(hit.tool_id)) is not None ] return [*capabilities, *top_k] ``` There are two dispatch paths, and getting them right matters because a registered executor may be sync or async: * **Catalog tools** (your top-K hits): dispatch through `await catalog.invoke(tool_id, args)`. It awaits the handler's coroutine only when needed and records the `invoke_*` trace events. Never `await` a raw `execute` yourself, or a sync handler raises. * **The two capability tools** (`search_capabilities` / `invoke_tool`): they are *not* catalog entries, and they own `async` handlers, so await `t.execute(args)` directly. The full Pydantic AI wiring is on [Framework integrations](/docs/core/python/framework-integrations). ## Retrieval methods Ranking is BM25 unless you opt out. Both catalogs take a `method=` constructor keyword, and `search` accepts the same values per call (`"bm25"`, the default, `"semantic"`, or `"hybrid"`); the per-call value wins: ```python catalog = ToolCatalog(method="hybrid") # catalog-wide default catalog.search("resize the logo", 5) # ranks with "hybrid" catalog.search("resize the logo", 5, method="bm25") # per-call override ``` Semantic and hybrid rank against a prebuilt embedding cache. A catalog constructed as `"semantic"` or `"hybrid"` embeds each tool as it is registered, so a search only ever embeds the query; on a BM25-default catalog, call `build_embeddings()` before a per-call override — the full cache lifecycle is on [Build the embedding cache](/docs/core/python/register-tools#build-the-embedding-cache). A search with an unbuilt cache raises `RuntimeError`; an unknown method string raises `ValueError`. `search_capabilities` passes no method, so it always inherits the catalog's construction-time default — switching the agent's engine means constructing the catalog with it. How BM25 ranks is covered in [Keyword search](/docs/core/keyword-search); how the semantic and hybrid engines rank, fuse, and fail is covered in [Semantic & hybrid search](/docs/core/semantic-hybrid-search). ## Next steps [Framework integrations](/docs/core/python/framework-integrations) Wire the capability tools and the pre-filter into Pydantic AI. [Progressive disclosure](/docs/core/progressive-disclosure) The full contract these tools expose to the model, including the error table. [Semantic & hybrid search](/docs/core/semantic-hybrid-search) When to switch methods, and what the embedding path costs. --- # Framework integrations Source: https://docs.ratel.sh/docs/core/python/framework-integrations Ratel ships no framework plugins because it needs none. Everything the SDK hands you — your registered tools and the capability tools (`search_capabilities` / `invoke_tool`; `get_skill_content` joins the set when a `SkillCatalog` is registered) — is a plain executable tool: metadata plus an `execute` handler. Wiring a framework is two small pieces: 1. **An adapter** from that shape to the framework's tool type. 2. **A turn assembler** that builds each turn's tool list: the always-present capability tools plus the top-K catalog hits for the user's message ([ADR 0004](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0004-retrieval-and-tool-selection.md) replace-mode pre-filtering). This page follows the shipped [`examples/pydantic-ai`](https://github.com/ratel-ai/ratel/blob/main/examples/pydantic-ai/README.md). Using the Vercel AI SDK? Its wiring lives on the [TypeScript framework integrations](/docs/core/typescript/framework-integrations) page. ## Pydantic AI The example pins `pydantic-ai>=1.0` alongside `ratel-ai`, on Python 3.10+. Every tool is built from the catalog's JSON schemas via Pydantic AI's `Tool.from_schema`, no Python signature introspection, so the schema the model sees is the one Ratel ranks. ### The adapter: two dispatch paths Catalog tools (the top-K hits) dispatch through `catalog.invoke`; the capability tools own `async def` handlers and are awaited directly: ```python from typing import Any from pydantic_ai import Tool from ratel_ai import ToolCatalog def _tool_from_fn(fn: Any, name: str, description: str, schema: dict[str, Any]) -> Tool: fn.__name__ = name return Tool.from_schema(fn, name=name, description=description, json_schema=schema) def _catalog_tool(catalog: ToolCatalog, tool_id: str, description: str, schema: dict[str, Any]) -> Tool: # `invoke` is the SDK's tested entry point: it handles sync *and* async # executors and emits the `invoke_*` trace events. async def fn(**kwargs: Any) -> Any: return await catalog.invoke(tool_id, kwargs) return _tool_from_fn(fn, tool_id, description, schema) def _capability_tool(execute: Any, name: str, description: str, schema: dict[str, Any]) -> Tool: # search_capabilities / invoke_tool are not catalog entries; their execute # handlers are async, so await them directly. async def fn(**kwargs: Any) -> Any: return await execute(kwargs) return _tool_from_fn(fn, name, description, schema) ``` > Never `await` a catalog tool's raw `execute` in Python: executors may be sync, and re-wrapping them is how a sync executor ends up wrongly awaited (`TypeError: object dict can't be used in 'await' expression`). Route catalog tools through `catalog.invoke`, which awaits only when needed. ### Assemble the turn and run the agent ```python from pydantic_ai import Agent from ratel_ai import invoke_tool_tool, search_capabilities_tool def build_tools(catalog: ToolCatalog, prompt: str, initial_top_k: int = 3) -> list[Tool]: search = search_capabilities_tool(catalog) invoke = invoke_tool_tool(catalog) tools: dict[str, Tool] = { search.id: _capability_tool(search.execute, search.id, search.description, search.input_schema), invoke.id: _capability_tool(invoke.execute, invoke.id, invoke.description, invoke.input_schema), } for hit in catalog.search(prompt, initial_top_k): executable = catalog.get_executable(hit.tool_id) if executable is not None: tools[executable.id] = _catalog_tool( catalog, executable.id, executable.description, executable.input_schema ) return list(tools.values()) async def run_agent(*, prompt: str, model: str, catalog: ToolCatalog) -> str: agent = Agent(model, tools=build_tools(catalog, prompt)) result = await agent.run(prompt) return result.output ``` Pydantic AI runs its own loop: `await agent.run(prompt)` auto-executes tools and threads results back until the final answer. `model` is a provider-prefixed id string rather than a model object, and the example sets no explicit step cap. ### Run it ```python import asyncio import os from agent import run_agent from tools import build_catalog # your ToolCatalog assembly model = os.environ.get("RATEL_EXAMPLE_MODEL", "openai:gpt-5-mini") text = asyncio.run(run_agent(prompt="find every TODO comment under src/", model=model, catalog=build_catalog())) ``` To run the shipped example from a checkout (`uv run` builds the native extension on first run): ```bash export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY, GEMINI_API_KEY, GROQ_API_KEY uv run main.py "send an email to alice@example.com saying ship it" ``` Swapping providers is an env var, not an import: `RATEL_EXAMPLE_MODEL=anthropic:claude-sonnet-4-6`. With no supported API key set, the example prints the BM25 top-3 in diagnostic mode and never calls a model. ## The same pattern over an MCP server [`examples/mcp-chat`](https://github.com/ratel-ai/ratel/blob/main/examples/mcp-chat/README.md) applies the identical wiring to tools it does not own: it ingests an upstream MCP server's tools into the catalog (server-namespaced ids like `ev__echo`), then rebuilds the tool list every turn. With 13 upstream tools registered and a top-K of 3, the model sees five tool definitions per turn; the rest stay reachable through `search_capabilities` / `invoke_tool` without occupying context. In Python, ingestion is [`register_mcp_server`](/docs/core/python/register-mcps). ## Any other framework LangChain, OpenAI Agents, a hand-rolled loop: the recipe does not change, because `ToolCatalog` entries and the capability-tool factories all return the same plain executable shape (`id`, `name`, `description`, input/output schemas, `execute`). Write one adapter from that shape to your framework's tool type, and one assembler that puts the capability tools plus the top-K hits into each turn. [Progressive disclosure](/docs/core/progressive-disclosure) covers the contract those tools expose to the model. ## Next steps [Register tools](/docs/core/python/register-tools) Build the ToolCatalog these examples pre-filter: local functions and upstream MCP tools. [Telemetry & OTel](/docs/core/python/telemetry) See the invoke and gateway events these integrations emit, locally and over OpenTelemetry. --- # Telemetry & OTel Source: https://docs.ratel.sh/docs/core/python/telemetry Ratel emits telemetry on two deliberately separate streams ([ADR 0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md)): **local trace events** recorded into a sink you configure, and **OpenTelemetry spans** exported as stock OTLP. Both are off by default. This page is the Python wiring. The full event-type vocabulary and span table live on [Telemetry](/docs/core/telemetry). ## Local trace sinks Pass `trace` to the `ToolCatalog` (or `SkillCatalog`) constructor and the core records every search, invoke, capability-tool, upstream-MCP, and auth event into the sink: ```python from ratel_ai import ToolCatalog, TraceSinkConfig catalog = ToolCatalog( trace=TraceSinkConfig(kind="jsonl", session_id="session-1", path="/tmp/ratel.jsonl"), ) # Every catalog.invoke, capability-tool call, and register_mcp_server call now # appends one JSON line per event to /tmp/ratel.jsonl. ``` `TraceSinkConfig` is a dataclass: `kind` is `"noop" | "memory" | "jsonl"`; `session_id` is required for memory and jsonl, `path` for jsonl. Misconfiguration raises `ValueError` (for example `"jsonl sink requires path"`). Sink kinds: * `kind="noop"` — drop everything (the default when no `trace` is passed). * `kind="memory"` — keep events in memory; drain via `catalog.drain_trace_events()`, which returns the buffered events and removes them. Useful in tests. * `kind="jsonl"` — append one JSON line per event to `path` (mode `0600` on Unix). Best-effort, lossy on backpressure. Inject your own events with `catalog.record_event(event)`: the dict must deserialize to a core trace event, else the native layer raises `ValueError` (`"invalid trace event: ..."`). `search_capabilities_tool` records its own `gateway_search` event and tags the underlying catalog search's `search` event with `origin="agent"`; direct callers (`catalog.search(query, k)`) default to `"direct"`. Override per call via `catalog.search(query, k, "agent")`. ## OpenTelemetry export Independently of the local sink, the SDK emits OpenTelemetry spans for the same funnel — `execute_tool`, `ratel.search`, `ratel.skill.load`, `ratel.upstream.register`, `ratel.auth.flow` (the `gen_ai.*` / `ratel.*` vocabulary, catalogued in [Telemetry](/docs/core/telemetry)). The base `ratel-ai` install has zero dependencies. If `opentelemetry` or `ratel_ai_telemetry` is missing, every telemetry helper is a straight pass-through — zero overhead, no spans. The `[otlp]` extra brings both; with them installed, spans go to whatever provider is registered. ### Greenfield: ship spans to Ratel Cloud No OpenTelemetry in your stack yet? One call at startup registers a global tracer provider with a batch OTLP exporter pointed at Ratel Cloud. The exporter ships behind the `[otlp]` extra (without it, `configure_telemetry` raises `ModuleNotFoundError` with this install hint): ```bash pip install 'ratel-ai[otlp]' ``` ```python import os from ratel_ai import configure_telemetry handle = configure_telemetry( endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"), api_key=os.environ["RATEL_API_KEY"], ) # ... run your agent: every ratel.* / execute_tool span now exports ... handle.shutdown() # flush the exporter on exit (handle.force_flush() mid-run) ``` The full keyword surface is `configure_telemetry(*, api_key=None, endpoint=None, headers=None, service_name=None, capture_content=None, include_span_and_events=None)` — the last two set the [content-capture](#content-capture) gate in code. It returns a per-call shutdown handle (`handle.shutdown()` / `handle.force_flush()`) over the shared Ratel-owned provider; shutting any handle down stops export for every caller. Resolution rules: * An explicit `endpoint=` beats the `RATEL_URL` env var. Either way it is the **full OTLP traces URL including `/v1/traces`**; with neither set it raises `ValueError`. * `api_key` is sent as `Authorization: Bearer ` on top of any `headers`. When omitted it falls back to the `RATEL_API_KEY` env var (unless you already set an `Authorization` header explicitly). * `service_name` defaults to `"ratel"`. > `configure_telemetry` owns the global provider. Calling it again once Ratel's provider is registered is idempotent — you get a handle to the same shared provider. It raises `RuntimeError` in two cases: a foreign tracer provider is already registered (you are on the wrong path — dual-export instead), or telemetry was already shut down in this process (shutdown is terminal). ### Already on OpenTelemetry: dual-export If Langfuse or your own collector already owns the provider, the SDK's spans are already flowing to it. Skip `configure_telemetry` and add the span processor from [`ratel-ai-telemetry`](/docs/core/packages/python/ratel-ai-telemetry) to send a copy of the Ratel cut to Cloud: ```python import os from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from ratel_ai_telemetry.otlp import ratel_span_processor provider = trace.get_tracer_provider() # the provider your existing setup registered if not isinstance(provider, TracerProvider): raise RuntimeError("register your OpenTelemetry SDK provider before adding processors") provider.add_span_processor( ratel_span_processor( endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"), api_key=os.environ["RATEL_API_KEY"], ) ) ``` `ratel_span_processor(*, api_key=None, endpoint=None, headers=None, span_filter=None, enabled=True)` (pass `enabled=False` for a no-op processor); `ratel_span_exporter(...)` is the bare OTLP exporter if you wire your own processor. The default filter forwards a span if its name starts with `ratel.` or any attribute key starts with `gen_ai.` or `ratel.`; pass `span_filter=lambda _s: True` to forward everything. ### Content capture Message and tool content never leaves the process by default. The gated payloads (`ratel.search.query`, `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`) ride span attributes only when the capture gate selects a span mode (`SPAN_ONLY` or `SPAN_AND_EVENT`); `EVENT_ONLY` does not put content on spans. Set the gate in code: * Greenfield path: `configure_telemetry(capture_content=...)`. * Dual-export path: `set_content_capture` from `ratel_ai_telemetry`. Code wins over the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` env var, the fallback when nothing is set in code. The full mode table is on [Telemetry](/docs/core/telemetry). ## Next steps [Telemetry](/docs/core/telemetry) The full event-type vocabulary and OpenTelemetry span table, language-neutral. [ratel-ai-telemetry](/docs/core/packages/python/ratel-ai-telemetry) The raw API reference and changelog for the telemetry package. [Packages](/docs/core/python/packages) The Python packages and where their generated references live. --- # Packages Source: https://docs.ratel.sh/docs/core/python/packages Two packages make up the Python surface: | Package | What it is | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`ratel-ai`](/docs/core/packages/python/ratel-ai) | The SDK (`import ratel_ai`): `ToolCatalog`, `SkillCatalog`, the capability tools, and MCP ingestion, over a PyO3 binding to the Rust core. | | [`ratel-ai-telemetry`](/docs/core/packages/python/ratel-ai-telemetry) | The `gen_ai.*` / `ratel.*` span vocabulary plus the OTLP export helpers (`ratel_span_processor`, `ratel_span_exporter`); pulled in by the `[otlp]` extra. | Each package page carries the raw generated API reference and its changelog. For the cross-language package map, see [Packages](/docs/core/packages). --- # Package list Source: https://docs.ratel.sh/docs/core/packages Ratel Core ships as **seven packages across three languages** — two SDKs, a Rust core, and a telemetry stack. Ratel Local ships separately as `@ratel-ai/mcp-server`. The Core package pages below expose every exported symbol with its signature and doc comment, generated straight from [ratel-ai/ratel](https://github.com/ratel-ai/ratel), plus each package's changelog, verbatim. For the guided, cross-language SDK surface, start at [SDKs](/docs/core/sdks) instead. | Package | Language | What it is | | --------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------- | | [`ratel-ai-core`](/docs/core/packages/rust/ratel-ai-core) | Rust | The Rust core both SDKs bundle — registries, retrieval, trace stream | | [`ratel-ai-telemetry` (crate)](/docs/core/packages/rust/ratel-ai-telemetry) | Rust | The Rust vocabulary for the `ratel.*` semantic conventions | | [`@ratel-ai/sdk`](/docs/core/packages/typescript/sdk) | TypeScript | The TypeScript SDK — catalogs, capability tools, MCP ingestion | | [`@ratel-ai/telemetry`](/docs/core/packages/typescript/telemetry) | TypeScript | OTel-free `ratel.*` semantic-convention vocabulary | | [`@ratel-ai/telemetry-otlp`](/docs/core/packages/typescript/telemetry-otlp) | TypeScript | One-call OTLP exporter wiring (`init()`, `ratelSpanProcessor`) | | [`@ratel-ai/mcp-server`](/docs/core/packages/typescript/mcp-server) | TypeScript | The package that powers Ratel Local — MCP serving, CLI configuration, and library embedding | | [`ratel-ai`](/docs/core/packages/python/ratel-ai) | Python | The Python SDK — same surface as TypeScript, PyO3-bundled core | | [`ratel-ai-telemetry`](/docs/core/packages/python/ratel-ai-telemetry) | Python | The `ratel.*` overlay on OpenTelemetry `gen_ai` spans | Every generated Core package page carries its source commit. The generator lives in the docs repo (`apps/docs/scripts/sync-api-reference.mjs`) and pulls from doc comments — rustdoc, TSDoc, and Python docstrings — which are lint-enforced at the source, so these pages track the shipped API rather than a hand-maintained copy. The Ratel Local package reference points to its separately maintained source and product documentation. --- # ratel-ai-core Source: https://docs.ratel.sh/docs/core/packages/rust/ratel-ai-core > v0.4.0 · Rust · [crates.io](https://crates.io/crates/ratel-ai-core) · [source](https://github.com/ratel-ai/ratel/tree/main/src/core) · [docs.rs](https://docs.rs/ratel-ai-core) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. Tool and skill retrieval for AI agents — the Rust core of the Ratel context engineering platform. Agents degrade when every tool definition is stuffed into the context window. This crate keeps the full catalog *outside* the context and retrieves only the entries relevant to the task at hand: register tools and skills once, then search them per turn. Everything runs in-process — no server, no infrastructure. # Mental model Two registries hold the corpus, one per capability kind: * `ToolRegistry` indexes `Tool`s — callable endpoints described by a name, a description, and JSON schemas. * `SkillRegistry` indexes `Skill`s — reusable instruction playbooks whose body is dispatched on demand. Both rank a query with one of three engines, selected by `SearchMethod`: * `SearchMethod::Bm25` (default) — lexical BM25. Needs no model and never fails; `ToolRegistry::search` and `SkillRegistry::search` use it unconditionally. * `SearchMethod::Semantic` — cosine similarity over dense embeddings from a local `bge-small-en-v1.5` model, downloaded into the HuggingFace cache on first use (ADR-0011). * `SearchMethod::Hybrid` — the BM25 and dense rankings fused with Reciprocal Rank Fusion. Semantic and hybrid searches rank against an embedding cache built by `ToolRegistry::build_embeddings` / `SkillRegistry::build_embeddings`; a search itself never embeds the corpus and never downloads the model. Every register and search also emits a `TraceEvent` on the registry's `TraceSink` — the local trace stream behind the inspector and usage reporting (ADR-0007). The default sink is `NoopSink` (discard); `MemorySink` buffers for tests and introspection, `JsonlSink` appends to a local file. # Example: register and search (BM25) ```rust use ratel_ai_core::{Tool, ToolRegistry}; let mut registry = ToolRegistry::new(); registry.register(Tool { id: "read_file".into(), name: "read_file".into(), description: "Read a file from disk".into(), input_schema: serde_json::json!({ "properties": { "path": { "type": "string", "description": "absolute path" } } }), output_schema: serde_json::json!({}), }); registry.register(Tool { id: "send_email".into(), name: "send_email".into(), description: "Send an email to a recipient".into(), input_schema: serde_json::json!({}), output_schema: serde_json::json!({}), }); let hits = registry.search("read a file", 5); assert_eq!(hits[0].tool_id, "read_file"); ``` The language SDKs (`@ratel-ai/sdk` on npm, `ratel-ai` on PyPI) bundle this crate and surface the same model; the agent-facing capability tools (`search_capabilities` / `invoke_tool` / `get_skill_content`) sit on top of them. Design rationale lives in the repo's `docs/adr/`. ## Full item-level reference rustdoc renders every public item of this crate — with the same doc comments — on [docs.rs](https://docs.rs/ratel-ai-core). --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/rust/ratel-ai-core/changelog > ratel-ai-core release history · [crates.io](https://crates.io/crates/ratel-ai-core) · [source](https://github.com/ratel-ai/ratel/tree/main/src/core) · [docs.rs](https://docs.rs/ratel-ai-core) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `ratel-ai-core` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this crate adheres to [Semantic Versioning](https://semver.org/). ## \[0.4.0] - 2026-07-09 ### Fixed * Re-registering a tool or skill (MCP re-sync, hot-reload) left a stale duplicate in the corpus instead of replacing it in place, causing BM25 score drift and an unbounded memory leak. `ToolRegistry`/`SkillRegistry` are now id-keyed so `register` replaces in place, and the dense embedding cache invalidates on replace so `build_embeddings` re-embeds the changed id. ## \[0.3.0] - 2026-07-06 ### Added * **Selectable retrieval methods** (ADR-0011): a `SearchMethod` enum — `Bm25` (default), `Semantic`, `Hybrid` — chosen per registry or per call via `ToolRegistry::search_with_method` / `SkillRegistry::search_with_method`. Semantic ranks a local `BAAI/bge-small-en-v1.5` embedding (pure-Rust Candle); hybrid fuses the BM25 and dense arms with Reciprocal Rank Fusion (no reranker). * `EmbedderError` (surfaced from `search_with_method` on the semantic/hybrid path) and a `TraceEvent::EmbedderLoad` / `EmbedderLoadStatus` flagging a slow (possibly underpowered machine) or failed model load. * `ToolRegistry::build_embeddings` / `SkillRegistry::build_embeddings` — pre-compute embeddings for not-yet-embedded tools/skills so a later semantic/hybrid search only embeds the query. ### Changed * BM25 remains the default engine. `search` / `search_with_origin` keep their infallible `Vec` signature and BM25 behavior unchanged. * The dense embedding cache is now **incremental** — a growing prefix of the corpus. `register` only appends (never invalidates), and `build_embeddings` embeds only newly-registered tools, so an existing vector is never recomputed (adding one tool costs one embedding, not N). A BM25-only registry still never loads the model. * A semantic/hybrid search over an un-built corpus now returns `EmbedderError::EmbeddingsNotBuilt` instead of embedding inside the search path — a search never silently pays the corpus-embedding cost. Populate the cache with `build_embeddings()` first. ## \[0.2.1-rc.1] - 2026-07-04 ### Changed * First release cut under the per-package release scheme (ADR-0008): `ratel-ai-core` now versions and ships independently, tagged `core-v*`. No crate API changes since 0.2.0. ## \[0.2.0] - 2026-06-16 ### Added * First-class **skills**: a `Skill { id, name, description, tags, tools, metadata, body }` type and a separate `SkillRegistry` BM25 index — ranked independently of tools. Only `name`/`description`/`tags` are indexed; `tools` (a declared dependency edge surfaced at the gateway), `metadata` (non-indexed context such as `stacks`), and `body` are not. Plus `skill_search` / `skill_churn` / `skill_invoke` trace events for the retrieval funnel. ## \[0.1.6] - 2026-06-10 ### Changed * Version bump for the coordinated v0.1.6 release (first release shipping the `ratel-ai` Python SDK). No crate source changes since 0.1.5; re-published in lockstep to keep all artifacts version-aligned. ## \[0.1.5] - 2026-05-10 ### Added * Initial release on the v1 (revamp) line. BM25 tool retrieval, MCP ingestion, framework-neutral catalog. See the [crate README](https://github.com/ratel-ai/ratel/blob/main/src/core/README.md) for the full surface. * `trace` module: `TraceEvent` tagged enum, `TraceEnvelope`, `TraceSink` trait with `NoopSink`, `MemorySink`, and `JsonlSink` (synchronous `O_APPEND`, mode `0600` on Unix) — single tagged event stream per [ADR-0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md). `ToolRegistry::with_trace_sink` / `set_trace_sink` / `record_event` plus a `search_with_origin` method. `register` emits `index_churn{Add}`; `search` emits `search` with a `bm25` stage. The origin enum tags each search as `direct` (Rust callers, pre-fetch helpers, benchmarks) or `agent` (LLM-synthesized via the gateway), to let downstream consumers separate the two paths. --- # ratel-ai-telemetry (Rust) Source: https://docs.ratel.sh/docs/core/packages/rust/ratel-ai-telemetry > v0.1.0 · Rust · [crates.io](https://crates.io/crates/ratel-ai-telemetry) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/core) · [docs.rs](https://docs.rs/ratel-ai-telemetry) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. `ratel-ai-telemetry` — the `ratel.*` telemetry vocabulary. See [README.md](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/core/README.md) and the wire contract in [CONVENTIONS.md](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/CONVENTIONS.md) for design. ## Full item-level reference rustdoc renders every public item of this crate — with the same doc comments — on [docs.rs](https://docs.rs/ratel-ai-telemetry). --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/rust/ratel-ai-telemetry/changelog > ratel-ai-telemetry release history · [crates.io](https://crates.io/crates/ratel-ai-telemetry) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/core) · [docs.rs](https://docs.rs/ratel-ai-telemetry) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `ratel-ai-telemetry` (the Rust telemetry constants crate) are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.1.0] - 2026-07-06 ### Added * The telemetry vocabulary (ADR-0015): the full `ratel.*` constants (attribute keys, span/event names, `gen_ai.*` interop keys, and the `Origin`/`SearchTarget`/`AuthOutcome` value enums) pinned to OpenTelemetry semconv `gen_ai` v1.42.0, as zero-dependency `&str` constants and enums. * The `contract-against-the-pin` test suite: every constant is asserted against its pinned wire key, so the vocabulary cannot drift from `CONVENTIONS.md`. ### Changed * Released as an independent crates.io unit under the `telemetry-core-v*` tag prefix. This crate is constants-only; the TS and Python helpers ship `init()`. --- # @ratel-ai/sdk Source: https://docs.ratel.sh/docs/core/packages/typescript/sdk > v0.4.1 · TypeScript · [npm](https://www.npmjs.com/package/@ratel-ai/sdk) · [source](https://github.com/ratel-ai/ratel/tree/main/src/sdk/ts) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. `@ratel-ai/sdk` — TypeScript SDK for Ratel, the context engineering platform for AI agents. In-process, no infra: a native (Rust) BM25/semantic/hybrid index behind [ToolCatalog](#api-toolcatalog) and [SkillCatalog](#api-skillcatalog), MCP ingestion via [registerMcpServer](#api-registermcpserver), and the framework-neutral capability tools ([searchCapabilitiesTool](#api-searchcapabilitiestool), [invokeToolTool](#api-invoketooltool), [getSkillContentTool](#api-getskillcontenttool)) that let a model discover and run what the catalogs hold. Everything emits OTel `ratel.*`/`gen_ai.*` spans plus a local trace stream (ADR-0007). ## Classes ### SkillCatalog In-memory catalog of skills, ranked by the native BM25 `SkillRegistry`. The on-demand analog of [ToolCatalog](#api-toolcatalog): registered skills are searched by relevance; the matching body is fetched only on [SkillCatalog.invoke](#api-invoke). #### Constructors ##### Constructor > **new SkillCatalog**(`options?`): [`SkillCatalog`](#api-skillcatalog) Create an empty catalog. ###### Parameters | Parameter | Type | Description | | --------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `options` | [`SkillCatalogOptions`](#api-skillcatalogoptions) | Trace sink and default retrieval method. A `"semantic"`/ `"hybrid"` default makes every subsequent `register` embed the new skill immediately (loading the embedding model on first use); the `"bm25"` default stays model-free. | ###### Returns [`SkillCatalog`](#api-skillcatalog) #### Methods ##### buildEmbeddings() > **buildEmbeddings**(): `void` Pre-compute embeddings for not-yet-embedded skills. See `ToolCatalog.buildEmbeddings`. ###### Returns `void` ##### drainTraceEvents() > **drainTraceEvents**(): `unknown`\[] Drain the envelopes captured by a `"memory"` trace sink, emptying its buffer. Same contract as `ToolCatalog.drainTraceEvents`. ###### Returns `unknown`\[] The captured envelopes in record order; `[]` unless the active sink is `"memory"`. ##### get() > **get**(`skillId`): [`Skill`](#api-skill) | `undefined` Look up a skill by id. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------ | | `skillId` | `string` | The id to look up. | ###### Returns [`Skill`](#api-skill) | `undefined` The skill as registered (including `body`), or `undefined` for an unknown id. ##### has() > **has**(`skillId`): `boolean` Whether a skill with this id is registered. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------ | | `skillId` | `string` | The id to look up. | ###### Returns `boolean` `true` if [SkillCatalog.invoke](#api-invoke) would find it. ##### invoke() > **invoke**(`skillId`): `string` Return a skill's body for dispatch, recording a `skill_invoke` event. Throws on an unknown id — callers at the capability-tool boundary translate that into a structured error for the agent. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------------- | | `skillId` | `string` | Id of a registered skill. | ###### Returns `string` The skill's `body` (`""` when it was registered without one). ##### recordEvent() > **recordEvent**(`event`): `void` Record a custom event on the local trace stream (ADR-0007). Same contract as `ToolCatalog.recordEvent`: the event is a tagged wire-shape object (`{ type: "...", ... }`, snake\_case fields) and an unknown shape throws. ###### Parameters | Parameter | Type | Description | | --------- | -------- | -------------------------- | | `event` | `object` | The trace event to record. | ###### Returns `void` ##### register() > **register**(`skill`): `void` Add a skill to the catalog, or replace it in place when the id is already registered. Name, description, and tags are indexed for ranking; the `body` is not (it is the dispatch payload, fetched by [SkillCatalog.invoke](#api-invoke)). On a semantic/hybrid catalog this also embeds the new skill immediately, and throws if the embedding model fails to load. ###### Parameters | Parameter | Type | Description | | --------- | --------------------- | ---------------------------------------------- | | `skill` | [`Skill`](#api-skill) | The skill to register; `id` is its lookup key. | ###### Returns `void` ##### search() > **search**(`query`, `topK`, `origin?`, `method?`): [`SkillHit`](#api-skillhit)\[] Search the catalog — the skill counterpart of `ToolCatalog.search`, with the same method semantics (a `"semantic"`/`"hybrid"` call throws `EmbeddingsNotBuilt` if the embedding cache isn't built). ###### Parameters | Parameter | Type | Default value | Description | | --------- | ----------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------- | | `query` | `string` | `undefined` | Natural-language description of the task at hand. | | `topK` | `number` | `undefined` | Maximum number of hits to return. | | `origin` | [`SearchOrigin`](#api-searchorigin) | `"direct"` | Who initiated the call (default `"direct"`); recorded on the trace event and span, never affects ranking. | | `method?` | [`SearchMethod`](#api-searchmethod) | `undefined` | Per-call override of the catalog's default retrieval method. | ###### Returns [`SkillHit`](#api-skillhit)\[] Up to `topK` hits, best-first with ties broken by skill id; the `score` scale depends on the method (BM25 / cosine / RRF). ##### size() > **size**(): `number` Number of registered skills (distinct ids). `searchCapabilitiesTool` uses this to decide whether to advertise a `skills` bucket at all. ###### Returns `number` The count. *** ### SkillRegistry Node binding over the `ratel-ai-core` skill registry — the skill twin of `ToolRegistry`, ranking registered skills against a natural-language query. Skill bodies are stored but never indexed; fetch them a layer up (the SDK's `SkillCatalog.invoke`). #### Constructors ##### Constructor > **new SkillRegistry**(): [`SkillRegistry`](#api-skillregistry) Create an empty registry with a no-op trace sink. ###### Returns [`SkillRegistry`](#api-skillregistry) #### Methods ##### buildEmbeddings() > **buildEmbeddings**(): `void` See `ToolRegistry.build_embeddings`. ###### Returns `void` ##### drainTraceEvents() > **drainTraceEvents**(): `any`\[] Drain captured envelopes from the active sink. Returns `[]` unless the active sink is "memory". ###### Returns `any`\[] ##### recordEvent() > **recordEvent**(`event`): `void` Record a custom event on the local trace stream — see `ToolRegistry.recordEvent`. Throws on an object that doesn't parse as a known trace event. ###### Parameters | Parameter | Type | | --------- | ----- | | `event` | `any` | ###### Returns `void` ##### register() > **register**(`skill`): `void` Index a skill, or replace one in place if its id is already registered. Omitted optional fields default to empty (`tags`/`tools`/`metadata`) and `""` (`body`). See `ToolRegistry.register`. ###### Parameters | Parameter | Type | | --------- | --------------------- | | `skill` | [`Skill`](#api-skill) | ###### Returns `void` ##### search() > **search**(`query`, `topK`): [`SkillHit`](#api-skillhit)\[] Lexical BM25 search over skills — see `ToolRegistry.search` for the contract (best-first, ties by id, infallible, traced as `"direct"`). ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | ###### Returns [`SkillHit`](#api-skillhit)\[] ##### searchWithMethod() > **searchWithMethod**(`query`, `topK`, `origin`, `method`): [`SkillHit`](#api-skillhit)\[] Search with an explicit method — see \[`ToolRegistry::search_with_method`]. ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | | `origin` | `string` | | `method` | `string` | ###### Returns [`SkillHit`](#api-skillhit)\[] ##### searchWithOrigin() > **searchWithOrigin**(`query`, `topK`, `origin`): [`SkillHit`](#api-skillhit)\[] BM25 search with an explicit origin — see `ToolRegistry.searchWithOrigin`. ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | | `origin` | `string` | ###### Returns [`SkillHit`](#api-skillhit)\[] ##### setTraceSink() > **setTraceSink**(`config`): `void` Replace the trace sink — see `ToolRegistry.setTraceSink`. ###### Parameters | Parameter | Type | | --------- | ----------------- | | `config` | `TraceSinkConfig` | ###### Returns `void` *** ### ToolCatalog In-process catalog of executable tools, ranked by the native Rust registry. The SDK's central surface: [ToolCatalog.register](#api-register-2) tools (or ingest an MCP server's via `registerMcpServer`), [ToolCatalog.search](#api-search-2) them by relevance, and [ToolCatalog.invoke](#api-invoke-1) the chosen one. Every operation emits both an OTel span (to whatever provider is active — see telemetry.ts) and a local trace event (to the sink from [ToolCatalogOptions.trace](#api-trace-1)), per ADR-0007. #### Example ```ts import { ToolCatalog } from "@ratel-ai/sdk"; import { readFile } from "node:fs/promises"; const catalog = new ToolCatalog(); catalog.register({ id: "read_file", name: "read_file", description: "Read a file from local disk and return its textual contents.", inputSchema: { type: "object", properties: { path: { type: "string", description: "absolute path to the file" } }, required: ["path"], }, outputSchema: { type: "object" }, execute: async ({ path }) => ({ contents: await readFile(path, "utf8") }), }); const [hit] = catalog.search("read a file from disk", 5); const result = await catalog.invoke(hit.toolId, { path: "/tmp/notes.txt" }); ``` #### Constructors ##### Constructor > **new ToolCatalog**(`options?`): [`ToolCatalog`](#api-toolcatalog) Create an empty catalog. ###### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `options` | [`ToolCatalogOptions`](#api-toolcatalogoptions) | Trace sink and default retrieval method. A `"semantic"`/ `"hybrid"` default makes every subsequent `register` embed the new tool immediately (loading the embedding model on first use); the `"bm25"` default stays model-free. | ###### Returns [`ToolCatalog`](#api-toolcatalog) #### Methods ##### buildEmbeddings() > **buildEmbeddings**(): `void` Pre-compute embeddings for any not-yet-embedded tools. Call after a bulk register, or rely on the automatic per-register embedding a semantic/hybrid catalog does. No-op for a BM25 catalog's cache. Incremental: only tools registered since the last call are embedded. Throws if the embedding model fails to load. ###### Returns `void` ##### drainTraceEvents() > **drainTraceEvents**(): `unknown`\[] Drain the envelopes captured by a `"memory"` trace sink, emptying its buffer. ###### Returns `unknown`\[] The captured envelopes (`{ v, ts, session_id, type, ... }` — the event fields are flattened alongside the envelope stamp) in record order. Always `[]` unless the active sink is `"memory"`. ##### get() > **get**(`toolId`): [`Tool`](#api-tool) | `undefined` Look up a tool's searchable metadata (no executor attached). ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------ | | `toolId` | `string` | The id to look up. | ###### Returns [`Tool`](#api-tool) | `undefined` The metadata as registered, or `undefined` for an unknown id. ##### getExecutable() > **getExecutable**(`toolId`): [`ExecutableTool`](#api-executabletool) | `undefined` Look up a tool with its executor reattached. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------ | | `toolId` | `string` | The id to look up. | ###### Returns [`ExecutableTool`](#api-executabletool) | `undefined` A copy of the registered tool including `execute`, or `undefined` for an unknown id. ##### has() > **has**(`toolId`): `boolean` Whether a tool with this id is registered. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------ | | `toolId` | `string` | The id to look up. | ###### Returns `boolean` `true` if [ToolCatalog.invoke](#api-invoke-1) would find an executor for it. ##### invoke() > **invoke**(`toolId`, `args`): `Promise`\<`unknown`> Run a registered tool's executor. Sync-absorbing: the executor may return a plain value or a `Promise` (both are awaited), and a synchronous `throw` inside it surfaces as a rejection of the returned promise — `invoke` never throws synchronously, including for an unknown `toolId` (that rejects with `unknown toolId: …`). The call is wrapped in an `execute_tool` OTel span and bracketed by `invoke_start` / `invoke_end` (or `invoke_error`, with the error message) events on the local trace stream, `took_ms` in wall-clock milliseconds. ###### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ---------------------------------------------------------- | | `toolId` | `string` | Id of a registered tool. | | `args` | `Record`\<`string`, `unknown`> | Arguments object passed through to the executor unchanged. | ###### Returns `Promise`\<`unknown`> Whatever the executor returns (resolved if it returned a promise). ##### recordEvent() > **recordEvent**(`event`): `void` Record a custom event on the local trace stream (ADR-0007), e.g. an `upstream_register` from an ingestion layer. Delivered to the sink configured at construction; a no-op sink discards it. ###### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `event` | `object` | A tagged trace event in wire shape: `{ type: "...", ... }` with snake\_case fields. Throws if the object is not a known trace event. | ###### Returns `void` ##### register() > **register**(`tool`): `void` Add a tool to the catalog, or replace it in place when the id is already registered (metadata, executor, and index entry — the corpus never holds a duplicate). On a semantic/hybrid catalog this also embeds the new tool immediately, and throws if the embedding model fails to load. ###### Parameters | Parameter | Type | Description | | --------- | --------------------------------------- | ----------------------------------------------------------- | | `tool` | [`ExecutableTool`](#api-executabletool) | The tool's searchable metadata plus its `execute` function. | ###### Returns `void` ##### search() > **search**(`query`, `topK`, `origin?`, `method?`): [`SearchHit`](#api-searchhit)\[] Search the catalog. `method` overrides the catalog default for this call. `"semantic"`/`"hybrid"` rank against the prebuilt embedding cache and throw `EmbeddingsNotBuilt` if it isn't built; they never load the model in-search (a semantic/hybrid catalog builds embeddings eagerly at register). ###### Parameters | Parameter | Type | Default value | Description | | --------- | ----------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------- | | `query` | `string` | `undefined` | Natural-language description of what the caller wants to do. | | `topK` | `number` | `undefined` | Maximum number of hits to return. | | `origin` | [`SearchOrigin`](#api-searchorigin) | `"direct"` | Who initiated the call (default `"direct"`); recorded on the trace event and span, never affects ranking. | | `method?` | [`SearchMethod`](#api-searchmethod) | `undefined` | Per-call override of the catalog's default retrieval method. | ###### Returns [`SearchHit`](#api-searchhit)\[] Up to `topK` hits, best-first with ties broken by tool id. The `score` scale depends on the method: a raw BM25 relevance score, a cosine similarity, or an RRF fusion score — comparable within one result list, not across methods. *** ### ToolRegistry Node binding over the `ratel-ai-core` tool registry: an in-process index that ranks registered tools against a natural-language query (BM25 by default; semantic/hybrid once embeddings are built). Metadata-only — the SDK's `ToolCatalog` layers executors, OTel spans, and defaults on top. #### Constructors ##### Constructor > **new ToolRegistry**(): [`ToolRegistry`](#api-toolregistry) Create an empty registry with a no-op trace sink. ###### Returns [`ToolRegistry`](#api-toolregistry) #### Methods ##### buildEmbeddings() > **buildEmbeddings**(): `void` Pre-compute embeddings for not-yet-embedded tools (incremental) so a later semantic/hybrid search only embeds the query. Throws if the model fails to load. The catalog calls this after `register` in semantic mode. ###### Returns `void` ##### drainTraceEvents() > **drainTraceEvents**(): `any`\[] Drain captured envelopes from the active sink. Returns `[]` unless the active sink is "memory". ###### Returns `any`\[] ##### recordEvent() > **recordEvent**(`event`): `void` Record a custom event on the local trace stream (ADR-0007). `event` is the tagged wire shape — `{ type: "...", ... }` with snake\_case fields — and an object that doesn't parse as a known event throws (`invalid trace event`). Higher layers use this to put their invoke/upstream/auth lifecycle events on the same stream as the registry's own search events. ###### Parameters | Parameter | Type | | --------- | ----- | | `event` | `any` | ###### Returns `void` ##### register() > **register**(`tool`): `void` Index a tool, or replace one in place if its id is already registered (the corpus never holds a duplicate). Infallible and model-free; a semantic caller embeds afterwards via `buildEmbeddings`. ###### Parameters | Parameter | Type | | --------- | ------------------- | | `tool` | [`Tool`](#api-tool) | ###### Returns `void` ##### search() > **search**(`query`, `topK`): [`SearchHit`](#api-searchhit)\[] Lexical BM25 search: up to `topK` hits, best-first with ties broken by id. Model-free and infallible; an empty registry returns `[]`. Records the query on the local trace stream with origin `"direct"`. ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | ###### Returns [`SearchHit`](#api-searchhit)\[] ##### searchWithMethod() > **searchWithMethod**(`query`, `topK`, `origin`, `method`): [`SearchHit`](#api-searchhit)\[] Search with an explicit method (`"bm25"` | `"semantic"` | `"hybrid"`). `bm25` is infallible; `semantic`/`hybrid` rank against the prebuilt embedding cache and throw (`EmbeddingsNotBuilt`) if it isn't built — the model loads at `build_embeddings`, never inside a search. An unknown method string throws too. ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | | `origin` | `string` | | `method` | `string` | ###### Returns [`SearchHit`](#api-searchhit)\[] ##### searchWithOrigin() > **searchWithOrigin**(`query`, `topK`, `origin`): [`SearchHit`](#api-searchhit)\[] BM25 search with an explicit origin — `"agent"` for a call the model synthesized (capability tools), anything else counts as `"direct"` (host code). Origin only annotates the trace event; ranking is identical to `search`. ###### Parameters | Parameter | Type | | --------- | -------- | | `query` | `string` | | `topK` | `number` | | `origin` | `string` | ###### Returns [`SearchHit`](#api-searchhit)\[] ##### setTraceSink() > **setTraceSink**(`config`): `void` Replace the trace sink; subsequent events go to the new destination, already-recorded ones are not replayed. Throws on an unknown `kind`, a missing `sessionId`/`path`, or a `"jsonl"` file that can't be opened. ###### Parameters | Parameter | Type | | --------- | ----------------- | | `config` | `TraceSinkConfig` | ###### Returns `void` ## Interfaces ### CapabilitySkillHit One ranked skill in the `skills` bucket of [SearchCapabilitiesResult](#api-searchcapabilitiesresult). #### Properties ##### description > **description**: `string` The skill's description, compacted to \~160 chars for the listing. ##### score > **score**: `number` Retrieval score from the skill catalog's search (BM25 by default). ##### skillId > **skillId**: `string` Skill id to pass to `get_skill_content`. *** ### CapabilityToolGroup Tool hits grouped by upstream server. The group key is the `__` id prefix (a plain, un-prefixed tool id groups under itself); `description` and `instructions` are attached when a matching [UpstreamServerInfo](#api-upstreamserverinfo) was provided. #### Properties ##### hits > **hits**: [`CapabilityToolHit`](#api-capabilitytoolhit)\[] The server's ranked hits, in overall result order. ##### server > **server**: `object` The owning server: its name, plus optional description/instructions metadata. ###### description? > `optional` **description?**: `string` The server's one-line summary, when known. ###### instructions? > `optional` **instructions?**: `string` The server's usage instructions, when known. ###### name > **name**: `string` Server name (the `__` prefix of the group's tool ids). *** ### CapabilityToolHit One ranked tool in the `tools` bucket of [SearchCapabilitiesResult](#api-searchcapabilitiesresult). #### Properties ##### description > **description**: `string` The tool's description, as registered. ##### inputSchema > **inputSchema**: `Record`\<`string`, `unknown`> The tool's input JSON Schema, so the model can call it without another lookup. ##### score > **score**: `number` Retrieval score from the tool catalog's search (scale depends on the catalog's method — BM25 by default), or `0` when the tool was pulled in as a matched skill's declared dependency rather than by the query itself. ##### toolId > **toolId**: `string` Catalog id to pass to `invoke_tool` (`__` for MCP-proxied tools). *** ### ConfigureTelemetryOptions Options for [configureTelemetry](#api-configuretelemetry): the exporter wiring of [InitOptions](#api-initoptions) plus programmatic control of the message/tool content-capture gate. #### Extends * [`InitOptions`](#api-initoptions) #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ###### Inherited from [`InitOptions`](#api-initoptions).[`apiKey`](#api-apikey-1) ##### captureContent? > `optional` **captureContent?**: [`ContentCapture`](#api-contentcapture) Exact content-capture mode, set programmatically instead of via `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`. Code-level config wins over the env var (OTel treats env vars as the fallback for code-level configuration), and wins over [ConfigureTelemetryOptions.includeSpanAndEvents](#api-includespanandevents). ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ###### Inherited from [`InitOptions`](#api-initoptions).[`endpoint`](#api-endpoint-1) ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ###### Inherited from [`InitOptions`](#api-initoptions).[`headers`](#api-headers-1) ##### includeSpanAndEvents? > `optional` **includeSpanAndEvents?**: `boolean` Convenience switch over [ConfigureTelemetryOptions.captureContent](#api-capturecontent): `true` is full capture (`SPAN_AND_EVENT`), `false` is none (`NO_CONTENT`). Ignored when `captureContent` is set. When neither is provided, the env var keeps ruling. ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/ts/src/config.ts). ###### Inherited from [`InitOptions`](#api-initoptions).[`serviceName`](#api-servicename-1) *** ### ExecutableTool A tool the catalog can both retrieve *and* run: the searchable metadata of a `Tool` (id, name, description, schemas) plus its [Executor](#api-executor). The unit [ToolCatalog.register](#api-register-2) accepts and [ToolCatalog.getExecutable](#api-getexecutable) returns. #### Extends * [`Tool`](#api-tool) #### Properties ##### description > **description**: `string` What the tool does and when to use it — the main ranking signal. ###### Inherited from [`Tool`](#api-tool).[`description`](#api-description-5) ##### execute > **execute**: [`Executor`](#api-executor) Runs the tool. Called by [ToolCatalog.invoke](#api-invoke-1) with the args object. ##### id > **id**: `string` Unique id, the registry key. Re-registering an existing id replaces the entry in place. MCP-proxied tools use the `__` convention. ###### Inherited from [`Tool`](#api-tool).[`id`](#api-id-2) ##### inputSchema > **inputSchema**: `JSONSchema7` JSON Schema of the arguments. Property names and their `description`s (nested included) are indexed for ranking. ###### Inherited from [`Tool`](#api-tool).[`inputSchema`](#api-inputschema-3) ##### name > **name**: `string` Callable name (typically the same as `id` for local tools); indexed for ranking both whole and split on `snake_case`/`camelCase` boundaries. ###### Inherited from [`Tool`](#api-tool).[`name`](#api-name-3) ##### outputSchema > **outputSchema**: `JSONSchema7` JSON Schema of the result; indexed the same way as `inputSchema`. ###### Inherited from [`Tool`](#api-tool).[`outputSchema`](#api-outputschema-1) *** ### InitOptions `init()` (in `@ratel-ai/telemetry-otlp`) resolves endpoint + auth from `RATEL_URL` / `RATEL_API_KEY`, with explicit `{ endpoint, apiKey }` values taking precedence over the environment. An explicit `apiKey` sets the Bearer header; the `RATEL_API_KEY` fallback only applies when neither `apiKey` nor an explicit `Authorization` header is given, so ambient env never overrides an auth header the caller passed on purpose. #### Extended by * [`ConfigureTelemetryOptions`](#api-configuretelemetryoptions) #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/ts/src/config.ts). *** ### InvokeToolToolOptions Options for [invokeToolTool](#api-invoketooltool). #### Properties ##### onUnauthorized? > `optional` **onUnauthorized?**: (`upstream`) => `void` | `Promise`\<`void`> Notified when the underlying tool throws UnauthorizedError, with the upstream name inferred from the toolId. ###### Parameters | Parameter | Type | | ---------- | -------- | | `upstream` | `string` | ###### Returns `void` | `Promise`\<`void`> *** ### McpServerHandle What [registerMcpServer](#api-registermcpserver) returns: the ingested ids plus lifecycle control. #### Properties ##### close > **close**: () => `Promise`\<`void`> Close the underlying MCP client connection. The proxied tools stay in the catalog but invoking them after close fails. ###### Returns `Promise`\<`void`> ##### serverInstructions > **serverInstructions**: `string` | `undefined` The usage instructions the server declared during the MCP initialize handshake, or `undefined` if it declared none. Useful as `UpstreamServerInfo.instructions` when building capability tools. ##### toolIds > **toolIds**: `string`\[] Namespaced ids (`__`) of every tool registered, in server order. *** ### RegisterMcpServerOptions Options for [registerMcpServer](#api-registermcpserver). #### Properties ##### name > **name**: `string` Namespace for the server's tools inside the catalog: each tool is registered as `__`. Also the `server` name that trace events and result groups report for these tools. ##### transport > **transport**: `Transport` An MCP client transport for the server (e.g. `StdioClientTransport`, `StreamableHTTPClientTransport`, or an `InMemoryTransport` pair in tests). [registerMcpServer](#api-registermcpserver) connects it; it must not be connected already. *** ### SearchCapabilitiesOptions Options for [searchCapabilitiesTool](#api-searchcapabilitiestool). #### Properties ##### upstreamServers? > `optional` **upstreamServers?**: readonly [`UpstreamServerInfo`](#api-upstreamserverinfo)\[] Upstream MCP servers to advertise in the tool description and result groups. *** ### SearchCapabilitiesResult Result shape of the `search_capabilities` tool: two independently-ranked buckets with separate top-K budgets. Scores are comparable within a bucket, not across the two (tools and skills are indexed as different text shapes). #### Properties ##### skills > **skills**: [`CapabilitySkillHit`](#api-capabilityskillhit)\[] Skill hits — playbooks to load via `get_skill_content`. ##### tools > **tools**: `object` Executable tool hits, grouped by upstream server. ###### groups > **groups**: [`CapabilityToolGroup`](#api-capabilitytoolgroup)\[] Groups in ranking order (a server appears where its best hit ranked). *** ### SearchHit One ranked tool from a registry search, best-first. #### Properties ##### score > **score**: `number` Relevance score; higher is better, ties break by id ascending. The scale depends on the method: raw BM25 (unbounded) for `"bm25"`, cosine similarity for `"semantic"`, Reciprocal Rank Fusion for `"hybrid"` — comparable within one result list, not across methods. ##### toolId > **toolId**: `string` Id of the matched tool, as registered. *** ### ~~SearchToolHit~~ #### Deprecated Use `CapabilityToolHit`. #### Properties ##### ~~description~~ > **description**: `string` The tool's description, as registered. ##### ~~inputSchema~~ > **inputSchema**: `Record`\<`string`, `unknown`> The tool's input JSON Schema. ##### ~~score~~ > **score**: `number` Retrieval score from the catalog's search (BM25 by default). ##### ~~toolId~~ > **toolId**: `string` Catalog id to pass to `invoke_tool`. *** ### ~~SearchToolsGroup~~ #### Deprecated Use `CapabilityToolGroup`. #### Properties ##### ~~hits~~ > **hits**: [`SearchToolHit`](#api-searchtoolhit)\[] The server's ranked hits, in overall result order. ##### ~~server~~ > **server**: `object` The owning server: its name, plus optional description/instructions metadata. ###### ~~description?~~ > `optional` **description?**: `string` The server's one-line summary, when known. ###### ~~instructions?~~ > `optional` **instructions?**: `string` The server's usage instructions, when known. ###### ~~name~~ > **name**: `string` Server name (the `__` prefix of the group's tool ids). *** ### ~~SearchToolsResult~~ #### Deprecated Use `SearchCapabilitiesResult` (the `tools.groups` field). #### Properties ##### ~~groups~~ > **groups**: [`SearchToolsGroup`](#api-searchtoolsgroup)\[] Tool hits grouped by upstream server, in ranking order. *** ### ~~SearchToolsToolOptions~~ #### Deprecated Use `SearchCapabilitiesOptions`. #### Properties ##### ~~upstreamServers?~~ > `optional` **upstreamServers?**: readonly [`UpstreamServerInfo`](#api-upstreamserverinfo)\[] Upstream MCP servers to advertise in the tool description and result groups. *** ### Skill A reusable playbook: instructions the agent *reads* and follows, in contrast to a `Tool` it executes. Name, description, and tags are indexed for ranking; the `body` is the dispatch payload, deliberately excluded from the index so it can't drown the description's term weights. #### Properties ##### body? > `optional` **body?**: `string` The full instructions (Markdown) returned on load — the dispatch payload, never indexed for ranking. Optional (defaults to `""`) — parity with the Python SDK's default body. ##### description > **description**: `string` What the skill covers and when to reach for it — the main ranking signal. ##### id > **id**: `string` Unique id, the registry key. Re-registering an existing id replaces the entry in place. ##### metadata? > `optional` **metadata?**: `Record`\<`string`, `string`\[]> Free-form, non-indexed context for higher layers — e.g. `{ stacks: ["react"] }` for the push ranker to boost by project context. ##### name > **name**: `string` Human-readable name; indexed for ranking both whole and split on `snake_case`/`camelCase` boundaries. ##### tags? > `optional` **tags?**: `string`\[] Author-declared labels and task phrases ("frontend", "login form"); indexed for ranking. Optional (defaults to `[]`) — a minimal `Skill(id, name, description)` is valid, in parity with the Python SDK. ##### tools? > `optional` **tools?**: `string`\[] Ids of tools this skill's instructions call; surfaced into the `search_capabilities` tools bucket — not indexed as query terms. *** ### SkillCatalogOptions Construction options for [SkillCatalog](#api-skillcatalog). #### Properties ##### method? > `optional` **method?**: [`SearchMethod`](#api-searchmethod) Default retrieval method for `search` (default `"bm25"`). ##### trace? > `optional` **trace?**: [`TraceSinkConfig`](#api-tracesinkconfig) Local trace stream destination (default: discard). See [TraceSinkConfig](#api-tracesinkconfig). *** ### SkillHit One ranked skill from a registry search, best-first — the skill twin of `SearchHit`, with the same score semantics per method. #### Properties ##### score > **score**: `number` Relevance score; higher is better, ties break by id ascending. Scale depends on the method (BM25 / cosine / RRF), as on `SearchHit.score`. ##### skillId > **skillId**: `string` Id of the matched skill, as registered. *** ### TelemetryHandle Handle returned by [configureTelemetry](#api-configuretelemetry); `shutdown()` flushes the exporter. #### Methods ##### shutdown() > **shutdown**(): `Promise`\<`void`> Flush pending spans and shut the exporter down. Call once at process exit. ###### Returns `Promise`\<`void`> *** ### Tool A tool's searchable metadata: what the registry indexes and what a search hit resolves back to. Execution lives a layer up (the SDK's `ToolCatalog` pairs each `Tool` with its executor). #### Extended by * [`ExecutableTool`](#api-executabletool) #### Properties ##### description > **description**: `string` What the tool does and when to use it — the main ranking signal. ##### id > **id**: `string` Unique id, the registry key. Re-registering an existing id replaces the entry in place. MCP-proxied tools use the `__` convention. ##### inputSchema > **inputSchema**: `JSONSchema7` JSON Schema of the arguments. Property names and their `description`s (nested included) are indexed for ranking. ##### name > **name**: `string` Callable name (typically the same as `id` for local tools); indexed for ranking both whole and split on `snake_case`/`camelCase` boundaries. ##### outputSchema > **outputSchema**: `JSONSchema7` JSON Schema of the result; indexed the same way as `inputSchema`. *** ### ToolCatalogOptions Construction options for [ToolCatalog](#api-toolcatalog). #### Properties ##### method? > `optional` **method?**: [`SearchMethod`](#api-searchmethod) Default retrieval method for `search` (default `"bm25"`, model-free). A per-call `method` argument overrides it. ##### trace? > `optional` **trace?**: [`TraceSinkConfig`](#api-tracesinkconfig) Local trace stream destination (default: discard). See [TraceSinkConfig](#api-tracesinkconfig). *** ### UpstreamServerInfo Descriptive metadata about one upstream MCP server behind the catalog. Fed to [searchCapabilitiesTool](#api-searchcapabilitiestool) via [SearchCapabilitiesOptions](#api-searchcapabilitiesoptions): the servers are listed in the tool's description (via [formatUpstreamLine](#api-formatupstreamline)), and `description`/`instructions` enrich the matching [CapabilityToolGroup.server](#api-server) in results. #### Properties ##### description? > `optional` **description?**: `string` One-line summary of what the server offers; compacted in the listing. ##### instructions? > `optional` **instructions?**: `string` The server's own usage instructions (e.g. `McpServerHandle.serverInstructions`). ##### name > **name**: `string` Server name; must match the `name` the server was registered under (`registerMcpServer`), i.e. the `__` prefix of its tool ids. ##### needsAuth? > `optional` **needsAuth?**: `boolean` True when the upstream rejected its boot connection with 401 / requires re-authorization. ##### toolCount? > `optional` **toolCount?**: `number` Number of tools the server contributes; shown in the listing when set. ## Type Aliases ### ContentCapture > **ContentCapture** = *typeof* [`ContentCapture`](#api-contentcapture-1)\[keyof *typeof* [`ContentCapture`](#api-contentcapture-1)] Message/tool content capture modes for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` (CONVENTIONS.md § Capture gating). Default off. *** ### Executor > **Executor** = (`input`) => `Promise`\<`unknown`> | `unknown` The function that runs a tool. Receives the arguments object and may return either a plain value or a `Promise` — [ToolCatalog.invoke](#api-invoke-1) awaits both, so synchronous executors need no wrapping. #### Parameters | Parameter | Type | | --------- | ----- | | `input` | `any` | #### Returns `Promise`\<`unknown`> | `unknown` *** ### SearchMethod > **SearchMethod** = `"bm25"` | `"semantic"` | `"hybrid"` Retrieval engine for [ToolCatalog.search](#api-search-2) (and the skill catalog's `search`): * `"bm25"` — lexical ranking; model-free and infallible (the default). * `"semantic"` — cosine similarity over prebuilt embeddings. * `"hybrid"` — BM25 and semantic rankings fused with Reciprocal Rank Fusion (ADR-0011). `"semantic"`/`"hybrid"` require the embedding cache (built eagerly at registration when they are the catalog default, or via `buildEmbeddings()`). *** ### SearchOrigin > **SearchOrigin** = `"direct"` | `"agent"` Who initiated a search: `"direct"` for host code calling the SDK itself (pre-fetch helpers, benchmarks), `"agent"` for a call the model synthesized through the capability tools (`search_capabilities`). Recorded on trace events and the `ratel.origin` span attribute so consumers can separate the two paths. *** ### TraceSinkConfig > **TraceSinkConfig** = \{ `kind`: `"noop"`; } | \{ `kind`: `"memory"`; `sessionId`: `string`; } | \{ `kind`: `"jsonl"`; `path`: `string`; `sessionId`: `string`; } Where the local trace stream (ADR-0007) goes. Distinct from the OTel spans in [ToolCatalog](#api-toolcatalog)'s docs — this is the in-process channel drained via [ToolCatalog.drainTraceEvents](#api-draintraceevents-2) or written to disk. * `"noop"` — discard every event (the default when no `trace` option is given). * `"memory"` — buffer envelopes in-process; read them back with [ToolCatalog.drainTraceEvents](#api-draintraceevents-2). `sessionId` is stamped on each envelope. * `"jsonl"` — append one JSON envelope per line to the file at `path` (parent directories are created). `sessionId` is stamped on each envelope. #### Union Members ##### Type Literal \{ `kind`: `"noop"`; } | Name | Type | Description | | ------ | -------- | -------------------- | | `kind` | `"noop"` | Discard every event. | *** ##### Type Literal \{ `kind`: `"memory"`; `sessionId`: `string`; } | Name | Type | Description | | ----------- | ---------- | --------------------------------------------------- | | `kind` | `"memory"` | Buffer envelopes in-process for `drainTraceEvents`. | | `sessionId` | `string` | Session id stamped on every envelope. | *** ##### Type Literal \{ `kind`: `"jsonl"`; `path`: `string`; `sessionId`: `string`; } | Name | Type | Description | | ----------- | --------- | -------------------------------------------------- | | `kind` | `"jsonl"` | Append one JSON envelope per line to `path`. | | `path` | `string` | File to append to; parent directories are created. | | `sessionId` | `string` | Session id stamped on every envelope. | ## Variables ### ContentCapture > `const` **ContentCapture**: `object` Message/tool content capture modes for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` (CONVENTIONS.md § Capture gating). Default off. #### Type Declaration | Name | Type | | --------------- | ------------------ | | `EventOnly` | `"EVENT_ONLY"` | | `NoContent` | `"NO_CONTENT"` | | `SpanAndEvent` | `"SPAN_AND_EVENT"` | | `SpanOnly` | `"SPAN_ONLY"` | *** ### GET\_SKILL\_CONTENT\_ID > `const` **GET\_SKILL\_CONTENT\_ID**: `"get_skill_content"` Wire id (`"get_skill_content"`) of the skill-loading capability tool built by [getSkillContentTool](#api-getskillcontenttool) — the name the model calls it by. *** ### INVOKE\_TOOL\_ID > `const` **INVOKE\_TOOL\_ID**: `"invoke_tool"` Wire id (`"invoke_tool"`) of the execution capability tool built by [invokeToolTool](#api-invoketooltool) — the name the model calls it by. *** ### SEARCH\_CAPABILITIES\_ID > `const` **SEARCH\_CAPABILITIES\_ID**: `"search_capabilities"` Wire id (`"search_capabilities"`) of the discovery capability tool built by [searchCapabilitiesTool](#api-searchcapabilitiestool) — the name the model calls it by. *** ### ~~SEARCH\_TOOLS\_ID~~ > `const` **SEARCH\_TOOLS\_ID**: `"search_tools"` #### Deprecated Use `SEARCH_CAPABILITIES_ID` (`"search_capabilities"`). ## Functions ### clearContentCapture() > **clearContentCapture**(`generation`): `void` Clear the programmatic content-capture override, but only when `generation` — the token returned by [setContentCapture](#api-setcontentcapture) — still identifies the most recent set. A stale token no-ops, so an old handle shutting down late cannot clobber an override a newer caller installed (and silently re-enable, or disable, capture via the env fallback). For an unconditional clear, use `setContentCapture(null)`. #### Parameters | Parameter | Type | | ------------ | -------- | | `generation` | `number` | #### Returns `void` *** ### configureTelemetry() > **configureTelemetry**(`options?`): `Promise`\<[`TelemetryHandle`](#api-telemetryhandle)> Convenience wiring for the greenfield case: register a Ratel-owned OTLP exporter so the spans this SDK emits are shipped to Ratel Cloud (or any OTLP endpoint). Delegates to the optional `@ratel-ai/telemetry-otlp` package, lazily imported so the base SDK install stays OTel-SDK-free (ADR-0007). A host that already runs its own OpenTelemetry provider should skip this — the SDK's spans flow to that provider automatically — and add `ratelSpanProcessor` from `@ratel-ai/telemetry-otlp`. `captureContent` / `includeSpanAndEvents` opt into content capture in code via `setContentCapture` (an unrecognized `captureContent` throws a `TypeError` before any exporter is wired); the returned handle's `shutdown()` clears the override, so tests and hot-reloads return to env-driven behavior. The clear is generation-scoped: a stale handle shutting down late never clobbers an override a newer `configureTelemetry`/`setContentCapture` installed. #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------------- | | `options` | [`ConfigureTelemetryOptions`](#api-configuretelemetryoptions) | #### Returns `Promise`\<[`TelemetryHandle`](#api-telemetryhandle)> *** ### formatUpstreamLine() > **formatUpstreamLine**(`s`): `string` Format one upstream server as the bullet line the capability-tool descriptions embed (`- ( tools) (auth required)`, omitting the parts that are unset). Exported for the deprecated `searchToolsTool` shim, which builds the same listing. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------- | ----------------------- | | `s` | [`UpstreamServerInfo`](#api-upstreamserverinfo) | The server to describe. | #### Returns `string` A single `- `-prefixed line with the description compacted. *** ### getSkillContentTool() > **getSkillContentTool**(`catalog`): [`ExecutableTool`](#api-executabletool) Build the `get_skill_content` capability tool: load a skill's full instructions by id — the counterpart to `invoke_tool`. Skills are *read*, not executed: the agent discovers a skill in the `skills` bucket of `search_capabilities`, then pulls its playbook into context here. The tool takes `{ skillId }` and resolves to `{ body }` (the skill's Markdown) on success, or `{ error, isError: true }` for an unknown id — a structured result rather than a rejection, so the model can recover. Each load records a `ratel.skill.load` span plus a `skill_invoke` trace event (unknown ids record `gateway_error`). #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------- | --------------------------------- | | `catalog` | [`SkillCatalog`](#api-skillcatalog) | Catalog whose skills this serves. | #### Returns [`ExecutableTool`](#api-executabletool) The tool, ready to expose to the model. *** ### invokeToolTool() > **invokeToolTool**(`catalog`, `opts?`): [`ExecutableTool`](#api-executabletool) Build the `invoke_tool` capability tool: the execution counterpart to [searchCapabilitiesTool](#api-searchcapabilitiestool). Takes `{ toolId, args }` and runs the catalog tool via [ToolCatalog.invoke](#api-invoke-1), returning its result. Failures come back as structured `{ error, isError: true }` results, not rejections, so the model can read and recover from them: an unknown `toolId` (with a hint to search first), a non-object `args`, or a thrown executor error. A tool that throws `UnauthorizedError` gets special handling — `opts.onUnauthorized` fires with the upstream name inferred from the `__` id prefix, a `ratel.auth.flow` span records the outcome, and the result is `{ error: "needs_auth", isError: true, upstream?, hint }`. A call with `args` missing (or `null`) is tolerated by treating the remaining top-level keys as the arguments. Outcomes are recorded as `gateway_invoke` / `gateway_error` events on the local trace stream. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ---------------------------------- | | `catalog` | [`ToolCatalog`](#api-toolcatalog) | Catalog whose tools this executes. | | `opts` | [`InvokeToolToolOptions`](#api-invoketooltooloptions) | Optional auth-failure callback. | #### Returns [`ExecutableTool`](#api-executabletool) The tool, ready to expose to the model. *** ### registerMcpServer() > **registerMcpServer**(`catalog`, `options`): `Promise`\<[`McpServerHandle`](#api-mcpserverhandle)> Ingest an MCP server into a [ToolCatalog](#api-toolcatalog): connect over the given transport, list its tools once (no live refresh), and register each as an [executable tool](#api-register-2) whose executor proxies `callTool` on the live client. A missing tool description registers as `""`; a missing output schema as `{ type: "object" }`. The whole registration is one `ratel.upstream.register` OTel span and an `upstream_register` local trace event; each later invocation records `upstream_invoke` (or `upstream_error`) alongside the catalog's own events (ADR-0007). Rejects if connecting or listing tools fails. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------------- | --------------------------------------------- | | `catalog` | [`ToolCatalog`](#api-toolcatalog) | Catalog that receives the proxied tools. | | `options` | [`RegisterMcpServerOptions`](#api-registermcpserveroptions) | Server name (the id namespace) and transport. | #### Returns `Promise`\<[`McpServerHandle`](#api-mcpserverhandle)> A handle with the registered ids, the server's instructions, and `close()`. #### Example ```ts import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { registerMcpServer, ToolCatalog } from "@ratel-ai/sdk"; const catalog = new ToolCatalog(); const github = await registerMcpServer(catalog, { name: "github", transport: new StdioClientTransport({ command: "github-mcp-server" }), }); // github.toolIds → ["github__create_issue", "github__get_pull_request", ...] const result = await catalog.invoke("github__create_issue", { title: "Flaky test on main", }); await github.close(); ``` *** ### searchCapabilitiesTool() > **searchCapabilitiesTool**(`toolCatalog`, `skillCatalog?`, `opts?`): [`ExecutableTool`](#api-executabletool) Build the `search_capabilities` capability tool: unified discovery over tools AND skills. Its result carries two independently-ranked buckets, each with its own top-K budget — so a relevant skill can never be starved out of the results by a large number of matching tools (and we avoid comparing BM25 scores across the two different text shapes). The tool takes `{ query, topKTools?, topKSkills? }` (defaults 5 and 3; values above 50 are capped, anything else non-positive or non-integer falls back to the default) and resolves to a [SearchCapabilitiesResult](#api-searchcapabilitiesresult). A matched skill's declared tool dependencies are pulled into the `tools` bucket additively — score `0`, beyond the `topKTools` budget, deduped against query hits. The `skills` bucket (and its mention in the tool description) exists only when `skillCatalog` is non-empty at build time. Each call records a `gateway_search` event on the local trace stream. #### Parameters | Parameter | Type | Description | | --------------- | ------------------------------------------------------------- | --------------------------------------------------------------- | | `toolCatalog` | [`ToolCatalog`](#api-toolcatalog) | Catalog the `tools` bucket is ranked from. | | `skillCatalog?` | [`SkillCatalog`](#api-skillcatalog) | Optional catalog the `skills` bucket is ranked from. | | `opts?` | [`SearchCapabilitiesOptions`](#api-searchcapabilitiesoptions) | Upstream-server metadata for the description and result groups. | #### Returns [`ExecutableTool`](#api-executabletool) The tool, ready to expose to the model (and to register alongside [invokeToolTool](#api-invoketooltool), which executes what this discovers). #### Example ```ts import { searchCapabilitiesTool, type SearchCapabilitiesResult } from "@ratel-ai/sdk"; const discovery = searchCapabilitiesTool(toolCatalog, skillCatalog, { upstreamServers: [{ name: "github", description: "GitHub API", toolCount: 30 }], }); const result = (await discovery.execute({ query: "open a pull request", })) as SearchCapabilitiesResult; ``` *** ### ~~searchToolsTool()~~ > **searchToolsTool**(`catalog`, `opts?`): [`ExecutableTool`](#api-executabletool) The pre-0.2.0 tools-only discovery tool, preserved verbatim (id `search_tools`, `{ groups }` result, `topK` input). New code should use [searchCapabilitiesTool](#api-searchcapabilitiestool), which additionally returns a reserved `skills` bucket. Registering both lets a host serve the old and new names during a migration window. #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------- | | `catalog` | [`ToolCatalog`](#api-toolcatalog) | | `opts?` | [`SearchToolsToolOptions`](#api-searchtoolstooloptions) | #### Returns [`ExecutableTool`](#api-executabletool) #### Deprecated Use `searchCapabilitiesTool`. Tracked for removal in RAT-250. *** ### setContentCapture() > **setContentCapture**(`mode`): `number` Programmatically set the content-capture mode. While set, [contentCaptureMode](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/ts/src/config.ts) returns this mode regardless of `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — programmatic config wins over the environment, matching how OpenTelemetry treats env vars as the fallback for code-level configuration. Pass `null`/`undefined` to clear the override unconditionally and return to env-driven parsing. The mode is validated like the env var (case-insensitive, legacy `true`/`false`/ `1`/`0` accepted) and **throws a `TypeError`** on anything unrecognized — failing loud at config time instead of storing a value that would both disable capture and mask the env var. Returns a generation token identifying this call as the current owner of the override; pass it to [clearContentCapture](#api-clearcontentcapture) to clear only if no newer set has happened since (the safe form for shutdown/teardown hooks). #### Parameters | Parameter | Type | | --------- | ---------------------------------------------------------------- | | `mode` | [`ContentCapture`](#api-contentcapture) \| `null` \| `undefined` | #### Returns `number` --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/typescript/sdk/changelog > @ratel-ai/sdk release history · [npm](https://www.npmjs.com/package/@ratel-ai/sdk) · [source](https://github.com/ratel-ai/ratel/tree/main/src/sdk/ts) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `@ratel-ai/sdk` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.4.1] - 2026-07-10 ### Added * `configureTelemetry` opts into message/tool content capture programmatically: `captureContent` sets the exact `ContentCapture` mode (validated like the env var — case-insensitive, legacy boolean forms accepted — throwing a `TypeError` on garbage before any exporter is wired), and `includeSpanAndEvents` is boolean sugar (`true` → `SPAN_AND_EVENT`, `false` → `NO_CONTENT`). `captureContent` wins over `includeSpanAndEvents`; when neither is provided, `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` keeps ruling (a provided option beats the env var, as in OTel code-over-env precedence). The handle's `shutdown()` restores env-driven behavior via a generation-scoped clear (`clearContentCapture`), so a stale handle shutting down late never clobbers an override a newer `configureTelemetry` installed. `ContentCapture`, `setContentCapture`, and `clearContentCapture` are re-exported from `@ratel-ai/sdk` (new `ConfigureTelemetryOptions` type). ## \[0.4.0] - 2026-07-07 ### Added * **OpenTelemetry emission.** The SDK now opens an OTel span at each funnel boundary — `execute_tool` (`gen_ai.operation.name`, `gen_ai.tool.name`, `ratel.tool.args_size_bytes`, plus `ratel.upstream.server` for MCP-proxied tools), `ratel.search` (target, `top_k`, origin, `hit_count`), `ratel.skill.load`, `ratel.upstream.register`, and `ratel.auth.flow` — alongside the existing local `recordEvent` stream, which is unchanged. Emission is transparent and free by default: spans flow to whatever OpenTelemetry provider is registered and are a no-op until one is, so a host already running OTel sees Ratel's funnel on its traces with no setup. Built on `@opentelemetry/api` + the OTel-free `@ratel-ai/telemetry` vocabulary, so the base install stays OTel-SDK-free. * `configureTelemetry({ apiKey })` convenience wiring (with `TelemetryHandle` / `InitOptions`): lazily loads the optional `@ratel-ai/telemetry-otlp` peer to ship the SDK's spans to Ratel Cloud (or any OTLP endpoint). Hosts already running OpenTelemetry skip it and add `ratelSpanProcessor` from `@ratel-ai/telemetry-otlp` instead. * Message/tool content (`ratel.search.query`, `gen_ai.tool.call.arguments` / `.result`) rides span attributes only when `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` selects a span mode (`SPAN_ONLY` / `SPAN_AND_EVENT`); default off. `ratel.tool.args_size_bytes` is measured in UTF-8 bytes. ## \[0.3.0] - 2026-07-06 ### Added * `ToolCatalog` / `SkillCatalog` accept a default `method` (`"bm25"` | `"semantic"` | `"hybrid"`) and `search(query, topK, origin?, method?)` takes a per-call override. `"bm25"` (default) is unchanged and model-free; `"semantic"` / `"hybrid"` load a local embedding model and throw if it fails to load. Exposed via the native `searchWithMethod` binding and the `SearchMethod` type. * A `"semantic"`/`"hybrid"` catalog embeds each tool/skill **eagerly at `register`** (incrementally), so searches never pay the corpus-embedding cost. New `catalog.buildEmbeddings()` pre-computes embeddings on demand (e.g. after a bulk register). BM25 catalogs do neither. A semantic/hybrid search on a catalog with no embeddings built throws (embeddings not computed) rather than embedding during the search. ## \[0.2.1-rc.1] - 2026-07-04 ### Changed * First release cut under the per-package release scheme (ADR-0008): `@ratel-ai/sdk` (loader + platform packages) now versions and ships independently, tagged `sdk-js-v*`. No API changes since 0.2.0. ## \[0.2.0] - 2026-06-16 ### Changed * **BREAKING:** the discovery gateway tool is renamed `search_tools` → `search_capabilities`. It now returns two independently-ranked, separately-budgeted buckets — `{ tools, skills }` — so a relevant skill is never crowded out by matching tools. New surface: `searchCapabilitiesTool`/`SEARCH_CAPABILITIES_ID`. The old `searchToolsTool`/`SEARCH_TOOLS_ID` are kept as deprecated aliases (see *Deprecated*), so `0.1.x` code keeps working after upgrading. ### Added * First-class **skills**: `SkillCatalog`, `getSkillContentTool` (`get_skill_content`), and `Skill`/`SkillHit`/`SkillRegistry`. Skills are reusable playbooks ranked by a separate BM25 corpus and loaded on demand. * Skill–tool coupling: a `Skill` can declare a `tools` list, and `search_capabilities` pulls a matched skill's declared tools into the `tools` bucket — additively (beyond `topKTools`) and deduped against query hits — so the agent gets the playbook and the tools it needs in one turn. ### Deprecated * `searchToolsTool`, `SEARCH_TOOLS_ID`, and the `SearchToolHit`/`SearchToolsGroup`/`SearchToolsResult`/`SearchToolsToolOptions` types. They retain their pre-0.2.0 behaviour — a tools-only `{ groups }` result and the `search_tools` id — so upgrading from `0.1.x` does not break existing callers. Migrate to `searchCapabilitiesTool`; the aliases will be removed in a future release. ### Fixed * Gateway error payloads (`invoke_tool`, `get_skill_content`) carry `isError: true`, so a host can flag a failed call rather than read it as content. * `invoke_tool` rejects a non-object `args` instead of forwarding stray top-level keys. * `search_capabilities` validates `topKTools`/`topKSkills` (declared `integer`, positive): `0`, negative, and fractional values fall back to the default instead of returning zero results — or, via a negative wrapping to `u32` in the native layer, an unbounded set. TypeScript and Python behave identically. * `search_capabilities` advertises the `skills` bucket and `get_skill_content` in its description only when a non-empty skill catalog is wired in. * `Skill.tags` and `Skill.body` are optional (default `[]`/`""`), matching the Python SDK — a minimal `{ id, name, description }` skill is valid. ## \[0.1.6] - 2026-06-10 ### Fixed * TypeScript typehint for JSON-schema tool input/output ([#54](https://github.com/ratel-ai/ratel/pull/54)). ## \[0.1.5] - 2026-05-10 ### Added * Initial release on the v1 (revamp) line. TypeScript SDK over the Rust core: BM25 tool retrieval, MCP ingestion, framework-neutral gateway tools. See the [package README](https://github.com/ratel-ai/ratel/blob/main/src/sdk/ts/README.md) for the full surface. * `ToolCatalog` accepts `{ trace }` config in its constructor — `noop` (default), `memory`, or `jsonl`. Captured events flow through the Rust core sink ([ADR-0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md)). New `recordEvent`, `drainTraceEvents`, and an optional third `origin` argument on `search`. * `searchToolsTool` emits `gateway_search` with `origin: "agent"`. `invokeToolTool` emits `gateway_invoke` on success and `gateway_error` for unknown ids, `needs_auth`, and underlying throws. * `ToolCatalog.invoke` emits `invoke_start` / `invoke_end` / `invoke_error` around the executor with `args_size_bytes` and `took_ms`. * `registerMcpServer` emits `upstream_register` on connect and `upstream_invoke` / `upstream_error` per upstream call. New `searchWithOrigin` and trace plumbing on the underlying NAPI `ToolRegistry`. `SearchOrigin` is now `"direct" | "agent"` (was `"user" | "agent"`); the union is exported from the package entry point. ### Changed * Rewrote `search_tools` description to nudge agents toward discovery before falling back to broad listing. --- # @ratel-ai/telemetry Source: https://docs.ratel.sh/docs/core/packages/typescript/telemetry > v0.1.2 · TypeScript · [npm](https://www.npmjs.com/package/@ratel-ai/telemetry) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/ts) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. `@ratel-ai/telemetry` — the `ratel.*` telemetry vocabulary. See the wire contract in [CONVENTIONS.md](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/CONVENTIONS.md). Emitting the vocabulary is done through the standard OpenTelemetry JS SDK; this package adds no transport and no schema (ADR-0007). These constants are the `ratel.*` overlay Ratel owns, plus the small subset of `gen_ai.*` keys the overlay emits directly on the `execute_tool` span (borrowed verbatim from OpenTelemetry, never renamed). This package is OTel-free (ADR-0007): the OTLP config resolver and the content-capture gate are re-exported from `./config`; the `init()` exporter, which does pull the OpenTelemetry SDK, lives in `@ratel-ai/telemetry-otlp`. ## Interfaces ### InitOptions `init()` (in `@ratel-ai/telemetry-otlp`) resolves endpoint + auth from `RATEL_URL` / `RATEL_API_KEY`, with explicit `{ endpoint, apiKey }` values taking precedence over the environment. An explicit `apiKey` sets the Bearer header; the `RATEL_API_KEY` fallback only applies when neither `apiKey` nor an explicit `Authorization` header is given, so ambient env never overrides an auth header the caller passed on purpose. #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](#api-default_service_name). *** ### ResolvedOtlpConfig Resolved exporter configuration; the pure core of the OTLP exporter, exposed for testing. #### Properties ##### headers > **headers**: `Record`\<`string`, `string`> ##### serviceName > **serviceName**: `string` ##### url > **url**: `string` ## Type Aliases ### AuthOutcome > **AuthOutcome** = *typeof* [`AuthOutcome`](#api-authoutcome-1)\[keyof *typeof* [`AuthOutcome`](#api-authoutcome-1)] Outcome of an MCP auth flow. Carried by `ratel.auth.outcome`; `NeedsAuth` is the 401-driven `AuthNeeds` case (ADR-0007 `auth_needs`). *** ### ContentCapture > **ContentCapture** = *typeof* [`ContentCapture`](#api-contentcapture-1)\[keyof *typeof* [`ContentCapture`](#api-contentcapture-1)] Message/tool content capture modes for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` (CONVENTIONS.md § Capture gating). Default off. *** ### Origin > **Origin** = *typeof* [`Origin`](#api-origin-1)\[keyof *typeof* [`Origin`](#api-origin-1)] Whether a `ratel.*` span was a direct library call or synthesized by the agent inside its loop. Carried by `ratel.origin`; mirrors the local trace `Origin` (ADR-0007). *** ### SearchTarget > **SearchTarget** = *typeof* [`SearchTarget`](#api-searchtarget-1)\[keyof *typeof* [`SearchTarget`](#api-searchtarget-1)] What a `ratel.search` span was searching. Carried by `ratel.search.target`; folds capability-tool search and skill search into one span shape. ## Variables ### API\_KEY\_ENV > `const` **API\_KEY\_ENV**: `"RATEL_API_KEY"` = `"RATEL_API_KEY"` Env var whose value is the default Ratel API key. *** ### AuthOutcome > `const` **AuthOutcome**: `object` Outcome of an MCP auth flow. Carried by `ratel.auth.outcome`; `NeedsAuth` is the 401-driven `AuthNeeds` case (ADR-0007 `auth_needs`). #### Type Declaration | Name | Type | Default value | | ------------ | -------------- | -------------- | | `Failed` | `"failed"` | `"failed"` | | `NeedsAuth` | `"needs_auth"` | `"needs_auth"` | | `Ok` | `"ok"` | `"ok"` | | `Refreshed` | `"refreshed"` | `"refreshed"` | *** ### CAPTURE\_CONTENT\_ENV > `const` **CAPTURE\_CONTENT\_ENV**: `"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"` = `"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"` The ecosystem instrumentation env var gating message/tool content capture. Default off; honored by `init()` rather than a Ratel-invented flag (CONVENTIONS.md § Capture gating). Values: legacy boolean, or the enum `NO_CONTENT` (default) / `SPAN_ONLY` / `EVENT_ONLY` / `SPAN_AND_EVENT`. *** ### ContentCapture > `const` **ContentCapture**: `object` Message/tool content capture modes for `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` (CONVENTIONS.md § Capture gating). Default off. #### Type Declaration | Name | Type | Default value | | --------------- | ------------------ | ------------------ | | `EventOnly` | `"EVENT_ONLY"` | `"EVENT_ONLY"` | | `NoContent` | `"NO_CONTENT"` | `"NO_CONTENT"` | | `SpanAndEvent` | `"SPAN_AND_EVENT"` | `"SPAN_AND_EVENT"` | | `SpanOnly` | `"SPAN_ONLY"` | `"SPAN_ONLY"` | *** ### DEFAULT\_SERVICE\_NAME > `const` **DEFAULT\_SERVICE\_NAME**: `"ratel"` = `"ratel"` `service.name` used when the caller does not pass one. *** ### ENDPOINT\_ENV > `const` **ENDPOINT\_ENV**: `"RATEL_URL"` = `"RATEL_URL"` Env var whose value is the default OTLP endpoint. *** ### EXECUTE\_TOOL > `const` **EXECUTE\_TOOL**: `"execute_tool"` = `"execute_tool"` `execute_tool` — the `gen_ai.operation.name` value for a tool invocation. Deliberately the standard OTel `gen_ai` operation, not a bespoke `ratel.invoke` span, so a generic OTel backend already understands it (locked 2026-07-05). The invoke is enriched with `ratel.*` attributes. *** ### GEN\_AI\_INFERENCE\_DETAILS > `const` **GEN\_AI\_INFERENCE\_DETAILS**: `"gen_ai.client.inference.operation.details"` = `"gen_ai.client.inference.operation.details"` `gen_ai.client.inference.operation.details` — the event that carries message text and tool-call content (never span attributes). Borrowed from gen\_ai (Tier 1). *** ### GEN\_AI\_OPERATION\_NAME > `const` **GEN\_AI\_OPERATION\_NAME**: `"gen_ai.operation.name"` = `"gen_ai.operation.name"` `gen_ai.operation.name` — set to [EXECUTE\_TOOL](#api-execute_tool) for a tool invocation. *** ### GEN\_AI\_TOOL\_CALL\_ARGUMENTS > `const` **GEN\_AI\_TOOL\_CALL\_ARGUMENTS**: `"gen_ai.tool.call.arguments"` = `"gen_ai.tool.call.arguments"` `gen_ai.tool.call.arguments` — tool arguments (Opt-In content, gated). *** ### GEN\_AI\_TOOL\_CALL\_ID > `const` **GEN\_AI\_TOOL\_CALL\_ID**: `"gen_ai.tool.call.id"` = `"gen_ai.tool.call.id"` `gen_ai.tool.call.id` — tool call id, when available. *** ### GEN\_AI\_TOOL\_CALL\_RESULT > `const` **GEN\_AI\_TOOL\_CALL\_RESULT**: `"gen_ai.tool.call.result"` = `"gen_ai.tool.call.result"` `gen_ai.tool.call.result` — tool result (Opt-In content, gated). *** ### GEN\_AI\_TOOL\_NAME > `const` **GEN\_AI\_TOOL\_NAME**: `"gen_ai.tool.name"` = `"gen_ai.tool.name"` `gen_ai.tool.name` — the capability tool id. *** ### Origin > `const` **Origin**: `object` Whether a `ratel.*` span was a direct library call or synthesized by the agent inside its loop. Carried by `ratel.origin`; mirrors the local trace `Origin` (ADR-0007). #### Type Declaration | Name | Type | Default value | | --------- | ---------- | ------------- | | `Agent` | `"agent"` | `"agent"` | | `Direct` | `"direct"` | `"direct"` | *** ### RATEL\_AUTH\_FLOW > `const` **RATEL\_AUTH\_FLOW**: `"ratel.auth.flow"` = `"ratel.auth.flow"` `ratel.auth.flow` — MCP auth flow. *** ### RATEL\_AUTH\_OUTCOME > `const` **RATEL\_AUTH\_OUTCOME**: `"ratel.auth.outcome"` = `"ratel.auth.outcome"` `ratel.auth.outcome` — `ok` / `refreshed` / `needs_auth` / `failed` (see [AuthOutcome](#api-authoutcome-1)). *** ### RATEL\_ORIGIN > `const` **RATEL\_ORIGIN**: `"ratel.origin"` = `"ratel.origin"` `ratel.origin` — direct library call vs agent-synthesized (shared attribute). *** ### RATEL\_SEARCH > `const` **RATEL\_SEARCH**: `"ratel.search"` = `"ratel.search"` `ratel.search` — capability search (unifies tool-search and skill-search). *** ### RATEL\_SEARCH\_HIT\_COUNT > `const` **RATEL\_SEARCH\_HIT\_COUNT**: `"ratel.search.hit_count"` = `"ratel.search.hit_count"` `ratel.search.hit_count` — results returned. *** ### RATEL\_SEARCH\_QUERY > `const` **RATEL\_SEARCH\_QUERY**: `"ratel.search.query"` = `"ratel.search.query"` `ratel.search.query` — the search text (content, gated like message content). *** ### RATEL\_SEARCH\_RESULTS > `const` **RATEL\_SEARCH\_RESULTS**: `"ratel.search.results"` = `"ratel.search.results"` `ratel.search.results` — Opt-In event carrying hit ids + scores + per-stage BM25 timing; gated like content. The `ratel.search` span itself carries only counts. *** ### RATEL\_SEARCH\_TARGET > `const` **RATEL\_SEARCH\_TARGET**: `"ratel.search.target"` = `"ratel.search.target"` `ratel.search.target` — `tool` or `skill` (see [SearchTarget](#api-searchtarget-1)). *** ### RATEL\_SEARCH\_TOP\_K > `const` **RATEL\_SEARCH\_TOP\_K**: `"ratel.search.top_k"` = `"ratel.search.top_k"` `ratel.search.top_k` — requested result count. *** ### RATEL\_SKILL\_ID > `const` **RATEL\_SKILL\_ID**: `"ratel.skill.id"` = `"ratel.skill.id"` `ratel.skill.id` — skill loaded on the `ratel.skill.load` span. *** ### RATEL\_SKILL\_LOAD > `const` **RATEL\_SKILL\_LOAD**: `"ratel.skill.load"` = `"ratel.skill.load"` `ratel.skill.load` — skill content load (`get_skill_content`). *** ### RATEL\_TOOL\_ARGS\_SIZE\_BYTES > `const` **RATEL\_TOOL\_ARGS\_SIZE\_BYTES**: `"ratel.tool.args_size_bytes"` = `"ratel.tool.args_size_bytes"` `ratel.tool.args_size_bytes` — argument payload size on the `execute_tool` span. *** ### RATEL\_UPSTREAM\_REGISTER > `const` **RATEL\_UPSTREAM\_REGISTER**: `"ratel.upstream.register"` = `"ratel.upstream.register"` `ratel.upstream.register` — upstream-MCP ingest. *** ### RATEL\_UPSTREAM\_SERVER > `const` **RATEL\_UPSTREAM\_SERVER**: `"ratel.upstream.server"` = `"ratel.upstream.server"` `ratel.upstream.server` — upstream MCP server backing a tool / auth flow. *** ### RATEL\_UPSTREAM\_TOOL\_COUNT > `const` **RATEL\_UPSTREAM\_TOOL\_COUNT**: `"ratel.upstream.tool_count"` = `"ratel.upstream.tool_count"` `ratel.upstream.tool_count` — tools ingested on register. *** ### RATEL\_UPSTREAM\_TRANSPORT > `const` **RATEL\_UPSTREAM\_TRANSPORT**: `"ratel.upstream.transport"` = `"ratel.upstream.transport"` `ratel.upstream.transport` — `stdio` / `http` / `sse` / ... *** ### SearchTarget > `const` **SearchTarget**: `object` What a `ratel.search` span was searching. Carried by `ratel.search.target`; folds capability-tool search and skill search into one span shape. #### Type Declaration | Name | Type | Default value | | -------- | --------- | ------------- | | `Skill` | `"skill"` | `"skill"` | | `Tool` | `"tool"` | `"tool"` | *** ### SEMCONV\_VERSION > `const` **SEMCONV\_VERSION**: `"1.42.0"` = `"1.42.0"` The pinned OpenTelemetry semantic-conventions version this vocabulary tracks (the `gen_ai` group). The pin is the contract; consumers read against this exact version, never "latest" (CONVENTIONS.md § The pin). ## Functions ### clearContentCapture() > **clearContentCapture**(`generation`): `void` Clear the programmatic content-capture override, but only when `generation` — the token returned by [setContentCapture](#api-setcontentcapture) — still identifies the most recent set. A stale token no-ops, so an old handle shutting down late cannot clobber an override a newer caller installed (and silently re-enable, or disable, capture via the env fallback). For an unconditional clear, use `setContentCapture(null)`. #### Parameters | Parameter | Type | | ------------ | -------- | | `generation` | `number` | #### Returns `void` *** ### contentCaptureMode() > **contentCaptureMode**(`env?`): [`ContentCapture`](#api-contentcapture) Parse the ecosystem content-capture gate. A mode set via [setContentCapture](#api-setcontentcapture) wins outright (env is the fallback, as in OTel); otherwise default [ContentCapture.NoContent](#api-property-nocontent) when unset/empty/unrecognized. The legacy boolean form maps `true` to full capture ([ContentCapture.SpanAndEvent](#api-property-spanandevent)) and `false` to none. #### Parameters | Parameter | Type | Default value | | --------- | -------------------------------------------- | ------------- | | `env` | `Record`\<`string`, `string` \| `undefined`> | `process.env` | #### Returns [`ContentCapture`](#api-contentcapture) *** ### resolveOtlpConfig() > **resolveOtlpConfig**(`opts?`, `env?`): [`ResolvedOtlpConfig`](#api-resolvedotlpconfig) Resolve [InitOptions](#api-initoptions) into concrete exporter config. Pure and env-injectable so the endpoint/auth precedence is testable without a network. #### Parameters | Parameter | Type | Default value | | --------- | -------------------------------------------- | ------------- | | `opts` | [`InitOptions`](#api-initoptions) | `{}` | | `env` | `Record`\<`string`, `string` \| `undefined`> | `process.env` | #### Returns [`ResolvedOtlpConfig`](#api-resolvedotlpconfig) *** ### setContentCapture() > **setContentCapture**(`mode`): `number` Programmatically set the content-capture mode. While set, [contentCaptureMode](#api-contentcapturemode) returns this mode regardless of `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — programmatic config wins over the environment, matching how OpenTelemetry treats env vars as the fallback for code-level configuration. Pass `null`/`undefined` to clear the override unconditionally and return to env-driven parsing. The mode is validated like the env var (case-insensitive, legacy `true`/`false`/ `1`/`0` accepted) and **throws a `TypeError`** on anything unrecognized — failing loud at config time instead of storing a value that would both disable capture and mask the env var. Returns a generation token identifying this call as the current owner of the override; pass it to [clearContentCapture](#api-clearcontentcapture) to clear only if no newer set has happened since (the safe form for shutdown/teardown hooks). #### Parameters | Parameter | Type | | --------- | ---------------------------------------------------------------- | | `mode` | [`ContentCapture`](#api-contentcapture) \| `null` \| `undefined` | #### Returns `number` --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/typescript/telemetry/changelog > @ratel-ai/telemetry release history · [npm](https://www.npmjs.com/package/@ratel-ai/telemetry) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/ts) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `@ratel-ai/telemetry` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.1.2] - 2026-07-11 ### Added * `API_KEY_ENV` (`RATEL_API_KEY`) and API-key environment fallback in `resolveOtlpConfig`. An explicit `apiKey` remains authoritative; the env fallback applies only when neither `apiKey` nor an explicit `Authorization` header is given, so ambient `RATEL_API_KEY` never clobbers a caller-supplied auth header. ## \[0.1.1] - 2026-07-10 ### Added * `setContentCapture(mode)`: programmatic override of the content-capture gate. While set, `contentCaptureMode()` returns the given mode regardless of `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — code-level config wins over the environment, matching how OpenTelemetry treats env vars as the fallback for programmatic configuration. The mode is validated exactly like the env var (case-insensitive, trimmed, legacy `true`/`false`/`1`/`0` forms accepted) and throws a `TypeError` naming the valid values on anything unrecognized — failing loud at config time instead of storing a value that would both disable capture and mask the env var. Pass `null`/`undefined` to clear unconditionally. Returns a generation token identifying the call as the current owner of the override. * `clearContentCapture(generation)`: clears the override only when `generation` (the token returned by `setContentCapture`) still identifies the most recent set. A stale token no-ops, so an old telemetry handle shutting down late cannot clobber an override a newer caller installed and silently flip capture back to the env value. ## \[0.1.0] - 2026-07-06 ### Added * The telemetry helper (ADR-0015): the full `ratel.*` vocabulary as constants (attribute keys, span/event names, `gen_ai.*` interop keys, and the `Origin`/`SearchTarget`/`AuthOutcome` value enums) pinned to OpenTelemetry semconv `gen_ai` v1.42.0. * Shared contract-against-the-pin conformance suite (`../conformance/fixtures.json`): spans built from the constants through the real SDK must emit the exact pinned keys. * Usage example in the README (runnable end-to-end in `examples/telemetry-ts`). * A regression guard that no `@opentelemetry/*` runtime dependency or shipped-source import can creep back into the vocabulary package. ### Changed * `init()` lives in [`@ratel-ai/telemetry-otlp`](https://github.com/ratel-ai/ratel/blob/main/src/telemetry/ts-otlp/README.md), not this package: importing `@ratel-ai/telemetry` pulls no OpenTelemetry SDK (ADR-0015), so the SDK (emit), the server (read), and edge/serverless emitters take the `ratel.*` vocabulary weight-free. This package keeps the constants plus the pure `resolveOtlpConfig` / `contentCaptureMode`; callers of `init()` install `@ratel-ai/telemetry-otlp` and import it from there. * Released as an independent npm unit under the `telemetry-ts-v*` tag prefix. --- # @ratel-ai/telemetry-otlp Source: https://docs.ratel.sh/docs/core/packages/typescript/telemetry-otlp > v0.1.1 · TypeScript · [npm](https://www.npmjs.com/package/@ratel-ai/telemetry-otlp) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/ts-otlp) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. `@ratel-ai/telemetry-otlp` — the OTLP exporter surface for Ratel telemetry. Turnkey OpenTelemetry SDK wiring over the OTel-free `@ratel-ai/telemetry` vocabulary. Split out (ADR-0007) so importing the constants never pulls the OTel SDK. Two entry points: `init()` for a greenfield app where Ratel owns the provider, and `ratelSpanProcessor()` / `ratelTraceExporter()` to compose Ratel onto a provider a partner already owns (Langfuse, the Vercel AI SDK, ...). The OTLP config resolver and options are re-exported from `@ratel-ai/telemetry` for convenience. ## Interfaces ### InitOptions `init()` (in `@ratel-ai/telemetry-otlp`) resolves endpoint + auth from `RATEL_URL` / `RATEL_API_KEY`, with explicit `{ endpoint, apiKey }` values taking precedence over the environment. An explicit `apiKey` sets the Bearer header; the `RATEL_API_KEY` fallback only applies when neither `apiKey` nor an explicit `Authorization` header is given, so ambient env never overrides an auth header the caller passed on purpose. #### Extended by * [`TelemetryInitOptions`](#api-telemetryinitoptions) * [`RatelSpanProcessorOptions`](#api-ratelspanprocessoroptions) #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](#api-default_service_name). *** ### RatelSpanProcessorOptions Options for [ratelSpanProcessor](#api-ratelspanprocessor): the OTLP endpoint/auth plus an optional filter. #### Extends * [`InitOptions`](#api-initoptions) #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ###### Inherited from [`InitOptions`](#api-initoptions).[`apiKey`](#api-apikey) ##### enabled? > `optional` **enabled?**: `boolean` Set false to skip exporter construction and return a no-op processor. ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ###### Inherited from [`InitOptions`](#api-initoptions).[`endpoint`](#api-endpoint) ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ###### Inherited from [`InitOptions`](#api-initoptions).[`headers`](#api-headers) ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](#api-default_service_name). ###### Inherited from [`InitOptions`](#api-initoptions).[`serviceName`](#api-servicename) ##### spanFilter? > `optional` **spanFilter?**: [`SpanFilter`](#api-spanfilter-2) Override the default [ratelSignalFilter](#api-ratelsignalfilter); `() => true` forwards every span. *** ### ResolvedOtlpConfig Resolved exporter configuration; the pure core of the OTLP exporter, exposed for testing. #### Properties ##### headers > **headers**: `Record`\<`string`, `string`> ##### serviceName > **serviceName**: `string` ##### url > **url**: `string` *** ### TelemetryHandle Handle returned by [init](#api-init); `shutdown()` flushes and stops the exporter. #### Methods ##### shutdown() > **shutdown**(): `Promise`\<`void`> ###### Returns `Promise`\<`void`> *** ### TelemetryInitOptions Options for the turnkey [init](#api-init) path. #### Extends * [`InitOptions`](#api-initoptions) #### Properties ##### apiKey? > `optional` **apiKey?**: `string` Ratel API key; sent as `Authorization: Bearer `. Defaults to `RATEL_API_KEY`. ###### Inherited from [`InitOptions`](#api-initoptions).[`apiKey`](#api-apikey) ##### enabled? > `optional` **enabled?**: `boolean` On first setup, set false to skip configuration and provider registration. ##### endpoint? > `optional` **endpoint?**: `string` Full OTLP traces URL (incl. `/v1/traces`). Defaults to `RATEL_URL`. ###### Inherited from [`InitOptions`](#api-initoptions).[`endpoint`](#api-endpoint) ##### headers? > `optional` **headers?**: `Record`\<`string`, `string`> Extra headers merged onto the request. An explicit `Authorization` here is kept over the `RATEL_API_KEY` env fallback. ###### Inherited from [`InitOptions`](#api-initoptions).[`headers`](#api-headers) ##### serviceName? > `optional` **serviceName?**: `string` `service.name` resource attribute. Defaults to [DEFAULT\_SERVICE\_NAME](#api-default_service_name). ###### Inherited from [`InitOptions`](#api-initoptions).[`serviceName`](#api-servicename) ##### spanFilter? > `optional` **spanFilter?**: [`SpanFilter`](#api-spanfilter-2) Override the default filter; `init()` forwards every span when omitted. ## Type Aliases ### SpanFilter > **SpanFilter** = (`span`) => `boolean` Predicate deciding whether a finished span is forwarded to Ratel. #### Parameters | Parameter | Type | | --------- | -------------- | | `span` | `ReadableSpan` | #### Returns `boolean` ## Variables ### API\_KEY\_ENV > `const` **API\_KEY\_ENV**: `"RATEL_API_KEY"` = `"RATEL_API_KEY"` Env var whose value is the default Ratel API key. *** ### DEFAULT\_SERVICE\_NAME > `const` **DEFAULT\_SERVICE\_NAME**: `"ratel"` = `"ratel"` `service.name` used when the caller does not pass one. *** ### ENDPOINT\_ENV > `const` **ENDPOINT\_ENV**: `"RATEL_URL"` = `"RATEL_URL"` Env var whose value is the default OTLP endpoint. ## Functions ### init() > **init**(`opts?`): [`TelemetryHandle`](#api-telemetryhandle) Wire an OTLP `http/protobuf` exporter + batch processor + `service.name` resource over a `NodeTracerProvider`, register it as the global provider, and return a shutdown handle. Everything else is the untouched OTel SDK. `init()` owns the global provider, so it exports every span by default (unlike [ratelSpanProcessor](#api-ratelspanprocessor), whose default `gen_ai.*`/`ratel.*` filter exists for sharing a provider). Pass `spanFilter` to narrow that set, or `enabled: false` for a no-op handle on first setup. Repeated calls return the existing handle when Ratel already owns the active provider—even if a later caller is disabled—so shutting that handle down stops export for every caller. It throws with a pointer to [ratelSpanProcessor](#api-ratelspanprocessor) when a foreign provider is registered. Shutdown is terminal: after `handle.shutdown()`, a later `init()` throws rather than hand back the dead handle (call `trace.disable()` first to re-initialize). #### Parameters | Parameter | Type | | --------- | --------------------------------------------------- | | `opts` | [`TelemetryInitOptions`](#api-telemetryinitoptions) | #### Returns [`TelemetryHandle`](#api-telemetryhandle) *** ### ratelSignalFilter() > **ratelSignalFilter**(`span`): `boolean` Default filter: forward only signal-bearing spans — a `ratel.*` span name, or any attribute key under `gen_ai.*` / `ratel.*`. This is what lets Ratel share a provider with e.g. Langfuse + the Vercel AI SDK and ingest only the gen\_ai/ratel signal (the AI SDK's `gen_ai.*` spans + Ratel's own `ratel.search` / `execute_tool`), dropping the framework's `ai.*` wrapper noise. #### Parameters | Parameter | Type | | --------- | -------------- | | `span` | `ReadableSpan` | #### Returns `boolean` *** ### ratelSpanProcessor() > **ratelSpanProcessor**(`opts?`): `SpanProcessor` A `BatchSpanProcessor` over the Ratel OTLP exporter that forwards only the spans passing [RatelSpanProcessorOptions.spanFilter](#api-spanfilter) (default [ratelSignalFilter](#api-ratelsignalfilter)). Add it to your own provider's `spanProcessors` to send Ratel telemetry alongside another provider — no global side effects, no resource. Greenfield apps that want Ratel to own the provider should call [init](#api-init) instead. `enabled: false` returns a no-op processor before resolving endpoint/auth configuration. #### Parameters | Parameter | Type | | --------- | ------------------------------------------------------------- | | `opts` | [`RatelSpanProcessorOptions`](#api-ratelspanprocessoroptions) | #### Returns `SpanProcessor` *** ### ratelTraceExporter() > **ratelTraceExporter**(`opts?`): `OTLPTraceExporter` Build the OTLP `http/protobuf` trace exporter at the resolved Ratel endpoint. The standalone exporter for callers wiring their own span-processor; [ratelSpanProcessor](#api-ratelspanprocessor) batches over it. Carries no resource — the caller's provider owns `service.name`. #### Parameters | Parameter | Type | | --------- | --------------------------------- | | `opts` | [`InitOptions`](#api-initoptions) | #### Returns `OTLPTraceExporter` *** ### resolveOtlpConfig() > **resolveOtlpConfig**(`opts?`, `env?`): [`ResolvedOtlpConfig`](#api-resolvedotlpconfig) Resolve [InitOptions](#api-initoptions) into concrete exporter config. Pure and env-injectable so the endpoint/auth precedence is testable without a network. #### Parameters | Parameter | Type | | --------- | -------------------------------------------- | | `opts?` | [`InitOptions`](#api-initoptions) | | `env?` | `Record`\<`string`, `string` \| `undefined`> | #### Returns [`ResolvedOtlpConfig`](#api-resolvedotlpconfig) --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/typescript/telemetry-otlp/changelog > @ratel-ai/telemetry-otlp release history · [npm](https://www.npmjs.com/package/@ratel-ai/telemetry-otlp) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/ts-otlp) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `@ratel-ai/telemetry-otlp` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.1.1] - 2026-07-11 ### Added * On first setup, `TelemetryInitOptions.enabled: false` returns a no-op shutdown handle without endpoint or provider setup; once Ratel owns the provider, repeated calls return its original handle. `RatelSpanProcessorOptions.enabled: false` always returns a no-op processor. * `TelemetryInitOptions.spanFilter` lets the turnkey provider select spans without hand-building a `NodeTracerProvider`; omitting it preserves the export-everything default. * `init()` is idempotent to the provider it installed: repeated and module-reloaded calls return the exact original handle, while a foreign global provider still raises the composition error. If another `init()` wins the registration race, the loser now returns that Ratel-owned handle instead of raising. * Re-exports `API_KEY_ENV` from `@ratel-ai/telemetry` alongside `ENDPOINT_ENV`. ### Changed * `@opentelemetry/api` is now a peer + development dependency so the host and Ratel share one global API instance. The exporter, resources, semantic conventions, and trace SDK packages remain runtime dependencies, preserving the one-package turnkey install (npm/pnpm auto-install the peer; yarn needs an explicit `add @opentelemetry/api`). * `init()` shutdown is terminal: after `handle.shutdown()`, a later `init()` throws an already-shut-down error instead of silently returning the dead handle (call `trace.disable()` first to re-initialize). A shared handle's `shutdown()` stops export for all callers. ## \[0.1.0] - 2026-07-06 ### Added * Initial release: the `init()` OTLP exporter, split out of `@ratel-ai/telemetry` so the `ratel.*` vocabulary stays OTel-free (ADR-0015). `init()` wires an OTLP `http/protobuf` exporter to `RATEL_URL` (or `{ endpoint, headers }`) over a `NodeTracerProvider`, registers it as the global provider, and returns a shutdown handle. Depends on `@ratel-ai/telemetry` for the vocabulary and the pure OTLP config resolution; re-exports `resolveOtlpConfig`, `InitOptions`, and `ResolvedOtlpConfig` for convenience. * `ratelSpanProcessor()` / `ratelTraceExporter()` + the default `ratelSignalFilter`: a composable OTLP span-processor for multi-provider coexistence. OpenTelemetry's model is one provider with many span-processors, so a partner already running one (e.g. Langfuse + the Vercel AI SDK) adds `ratelSpanProcessor` to their provider's `spanProcessors` to dual-export to Ratel — forwarding only the `gen_ai.*` / `ratel.*` signal (overridable via `spanFilter`; `() => true` sends everything), so the framework's `ai.*` wrapper noise stays out of Ratel. ### Changed * `init()` refactored onto `ratelSpanProcessor` (still exports every span — it owns the provider) and now throws, pointing at `ratelSpanProcessor`, when a `TracerProvider` is already registered globally, instead of silently no-op'ing. --- # @ratel-ai/mcp-server Source: https://docs.ratel.sh/docs/core/packages/typescript/mcp-server `@ratel-ai/mcp-server` is the TypeScript package behind [Ratel Local](/docs/local). It connects upstream MCP servers to one searchable catalog and exposes Ratel's capability tools to Claude Code, Codex, Cursor, and other MCP clients. > **Start with Ratel Local** > > Claude Code and Codex can follow the [Ratel Local quickstart](/docs/local/quickstart) to > install the package, migrate an existing MCP setup, and verify capability search. > Cursor uses [manual MCP configuration](/docs/local/cursor). ## Entry points | Entry point | Purpose | | ------------------------ | --------------------------------------------------------------------------- | | `ratel-mcp` | Configure upstreams, serve the catalog, manage skills, and run the local UI | | `createMcpServer` | Expose a `ToolCatalog` as an MCP server from your own process | | `buildGatewayFromConfig` | Connect configured upstreams and build the catalog used by the server | Install the documented release with `npm install @ratel-ai/mcp-server@0.4.0`, or run its CLI with `npx -y @ratel-ai/mcp-server@0.4.0`. ## Operational guides | Task | Guide | | ---------------------------------------------- | ---------------------------------------------------------- | | add, inspect, edit, or remove upstreams | [MCP servers](/docs/local/mcp-servers) | | migrate native Claude Code or Codex entries | [Import or link](/docs/local/import-or-link) | | authorize HTTP or SSE upstreams | [OAuth upstreams](/docs/local/oauth) | | manage skills and the Claude Code preload hook | [Managing skills](/docs/local/managing-skills) | | manage configuration from the browser | [Browser UI](/docs/local/browser-ui) | | inspect local events and estimated savings | [Statusline & telemetry](/docs/local/statusline-telemetry) | | create the gateway from Node code | [Embed as a library](/docs/local/library) | | diagnose setup and recover config files | [Troubleshooting](/docs/local/troubleshooting) | The package is published on [npm](https://www.npmjs.com/package/@ratel-ai/mcp-server), with source in [ratel-ai/ratel-local](https://github.com/ratel-ai/ratel-local). The identifiers `@ratel-ai/mcp-server` and `ratel-mcp` remain unchanged. --- # ratel-ai Source: https://docs.ratel.sh/docs/core/packages/python/ratel-ai > v0.4.2 · Python · [PyPI](https://pypi.org/project/ratel-ai/) · [source](https://github.com/ratel-ai/ratel/tree/main/src/sdk/python) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. ## Classes ### ExecutableTool ```python ExecutableTool(id: str, name: str, description: str, input_schema: dict[str, Any] = dict(), output_schema: dict[str, Any] = dict(), execute: Executor = None) ``` A `Tool` plus the handler that runs it. Registered into a `ToolCatalog`. **Attributes:** * `execute: Executor = field(default=None)` ### McpServerHandle ```python McpServerHandle(tool_ids: list[str], server_instructions: str | None, close: Callable[[], Awaitable[None]]) ``` What `register_mcp_server` returns: the registration's outcome. **Attributes:** * `tool_ids` (`list[str]`) — the namespaced `__` ids registered into the catalog, in upstream listing order. * `server_instructions` (`str | None`) — the `instructions` value passed to `register_mcp_server` (the caller reads it from its own `initialize` result), or `None`. * `close` (`Callable[[], Awaitable[None]]`) — async teardown — the `on_close` passed to `register_mcp_server`, or a no-op. The session itself stays caller-owned either way. ### SearchHit A single search result: the matched tool id and its relevance score. **Attributes:** * `score: float` — Relevance score; higher ranks first. The scale depends on the search method: raw BM25 (unbounded) for "bm25", cosine similarity for "semantic", reciprocal-rank-fusion for "hybrid" — scores from different methods are not comparable. * `tool_id: str` — Id of the matched tool, as passed to `register`. ### Skill ```python Skill(id: str, name: str, description: str, tags: list[str] = list(), tools: list[str] = list(), metadata: dict[str, list[str]] = dict(), body: str = '') ``` Skill metadata: what the index ranks and the capability tools surface. **Attributes:** * `body: str = ''` * `description: str` * `id: str` * `metadata: dict[str, list[str]] = field(default_factory=dict)` * `name: str` * `tags: list[str] = field(default_factory=list)` * `tools: list[str] = field(default_factory=list)` ### SkillCatalog ```python SkillCatalog(trace: TraceSinkConfig | None = None, method: SearchMethod = 'bm25') ``` Registry of skills. Register once, then search and load bodies by id. Create an empty skill catalog. **Parameters:** * `trace` (`TraceSinkConfig | None`) — where trace events go; `None` keeps the default no-op sink. * `method` (`SearchMethod`) — default retrieval method for `search` — see `ToolCatalog.__init__`; a semantic/hybrid catalog eagerly embeds each skill at registration. #### build\_embeddings ```python build_embeddings() -> None ``` Pre-compute embeddings for not-yet-embedded skills. See `ToolCatalog.build_embeddings` for when to call and what it raises. #### drain\_trace\_events ```python drain_trace_events() -> list[dict[str, Any]] ``` Drain captured trace envelopes; `[]` unless the sink is "memory". #### get ```python get(skill_id: str) -> Skill | None ``` Return the registered `Skill` for an id, or `None` if unknown. #### has ```python has(skill_id: str) -> bool ``` Return whether a skill with this id is registered. #### invoke ```python invoke(skill_id: str) -> str ``` Return a skill's body for dispatch, recording a `skill_invoke` event. Synchronous, unlike `ToolCatalog.invoke` — the body is already in memory, there is no handler to run. **Parameters:** * `skill_id` (`str`) — id of a registered skill. **Returns:** `str` — The skill's body (Markdown), verbatim as registered. **Raises:** * `ValueError` — on an unknown id — callers at the capability-tool boundary translate this into a structured error for the agent. #### record\_event ```python record_event(event: dict[str, Any]) -> None ``` Record a trace event into the catalog's sink. See `ToolCatalog.record_event` for the event contract. #### register ```python register(skill: Skill) -> None ``` Add a skill to the catalog (metadata into the index, body stored). Registering an id that is already present replaces it in place — the index never holds a duplicate. Name, description and tags are indexed for ranking; `tools`, `metadata` and `body` are stored but not indexed. **Parameters:** * `skill` (`Skill`) — the skill to register. **Raises:** * `RuntimeError` — on a semantic/hybrid catalog, if the embedding model fails to load while eagerly embedding the new skill. #### search ```python search(query: str, top_k: int, origin: SearchOrigin = 'direct', method: SearchMethod | None = None) -> list[SkillHit] ``` Rank registered skills against a natural-language query. The skill twin of `ToolCatalog.search` — same arguments, same method-override and `ValueError`/`RuntimeError` semantics, ranked against the skill corpus. **Returns:** `list[SkillHit]` — Up to `top_k` `SkillHit`s, best first. #### size ```python size() -> int ``` Return the number of registered skills. ### SkillHit A single skill search result: the matched skill id and its relevance score. The skill analogue of `SearchHit` (`tool_id` → `skill_id`). **Attributes:** * `score: float` — Relevance score; higher ranks first. Same method-dependent scale as `SearchHit.score`, computed against the skill corpus. * `skill_id: str` — Id of the matched skill, as passed to `register`. ### SkillRegistry ```python SkillRegistry() ``` Metadata-only BM25 index over the skill corpus (separate from tools). The on-demand analogue of `ToolRegistry`: a separate index, so skills are ranked independently of tools (own corpus statistics, own top-K). #### build\_embeddings ```python build_embeddings() -> None ``` See `ToolRegistry.build_embeddings`. #### drain\_trace\_events ```python drain_trace_events() -> list[dict[str, Any]] ``` Drain captured envelopes — see `ToolRegistry.drain_trace_events`. #### record\_event ```python record_event(event: dict[str, Any]) -> None ``` Record an SDK-layer trace event — see `ToolRegistry.record_event`. #### register ```python register(id: str, name: str, description: str, tags: list[str], tools: list[str], metadata: dict[str, list[str]], body: str) -> None ``` Register a skill's metadata into the index. Replaces in place when `id` is already registered. `tags` are indexed for ranking; `tools` and `metadata` ride along un-indexed for higher layers; `body` is the full instruction text, stored for on-demand load. #### search ```python search(query: str, top_k: int) -> list[SkillHit] ``` Lexical BM25 search over the skill corpus — see `ToolRegistry.search`. #### search\_with\_method ```python search_with_method(query: str, top_k: int, origin: str, method: str) -> list[SkillHit] ``` Search with an explicit method — see `ToolRegistry.search_with_method`. #### search\_with\_origin ```python search_with_origin(query: str, top_k: int, origin: str) -> list[SkillHit] ``` BM25 search tagged with who initiated it — see `ToolRegistry.search_with_origin`. #### set\_trace\_sink ```python set_trace_sink(kind: str, session_id: str | None = ..., path: str | None = ...) -> None ``` Route trace events to a sink — see `ToolRegistry.set_trace_sink`. ### Tool ```python Tool(id: str, name: str, description: str, input_schema: dict[str, Any] = dict(), output_schema: dict[str, Any] = dict()) ``` Tool metadata: what the index ranks and the capability tools surface. **Attributes:** * `description: str` * `id: str` * `input_schema: dict[str, Any] = field(default_factory=dict)` * `name: str` * `output_schema: dict[str, Any] = field(default_factory=dict)` ### ToolCatalog ```python ToolCatalog(trace: TraceSinkConfig | None = None, method: SearchMethod = 'bm25') ``` Registry + executors. Register tools once, then search and invoke by id. Create an empty catalog. **Parameters:** * `trace` (`TraceSinkConfig | None`) — where trace events go; `None` keeps the default no-op sink. * `method` (`SearchMethod`) — default retrieval method for `search` — "bm25" (the historical, model-free behavior), "semantic" or "hybrid". A per-call `method=` overrides it. A semantic/hybrid catalog eagerly embeds each tool at registration so searches never pay the embedding cost; a BM25 catalog never touches the model. #### build\_embeddings ```python build_embeddings() -> None ``` Pre-compute embeddings for any not-yet-embedded tools. Incremental: only tools registered since the last call are embedded. Call after a bulk register, or rely on the automatic per-register embedding that a semantic/hybrid catalog does. A BM25 catalog never needs it. **Raises:** * `RuntimeError` — if the embedding model fails to load. #### drain\_trace\_events ```python drain_trace_events() -> list[dict[str, Any]] ``` Drain captured trace envelopes; `[]` unless the sink is "memory". #### get ```python get(tool_id: str) -> Tool | None ``` Return the metadata-only `Tool` for an id, or `None` if unknown. #### get\_executable ```python get_executable(tool_id: str) -> ExecutableTool | None ``` Return the `ExecutableTool` (metadata plus handler) for an id, or `None`. #### has ```python has(tool_id: str) -> bool ``` Return whether a tool with this id is registered. #### invoke ```python async invoke(tool_id: str, args: dict[str, Any]) -> Any ``` Run a registered tool's handler and return its result. This is the canonical place that absorbs the sync/async executor difference: the handler is called first and the result awaited only if it is awaitable, so plain functions and `async def` executors (e.g. MCP/HTTP tools) are both supported. Callers must route invocations here rather than re-deriving that logic. Emits `invoke_start` / `invoke_end` / `invoke_error` trace events and wraps the call in an `execute_tool` OTel span (ADR-0007). **Parameters:** * `tool_id` (`str`) — id of a registered tool. * `args` (`dict[str, Any]`) — the arguments dict passed to the handler. **Returns:** `Any` — Whatever the handler returns (awaited if it returned an awaitable). **Raises:** * `ValueError` — if `tool_id` is not registered. * `Exception` — whatever the handler raises, re-raised after an `invoke_error` trace event is recorded. #### record\_event ```python record_event(event: dict[str, Any]) -> None ``` Record a trace event into the catalog's sink. **Parameters:** * `event` (`dict[str, Any]`) — a dict matching one of the core-owned `TraceEvent` shapes (ADR-0007), e.g. `{"type": "gateway_search", ...}`. **Raises:** * `ValueError` — if the dict doesn't match any known event shape. #### register ```python register(tool: ExecutableTool) -> None ``` Add a tool to the catalog (metadata into the index, handler by id). Registering an id that is already present replaces it in place — the index never holds a duplicate. **Parameters:** * `tool` (`ExecutableTool`) — the tool to register; `execute` must be set. **Raises:** * `ValueError` — if `tool.execute` is `None`, or if `input_schema` / `output_schema` contain values that are not JSON-serializable. * `RuntimeError` — on a semantic/hybrid catalog, if the embedding model fails to load while eagerly embedding the new tool. #### search ```python search(query: str, top_k: int, origin: SearchOrigin = 'direct', method: SearchMethod | None = None) -> list[SearchHit] ``` Rank registered tools against a natural-language query. **Parameters:** * `query` (`str`) — what the caller wants to do. * `top_k` (`int`) — max hits to return. * `origin` (`SearchOrigin`) — who initiated the search — labels the trace event only. * `method` (`SearchMethod | None`) — per-call override of the catalog's default retrieval method ("bm25" | "semantic" | "hybrid"). **Returns:** `list[SearchHit]` — Up to `top_k` `SearchHit`s, best first. **Raises:** * `ValueError` — if `method` is not "bm25", "semantic" or "hybrid". * `RuntimeError` — for "semantic"/"hybrid" when the embedding cache is not built (call `build_embeddings`) or query embedding fails. ### ToolRegistry ```python ToolRegistry() ``` Metadata-only BM25 index over `ratel-ai-core`. Executors and the capability-tool / MCP layers live in the pure-Python `ratel_ai` package above this binding. #### build\_embeddings ```python build_embeddings() -> None ``` Pre-compute embeddings for not-yet-embedded tools (incremental). A later semantic/hybrid search then only embeds the query. Raises `RuntimeError` if the model fails to load. The catalog calls this after `register` in semantic mode; BM25-only callers never do. #### drain\_trace\_events ```python drain_trace_events() -> list[dict[str, Any]] ``` Drain captured envelopes from the active sink. Returns `[]` unless the active sink is "memory". #### record\_event ```python record_event(event: dict[str, Any]) -> None ``` Record an SDK-layer trace event into the active sink. `event` must be a dict matching one of the core-owned `TraceEvent` shapes (ADR-0007, e.g. `{"type": "gateway_search", ...}`); anything else raises `ValueError`. #### register ```python register(id: str, name: str, description: str, input_schema: dict[str, Any], output_schema: dict[str, Any]) -> None ``` Register a tool's metadata into the index. Replaces in place when `id` is already registered. The schemas must be JSON-serializable dicts; anything else raises `ValueError`. #### search ```python search(query: str, top_k: int) -> list[SearchHit] ``` Lexical BM25 search: the top `top_k` tools for `query`, best first. Model-free and infallible; the trace event records origin "direct". #### search\_with\_method ```python search_with_method(query: str, top_k: int, origin: str, method: str) -> list[SearchHit] ``` Search with an explicit method ("bm25" | "semantic" | "hybrid"). "bm25" is infallible; "semantic"/"hybrid" rank against the prebuilt embedding cache and raise `RuntimeError` (`EmbeddingsNotBuilt`) if it isn't built — the model loads at `build_embeddings`, never inside a search. An unknown method string raises `ValueError`. #### search\_with\_origin ```python search_with_origin(query: str, top_k: int, origin: str) -> list[SearchHit] ``` BM25 search tagged with who initiated it. `origin` is "agent" (a model calling a capability tool) or anything else → "direct" (host code). The origin only labels the emitted trace event — ranking is identical to `search`. #### set\_trace\_sink ```python set_trace_sink(kind: str, session_id: str | None = ..., path: str | None = ...) -> None ``` Route trace events to a sink. `kind` is "noop" (drop everything, the initial state), "memory" (buffer for `drain_trace_events`; requires `session_id`) or "jsonl" (append to a file; requires `session_id` and `path`). Raises `ValueError` on an unknown kind, a missing required argument, or a jsonl path that cannot be opened. ### TraceSinkConfig ```python TraceSinkConfig(kind: str, session_id: str | None = None, path: str | None = None) ``` Where the catalog's trace events go. Mirrors the TS `TraceSinkConfig` union. kind: "noop" | "memory" | "jsonl". `session_id` is required for memory/jsonl; `path` is required for jsonl. **Attributes:** * `kind: str` * `path: str | None = None` * `session_id: str | None = None` ### UpstreamServerInfo ```python UpstreamServerInfo(name: str, description: str | None = None, instructions: str | None = None, tool_count: int | None = None, needs_auth: bool = False) ``` An upstream MCP server, as advertised in the capability-tool descriptions. Pass a list of these to `search_capabilities_tool` (or the deprecated `search_tools_tool`) to append a "this catalog aggregates…" listing to the tool description shown to the model, and to enrich each search result's server group with the upstream's description and instructions. **Attributes:** * `name` (`str`) — the catalog namespace of the server — the `` prefix of its `__` tool ids. * `description` (`str | None`) — what the server offers; compacted to one line in listings. * `instructions` (`str | None`) — the server's usage instructions (from its MCP `initialize` result), surfaced on matching server groups. * `tool_count` (`int | None`) — number of tools the server contributed, shown in listings. * `needs_auth` (`bool`) — True when the upstream rejected its boot connection with a 401 / re-auth needed; listings append "(auth required)". ## Functions ### configure\_telemetry ```python configure_telemetry(*, api_key: str | None = None, endpoint: str | None = None, headers: dict[str, str] | None = None, service_name: str | None = None, capture_content: ContentCapture | str | None = None, include_span_and_events: bool | None = None) -> Any ``` Register a Ratel-owned OTLP exporter (convenience wiring for the greenfield case). Ships the spans this SDK emits to Ratel Cloud (or any OTLP endpoint) by delegating to `ratel_ai_telemetry.init`, which needs the OpenTelemetry SDK — install it with `pip install 'ratel-ai[otlp]'`. A host already running its own OpenTelemetry provider should skip this (the SDK's spans flow to that provider) and add `ratel_span_processor` from `ratel_ai_telemetry`. `capture_content` / `include_span_and_events` opt into message/tool content capture in code via `set_content_capture`: `capture_content` sets an exact mode, `include_span_and_events` is bool sugar (True -> `SPAN_AND_EVENT`, False -> `NO_CONTENT`); `capture_content` wins when both are given. A provided option beats `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` (the env var is the fallback when neither is given, as in OTel). The returned handle's `shutdown()` clears the override, restoring env-driven behavior; the clear is generation-scoped, so a stale handle shutting down late never clobbers an override a newer `configure_telemetry`/`set_content_capture` call installed. **Parameters:** * `api_key` (`str | None`) — Ratel Cloud API key override; defaults to `RATEL_API_KEY`. * `endpoint` (`str | None`) — OTLP endpoint override; defaults to `RATEL_URL`. * `headers` (`dict[str, str] | None`) — Extra headers sent with every export request. * `service_name` (`str | None`) — `service.name` resource attribute; defaults per `init`. * `capture_content` (`ContentCapture | str | None`) — Exact content-capture mode to set (see above). * `include_span_and_events` (`bool | None`) — Boolean sugar for `capture_content` (see above). **Returns:** `Any` — A per-call shutdown handle (`handle.shutdown()` / `handle.force_flush()`), the same shape on every path. Attribute access delegates to the shared handle `init` returns; because that handle is shared across callers, shutting it down stops export for all of them. **Raises:** * `ModuleNotFoundError` — if the OpenTelemetry exporter is not installed (`pip install 'ratel-ai[otlp]'`). * `ValueError` — if `capture_content` is not a recognized mode — raised before any exporter is wired, so a bad option has no side effects. ### format\_upstream\_line ```python format_upstream_line(s: UpstreamServerInfo) -> str ``` Render one upstream server as a listing bullet for a tool description. Format: `- ( tools) (auth required)`, where the description is whitespace-collapsed and truncated to \~160 chars, and each trailing part appears only when the corresponding field is set. **Parameters:** * `s` (`UpstreamServerInfo`) — the upstream server to render. **Returns:** `str` — A single `- `-prefixed line, ready to join into the "this catalog aggregates…" listing. ### get\_skill\_content\_tool ```python get_skill_content_tool(catalog: SkillCatalog) -> ExecutableTool ``` Build the `get_skill_content` tool: load a skill's full body by id. The returned tool resolves `skillId` and answers `{"body": }` via `SkillCatalog.invoke` (which records a `skill_invoke` trace event). It never raises into the host: an unknown or non-string id comes back as a structured `{"error": ..., "isError": True}` payload the model can recover from, mirroring `invoke_tool`. **Parameters:** * `catalog` (`SkillCatalog`) — the skill catalog to load bodies from. **Returns:** `ExecutableTool` — An `ExecutableTool` to put in the agent's direct tool list alongside `search_capabilities`. ### invoke\_tool\_tool ```python invoke_tool_tool(catalog: ToolCatalog, *, on_unauthorized: OnUnauthorized | None = None) -> ExecutableTool ``` Build the `invoke_tool` tool: run any catalog tool by id. The returned tool resolves `toolId`, forwards the nested `args` object to `ToolCatalog.invoke`, and never raises into the host: an unknown id, malformed `args`, or a tool exception all come back as a structured `{"error": ..., "isError": True}` payload the model can recover from. A flattened call (arguments at the top level instead of nested under `args`) is tolerated. When the tool raises an `UnauthorizedError`, the payload is `{"error": "needs_auth", ...}` with a re-auth hint and, for namespaced `__` ids, the upstream server name. **Parameters:** * `catalog` (`ToolCatalog`) — the catalog to resolve and run tool ids against. * `on_unauthorized` (`OnUnauthorized | None`) — called with the upstream server name when a tool raises `UnauthorizedError`; may be sync or async. Skipped when the tool id has no `__` namespace prefix. **Returns:** `ExecutableTool` — An `ExecutableTool` to put in the agent's direct tool list. ### register\_mcp\_server ```python async register_mcp_server(catalog: ToolCatalog, *, name: str, session: Any, transport_label: str = 'unknown', instructions: str | None = None, on_close: Callable[[], Awaitable[None]] | None = None) -> McpServerHandle ``` Ingest an initialized MCP `ClientSession` into the catalog. **Parameters:** * `catalog` (`ToolCatalog`) — the catalog to register the upstream tools into. * `name` (`str`) — namespace prefix for tool ids (`__`). * `session` (`Any`) — an initialized `mcp.ClientSession` owned by the caller. * `transport_label` (`str`) — recorded on the `upstream_register` trace event. * `instructions` (`str | None`) — the upstream's server instructions (from `initialize`), if any. * `on_close` (`Callable[[], Awaitable[None]] | None`) — optional async teardown invoked by the handle's `close()`. **Returns:** `McpServerHandle` — An `McpServerHandle` with the registered tool ids. **Raises:** * `ImportError` — if the optional `mcp` package is not installed (`pip install 'ratel-ai[mcp]'`). ### search\_capabilities\_tool ```python search_capabilities_tool(catalog: ToolCatalog, skill_catalog: SkillCatalog | None = None, *, upstream_servers: Sequence[UpstreamServerInfo] | None = None) -> ExecutableTool ``` Build the `search_capabilities` tool: unified discovery over tools AND skills. The returned tool ranks two independent buckets, each with its own top-K budget — so a relevant skill is never starved out of the results by a large number of matching tools (and the two BM25 corpora are never score-compared). Tools land grouped by upstream server; a matched skill's declared tools are pulled into the tools bucket additively (score 0), so the agent gets the playbook and its toolkit in one turn. The `skills` bucket (and the mention of `get_skill_content` in the description) is only advertised when a non-empty `skill_catalog` is wired in. **Parameters:** * `catalog` (`ToolCatalog`) — the tool catalog to search. * `skill_catalog` (`SkillCatalog | None`) — optional skill catalog, ranked against the same query in its own bucket. * `upstream_servers` (`Sequence[UpstreamServerInfo] | None`) — upstream MCP servers to advertise in the tool description and to enrich result server groups with. **Returns:** `ExecutableTool` — An `ExecutableTool` to put in the agent's direct tool list. ### search\_tools\_tool ```python search_tools_tool(catalog: ToolCatalog, *, upstream_servers: Sequence[UpstreamServerInfo] | None = None) -> ExecutableTool ``` Build the pre-0.2.0 tools-only discovery tool (id `search_tools`). Keeps the original behaviour: a flat `{groups}` result and no skills bucket. New code should use `ratel_ai.search_capabilities_tool` instead. Registering both lets a host serve the old and new names during a migration window. **Parameters:** * `catalog` (`ToolCatalog`) — the tool catalog to search. * `upstream_servers` (`Sequence[UpstreamServerInfo] | None`) — upstream MCP servers to advertise in the tool description and to enrich result server groups with. **Returns:** `ExecutableTool` — An `ExecutableTool` to put in the agent's direct tool list. > **Deprecated** — Since 0.2.0: use `ratel_ai.search_capabilities_tool`. Tracked for > removal in RAT-250. ## Constants * `GET_SKILL_CONTENT_ID = 'get_skill_content'` — Id (and name) of the skill-loading tool built by `get_skill_content_tool`. * `INVOKE_TOOL_ID = 'invoke_tool'` — Id (and name) of the invocation tool built by `invoke_tool_tool`. * `SEARCH_CAPABILITIES_ID = 'search_capabilities'` — Id (and name) of the discovery tool built by `search_capabilities_tool`. * `SEARCH_TOOLS_ID = 'search_tools'` — Id (and name) of the deprecated pre-0.2.0 discovery tool built by `search_tools_tool`; superseded by `ratel_ai.SEARCH_CAPABILITIES_ID`. * `Executor = Callable[[dict[str, Any]], Union[Awaitable[Any], Any]]` — A tool handler: takes the tool's arguments dict, returns the result. May be sync or async (tool inputs are heterogeneous across the catalog); `ToolCatalog.invoke` absorbs the difference. * `OnUnauthorized = Callable[[str], Union[Awaitable[None], None]]` — Notified when the underlying tool raises `UnauthorizedError`, with the upstream server name inferred from the toolId. May be sync or async. * `SearchMethod = str` — Retrieval engine: `"bm25"` (lexical, model-free, the default), `"semantic"` (dense embeddings) or `"hybrid"` (both, fused). * `SearchOrigin = str` — Who initiated a search: `"direct"` (host code, the default) or `"agent"` (a model calling a capability tool). Labels the emitted trace event only — ranking is unaffected. --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/python/ratel-ai/changelog > ratel-ai release history · [PyPI](https://pypi.org/project/ratel-ai/) · [source](https://github.com/ratel-ai/ratel/tree/main/src/sdk/python) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `ratel-ai` (the Python SDK) are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.4.2] - 2026-07-11 ### Changed * `configure_telemetry()` returns a per-call shutdown handle (`handle.shutdown()` / `handle.force_flush()`) on every path — the no-override path no longer leaks `init()`'s shared provider directly. Because that provider is shared across callers, shutting one handle down stops export for all of them. ### Fixed * `configure_telemetry()` no longer mutates a shared provider's `shutdown` method. When idempotent telemetry initialization reuses one provider, a stale handle can no longer clear a newer caller's generation-scoped content-capture override. ## \[0.4.1] - 2026-07-10 ### Added * `configure_telemetry` opts into message/tool content capture programmatically: `capture_content` sets the exact `ContentCapture` mode (validated like the env var — case-insensitive, legacy boolean forms accepted — raising a `ValueError` on garbage before any exporter is wired), and `include_span_and_events` is boolean sugar (`True` → `SPAN_AND_EVENT`, `False` → `NO_CONTENT`). `capture_content` wins over `include_span_and_events`; when neither is provided, `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` keeps ruling (a provided option beats the env var, as in OTel code-over-env precedence). The returned provider's `shutdown()` restores env-driven behavior via a generation-scoped clear (`clear_content_capture`), so a stale handle shutting down late never clobbers an override a newer `configure_telemetry` installed. The `set_content_capture` / `clear_content_capture` / `ContentCapture` primitives live in `ratel_ai_telemetry`. ## \[0.4.0] - 2026-07-07 ### Added * **OpenTelemetry emission.** The SDK now opens an OTel span at each funnel boundary — `execute_tool` (`gen_ai.operation.name`, `gen_ai.tool.name`, `ratel.tool.args_size_bytes`), `ratel.search` (target, `top_k`, origin, `hit_count`), `ratel.skill.load`, `ratel.upstream.register`, and `ratel.auth.flow` — alongside the existing local `record_event` stream, which is unchanged. Emission is transparent and free by default: the `opentelemetry` API and the vocabulary are imported lazily, so the base (dependency-free) install is a pure pass-through no-op, and when OpenTelemetry is present the spans flow to whatever provider is registered. Built on the OTel-free `ratel_ai_telemetry` vocabulary, so the base install stays dependency-free. * `configure_telemetry(api_key=..., endpoint=..., headers=..., service_name=...)` convenience wiring, exported from `ratel_ai`: installs a Ratel-owned OTLP exporter (via the new `[otlp]` extra, `pip install 'ratel-ai[otlp]'`) that ships the SDK's spans to Ratel Cloud (or any OTLP endpoint) and returns the provider as a shutdown handle. Hosts already running OpenTelemetry skip it and add `ratel_span_processor` from `ratel_ai_telemetry` instead. * Message/tool content (`ratel.search.query`, `gen_ai.tool.call.arguments` / `.result`) rides span attributes only when `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` selects a span mode (`SPAN_ONLY` / `SPAN_AND_EVENT`); default off. ## \[0.3.0] - 2026-07-06 ### Added * `ToolCatalog` / `SkillCatalog` accept a default `method` (`"bm25"` | `"semantic"` | `"hybrid"`) and `search(query, top_k, origin=..., method=...)` takes a per-call override. `"bm25"` (default) is unchanged and model-free; `"semantic"` / `"hybrid"` load a local embedding model and raise `RuntimeError` if it fails to load. Exposed via the native `search_with_method` binding and the `SearchMethod` type. * A `"semantic"`/`"hybrid"` catalog embeds each tool/skill **eagerly at `register`** (incrementally), so searches never pay the corpus-embedding cost. New `catalog.build_embeddings()` pre-computes embeddings on demand (e.g. after a bulk register). BM25 catalogs do neither. A semantic/hybrid search on a catalog with no embeddings built raises `RuntimeError` (embeddings not computed) rather than embedding during the search. ## \[0.2.1-rc.1] - 2026-07-04 ### Changed * First release cut under the per-package release scheme (ADR-0008): `ratel-ai` now versions and ships independently of the core crate and JS SDK, tagged `sdk-py-v*`. No API changes since 0.2.0. ## \[0.2.0] - 2026-06-16 ### Changed * **BREAKING:** `search_tools_tool` → `search_capabilities_tool` (`SEARCH_TOOLS_ID` → `SEARCH_CAPABILITIES_ID`). It now returns two independently-ranked buckets — `{ tools, skills }`. Brings the Python SDK to parity with the TypeScript SDK. The old `search_tools_tool`/`SEARCH_TOOLS_ID` are kept as deprecated aliases (see *Deprecated*), so `0.1.x` code keeps working after upgrading. ### Added * First-class **skills**: `SkillCatalog`, `Skill`, `get_skill_content_tool` (`get_skill_content`), and the native `SkillRegistry`/`SkillHit` — the on-demand skill analogue of the tool catalog, ranked by a separate BM25 corpus. * Skill–tool coupling: a `Skill` can declare a `tools` list, and `search_capabilities` pulls a matched skill's declared tools into the `tools` bucket — additively (beyond `topKTools`) and deduped against query hits — so the agent gets the playbook and the tools it needs in one turn. ### Deprecated * `search_tools_tool` and `SEARCH_TOOLS_ID`. They retain their pre-0.2.0 behaviour — a tools-only `{groups}` result and the `search_tools` id — so upgrading from `0.1.x` does not break existing callers. Migrate to `search_capabilities_tool`; the aliases will be removed in a future release. ### Fixed * Gateway error payloads carry `isError: True`; `invoke_tool` rejects a non-object `args` instead of forwarding stray top-level keys. * `search_capabilities_tool` validates `topKTools`/`topKSkills` (declared `integer`, positive): `0`, negative, `bool`, and `float` fall back to the default, matching the TypeScript SDK exactly. * `search_capabilities_tool` advertises the `skills` bucket and `get_skill_content` in its description only when a non-empty `SkillCatalog` is provided. ## \[0.1.6] - 2026-06-10 ### Added * Initial release of the Python SDK. Binds the Rust core (`ratel-ai-core`) via PyO3, distributed as prebuilt `abi3` wheels for darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu, and win32-x64-msvc — no Rust toolchain required to install. (`v0.1.5` shipped TS-only on 2026-05-10; the first release carrying Python is the next version bump.) Binding strategy locked in [ADR-0006](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0006-native-ffi-bindings.md). * Full feature parity with the TypeScript SDK (`@ratel-ai/sdk`): * `ToolRegistry` / `SearchHit` — metadata-only BM25 index (native). * `ToolCatalog` accepts a `trace` config (`noop` default, `memory`, or `jsonl`); captured events flow through the Rust core sink ([ADR-0007](https://github.com/ratel-ai/ratel/blob/main/docs/adr/0007-telemetry-two-streams.md)). Exposes `record_event`, `drain_trace_events`, and an `origin` argument on `search`. `invoke` emits `invoke_start` / `invoke_end` / `invoke_error` with `args_size_bytes` and `took_ms`, and awaits coroutine executors. * `search_tools_tool` / `invoke_tool_tool` gateway factories with verbatim descriptions and JSON schemas from the TS SDK. `search_tools_tool` emits `gateway_search` with `origin: "agent"`; `invoke_tool_tool` emits `gateway_invoke` / `gateway_error` and handles the `needs_auth` / `on_unauthorized` path. * `register_mcp_server` ingests an upstream MCP `ClientSession` (optional `mcp` extra, `pip install 'ratel-ai[mcp]'`), namespacing tool ids as `__` and emitting `upstream_register` / `upstream_invoke` / `upstream_error`. * Ships type stubs (`_native.pyi`, `py.typed`) for a fully typed install. --- # ratel-ai-telemetry (Python) Source: https://docs.ratel.sh/docs/core/packages/python/ratel-ai-telemetry > v0.1.2 · Python · [PyPI](https://pypi.org/project/ratel-ai-telemetry/) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/python) > > Generated from the doc comments at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. ## Classes ### AuthOutcome Outcome of an MCP auth flow. Carried by ratel.auth.outcome; NEEDS\_AUTH is the 401-driven AuthNeeds case (ADR-0007 auth\_needs). **Attributes:** * `FAILED = 'failed'` * `NEEDS_AUTH = 'needs_auth'` * `OK = 'ok'` * `REFRESHED = 'refreshed'` ### Origin Whether a ratel.\* span was a direct library call or synthesized by the agent inside its loop. Carried by ratel.origin; mirrors the local trace Origin (ADR-0007). A str-Enum: each member equals its exact wire string, so it is usable directly as an OTel attribute value. **Attributes:** * `AGENT = 'agent'` * `DIRECT = 'direct'` ### SearchTarget What a ratel.search span was searching. Carried by ratel.search.target; folds capability-tool search and skill search into one span shape. **Attributes:** * `SKILL = 'skill'` * `TOOL = 'tool'` ### ContentCapture Message/tool content capture modes for OTEL\_INSTRUMENTATION\_GENAI\_CAPTURE\_MESSAGE\_CONTENT (CONVENTIONS.md § Capture gating). Default off. **Attributes:** * `EVENT_ONLY = 'EVENT_ONLY'` * `NO_CONTENT = 'NO_CONTENT'` * `SPAN_AND_EVENT = 'SPAN_AND_EVENT'` * `SPAN_ONLY = 'SPAN_ONLY'` ### OtlpConfig ```python OtlpConfig(url: str, headers: dict[str, str], service_name: str) ``` Resolved exporter configuration; the pure core of init(). **Attributes:** * `headers: dict[str, str]` * `service_name: str` * `url: str` ### TelemetryHandle What init() returns: a shutdown handle, not a full provider. Both the enabled path (a live TracerProvider) and the disabled/no-op path satisfy this, so the return type is honest either way. Emit spans through the global OpenTelemetry API (`opentelemetry.trace.get_tracer(...)`), not off this handle — init() registers the provider globally. #### force\_flush ```python force_flush(timeout_millis: int = 30000) -> bool ``` #### shutdown ```python shutdown() -> Any ``` ## Functions ### init ```python init(*, api_key: str | None = None, endpoint: str | None = None, headers: Mapping[str, str] | None = None, service_name: str | None = None, span_filter: SpanFilter | None = None, enabled: bool = True) -> TelemetryHandle ``` Wire an OTLP http/protobuf exporter + batch processor + service.name resource, register it as the global tracer provider, and return a shutdown handle (call handle.shutdown() / handle.force\_flush()). Emit spans through the global OTel API (`opentelemetry.trace.get_tracer(...)`); everything else is the untouched OTel SDK. init() owns the global provider, so it exports every span by default (unlike ratel\_span\_processor, whose default gen\_ai.*/ratel.* filter exists for sharing a provider); pass span\_filter to narrow it. Repeated calls return the original Ratel-owned handle. It raises — pointing at ratel\_span\_processor — if a foreign provider is already registered globally. On first setup, pass enabled=False for an OTel-free no-op handle that needs no endpoint; once Ratel owns the provider, repeated calls return it regardless of options. Enabled initialization needs the \[otlp] extra and raises a clear error without it. Shutdown is terminal: after handle.shutdown(), a later init() raises rather than hand back the dead provider (OTel's global provider is set once per process). Note that the handle is shared across repeated calls, so shutting it down stops export for every caller. ### resolve\_otlp\_config ```python resolve_otlp_config(*, api_key: str | None = None, endpoint: str | None = None, headers: Mapping[str, str] | None = None, service_name: str | None = None, env: Mapping[str, str] | None = None) -> OtlpConfig ``` Resolve init() options into concrete exporter config. Accepts either api\_key= (falling back to RATEL\_API\_KEY; endpoint defaults to RATEL\_URL; Authorization: Bearer) or endpoint=/headers= (custom endpoint / collector). The forms compose: explicit endpoint/api\_key values win over their environment fallbacks. An explicit api\_key sets the Bearer header; the RATEL\_API\_KEY fallback applies only when neither api\_key nor an explicit Authorization header is given, so ambient env never clobbers auth the caller set on purpose. env is injectable so the precedence is testable without a network. ### content\_capture\_mode ```python content_capture_mode(env: Mapping[str, str] | None = None) -> ContentCapture ``` Parse the ecosystem content-capture gate. A mode set via set\_content\_capture wins outright (env is the fallback, as in OTel); otherwise defaults to NO\_CONTENT when unset/empty/unrecognized. The legacy boolean form maps true to full capture (SPAN\_AND\_EVENT) and false to none. ### set\_content\_capture ```python set_content_capture(mode: ContentCapture | str | None) -> int ``` Programmatically set the content-capture mode. While set, content\_capture\_mode() returns this mode regardless of OTEL\_INSTRUMENTATION\_GENAI\_CAPTURE\_MESSAGE\_CONTENT — programmatic config wins over the environment, matching how OpenTelemetry treats env vars as the fallback for code-level configuration. Pass None to clear the override unconditionally and return to env-driven parsing. The mode is validated like the env var (case-insensitive, legacy true/false/1/0 accepted) and raises a ValueError on anything unrecognized — failing loud at config time instead of storing a value that would both disable capture and mask the env var. Returns a generation token identifying this call as the current owner of the override; pass it to clear\_content\_capture to clear only if no newer set has happened since (the safe form for shutdown/teardown hooks). ### clear\_content\_capture ```python clear_content_capture(generation: int) -> None ``` Clear the programmatic content-capture override, but only when generation — the token returned by set\_content\_capture — still identifies the most recent set. A stale token no-ops, so an old handle shutting down late cannot clobber an override a newer caller installed (and silently re-enable, or disable, capture via the env fallback). For an unconditional clear, use set\_content\_capture(None). ### ratel\_signal\_filter ```python ratel_signal_filter(span: ReadableSpan) -> bool ``` Default span filter: forward only signal-bearing spans — a ratel.\* span name, or any attribute key under gen\_ai.\* / ratel.\*. This is what lets Ratel share a provider with e.g. Langfuse + the Vercel AI SDK and ingest only the gen\_ai/ratel signal (the AI SDK's gen\_ai.\* spans + Ratel's own ratel.search / execute\_tool), dropping the framework's ai.\* wrapper noise. ### ratel\_span\_exporter ```python ratel_span_exporter(*, api_key: str | None = None, endpoint: str | None = None, headers: Mapping[str, str] | None = None) -> SpanExporter ``` Build the OTLP http/protobuf span exporter at the resolved Ratel endpoint. The standalone exporter for callers wiring their own span-processor; ratel\_span\_processor batches over it. Carries no resource — the caller's provider owns service.name. Needs the \[otlp] extra; raises a clear error otherwise. ### ratel\_span\_processor ```python ratel_span_processor(*, api_key: str | None = None, endpoint: str | None = None, headers: Mapping[str, str] | None = None, span_filter: SpanFilter | None = None, enabled: bool = True) -> SpanProcessor ``` A BatchSpanProcessor over the Ratel OTLP exporter that forwards only the spans passing span\_filter (default ratel\_signal\_filter; pass `lambda _s: True` to forward everything). Add it to your own provider (`provider.add_span_processor(ratel_span_processor(...))`) to send Ratel telemetry alongside another provider — no global side effects, no resource. Greenfield apps that want Ratel to own the provider should call init() instead. Pass enabled=False for an OTel-free no-op processor that needs no endpoint. Enabled processors need the \[otlp] extra and raise a clear error without it. ## Constants * `CAPTURE_CONTENT_ENV: Final = 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT'` — The ecosystem instrumentation env var gating message/tool content capture. Default off; honored by init() rather than a Ratel-invented flag (CONVENTIONS.md § Capture gating). Values: legacy boolean, or the enum NO\_CONTENT (default) / SPAN\_ONLY / EVENT\_ONLY / SPAN\_AND\_EVENT. * `EXECUTE_TOOL: Final = 'execute_tool'` — execute\_tool — the gen\_ai.operation.name value for a tool invocation. Deliberately the standard OTel gen\_ai operation, not a bespoke ratel.invoke span, so a generic OTel backend already understands it (locked 2026-07-05). The invoke is enriched with ratel.\* attributes. * `GEN_AI_INFERENCE_DETAILS: Final = 'gen_ai.client.inference.operation.details'` — gen\_ai.client.inference.operation.details — the event that carries message text and tool-call content (never span attributes). Borrowed from gen\_ai (Tier 1). * `GEN_AI_OPERATION_NAME: Final = 'gen_ai.operation.name'` — gen\_ai.operation.name — set to EXECUTE\_TOOL for a tool invocation. * `GEN_AI_TOOL_CALL_ARGUMENTS: Final = 'gen_ai.tool.call.arguments'` — gen\_ai.tool.call.arguments — tool arguments (Opt-In content, gated). * `GEN_AI_TOOL_CALL_ID: Final = 'gen_ai.tool.call.id'` — gen\_ai.tool.call.id — tool call id, when available. * `GEN_AI_TOOL_CALL_RESULT: Final = 'gen_ai.tool.call.result'` — gen\_ai.tool.call.result — tool result (Opt-In content, gated). * `GEN_AI_TOOL_NAME: Final = 'gen_ai.tool.name'` — gen\_ai.tool.name — the capability tool id. * `RATEL_AUTH_FLOW: Final = 'ratel.auth.flow'` — ratel.auth.flow — MCP auth flow. * `RATEL_AUTH_OUTCOME: Final = 'ratel.auth.outcome'` — ratel.auth.outcome — "ok" / "refreshed" / "needs\_auth" / "failed" (see AuthOutcome). * `RATEL_ORIGIN: Final = 'ratel.origin'` — ratel.origin — direct library call vs agent-synthesized (shared attribute). * `RATEL_SEARCH: Final = 'ratel.search'` — ratel.search — capability search (unifies tool-search and skill-search). * `RATEL_SEARCH_HIT_COUNT: Final = 'ratel.search.hit_count'` — ratel.search.hit\_count — results returned. * `RATEL_SEARCH_QUERY: Final = 'ratel.search.query'` — ratel.search.query — the search text (content, gated like message content). * `RATEL_SEARCH_RESULTS: Final = 'ratel.search.results'` — ratel.search.results — Opt-In event carrying hit ids + scores + per-stage BM25 timing; gated like content. The ratel.search span itself carries only counts. * `RATEL_SEARCH_TARGET: Final = 'ratel.search.target'` — ratel.search.target — "tool" or "skill" (see SearchTarget). * `RATEL_SEARCH_TOP_K: Final = 'ratel.search.top_k'` — ratel.search.top\_k — requested result count. * `RATEL_SKILL_ID: Final = 'ratel.skill.id'` — ratel.skill.id — skill loaded on the ratel.skill.load span. * `RATEL_SKILL_LOAD: Final = 'ratel.skill.load'` — ratel.skill.load — skill content load (get\_skill\_content). * `RATEL_TOOL_ARGS_SIZE_BYTES: Final = 'ratel.tool.args_size_bytes'` — ratel.tool.args\_size\_bytes — argument payload size on the execute\_tool span. * `RATEL_UPSTREAM_REGISTER: Final = 'ratel.upstream.register'` — ratel.upstream.register — upstream-MCP ingest. * `RATEL_UPSTREAM_SERVER: Final = 'ratel.upstream.server'` — ratel.upstream.server — upstream MCP server backing a tool / auth flow. * `RATEL_UPSTREAM_TOOL_COUNT: Final = 'ratel.upstream.tool_count'` — ratel.upstream.tool\_count — tools ingested on register. * `RATEL_UPSTREAM_TRANSPORT: Final = 'ratel.upstream.transport'` — ratel.upstream.transport — "stdio" / "http" / "sse" / ... * `SEMCONV_VERSION: Final = '1.42.0'` — The pinned OpenTelemetry semantic-conventions version this vocabulary tracks (the gen\_ai group). The pin is the contract; consumers read against this exact version, never "latest" (CONVENTIONS.md § The pin). * `API_KEY_ENV = 'RATEL_API_KEY'` — Env var whose value is the default API key when api\_key= is omitted. * `ENDPOINT_ENV = 'RATEL_URL'` — Env var whose value is the default OTLP endpoint. * `DEFAULT_SERVICE_NAME = 'ratel'` — service.name used when the caller does not pass one. * `SpanFilter = Callable[['ReadableSpan'], bool]` — Predicate deciding whether a finished span is forwarded to Ratel. --- # Changelog Source: https://docs.ratel.sh/docs/core/packages/python/ratel-ai-telemetry/changelog > ratel-ai-telemetry release history · [PyPI](https://pypi.org/project/ratel-ai-telemetry/) · [source](https://github.com/ratel-ai/ratel/tree/main/src/telemetry/python) > > Synced verbatim from the package's `CHANGELOG.md` at `ratel-ai/ratel@3be0ecb` — do not edit by hand. Regenerate with `pnpm sync:api` in `apps/docs`. All notable changes to `ratel-ai-telemetry` (the Python telemetry helper) are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this package adheres to [Semantic Versioning](https://semver.org/). ## \[0.1.2] - 2026-07-11 ### Added * `API_KEY_ENV` (`RATEL_API_KEY`) and API-key environment fallback. Explicit `api_key=` remains authoritative; the env fallback applies only when neither `api_key=` nor an explicit `Authorization` header is given, so ambient `RATEL_API_KEY` never clobbers a caller-supplied auth header. * On first setup, `init(enabled=False)` returns an OTel-free no-op handle without endpoint configuration or the `[otlp]` extra; once Ratel owns the provider, repeated calls return it. `ratel_span_processor(enabled=False)` always returns a no-op processor. * `init(span_filter=...)` filters the turnkey provider, and repeated/module-reloaded calls return the exact Ratel-owned handle while foreign providers still raise. If another `init()` wins the registration race, the loser now returns that Ratel-owned handle instead of raising. * Public `TelemetryHandle` protocol describing `init()`'s return (`shutdown()` / `force_flush()`). ### Changed * `init()` is now typed to return a `TelemetryHandle` (shutdown handle), not a `TracerProvider` — the disabled/no-op path no longer masquerades as a full provider. Emit spans through the global OTel API (`opentelemetry.trace.get_tracer(...)`). * `init()` shutdown is terminal: OTel's global provider is set once per process, so after the handle's `shutdown()` a later `init()` raises instead of returning a provider whose exporter is dead. A shared handle's `shutdown()` stops export for all callers. ## \[0.1.1] - 2026-07-10 ### Added * `set_content_capture(mode)`: programmatic override of the content-capture gate. While set, `content_capture_mode()` returns the given mode regardless of `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` — code-level config wins over the environment, matching how OpenTelemetry treats env vars as the fallback for programmatic configuration. The mode is validated exactly like the env var (case-insensitive, trimmed, legacy `true`/`false`/`1`/`0` forms accepted) and raises a `ValueError` naming the valid values on anything unrecognized — failing loud at config time instead of storing a value that would both disable capture and mask the env var. Pass `None` to clear unconditionally. Returns a generation token identifying the call as the current owner of the override. * `clear_content_capture(generation)`: clears the override only when `generation` (the token returned by `set_content_capture`) still identifies the most recent set. A stale token no-ops, so an old telemetry handle shutting down late cannot clobber an override a newer caller installed and silently flip capture back to the env value. ## \[0.1.0] - 2026-07-06 ### Added * The telemetry helper (ADR-0015): the full `ratel.*` vocabulary as constants (attribute keys, span/event names, `gen_ai.*` interop keys, and the `Origin`/`SearchTarget`/`AuthOutcome` value enums) pinned to OpenTelemetry semconv `gen_ai` v1.42.0. * `init()` sugar over the OpenTelemetry Python SDK: wires an OTLP `http/protobuf` exporter to `RATEL_URL` (or `endpoint=`/`headers=`) with a `service.name` resource and batch processor, and returns the provider as a shutdown handle. `content_capture_mode()` reads the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` gate (default off). OTel deps pinned below 1.42 to keep Python 3.9 support. * Shared contract-against-the-pin conformance suite (`../conformance/fixtures.json`): spans built from the constants through the real SDK must emit the exact pinned keys. * Usage example in the README (runnable end-to-end in `examples/telemetry-python`). * `ratel_span_processor()` / `ratel_span_exporter()` + the default `ratel_signal_filter`: a composable OTLP span-processor for multi-provider coexistence. OpenTelemetry's model is one provider with many span-processors, so a partner already running one (e.g. Langfuse + the Vercel AI SDK) calls `provider.add_span_processor(ratel_span_processor(...))` to dual-export to Ratel — forwarding only the `gen_ai.*` / `ratel.*` signal (overridable via `span_filter`), so the framework's `ai.*` wrapper noise stays out of Ratel. Resolvable top-level via the lazy accessor (a plain `import ratel_ai_telemetry` still pulls no OTel). * A regression guard that a plain `import ratel_ai_telemetry` pulls no OpenTelemetry SDK. ### Changed * The OpenTelemetry SDK is an optional `[otlp]` extra: importing `ratel_ai_telemetry` pulls no OTel SDK (ADR-0015), so the SDK (emit), the server (read), and edge/serverless emitters take the `ratel.*` vocabulary weight-free. `init()` lives in the `ratel_ai_telemetry.otlp` submodule (behind the extra) and raises a clear "install `ratel-ai-telemetry[otlp]`" error when it is absent; a lazy top-level accessor keeps `from ratel_ai_telemetry import init` working. * `init()` refactored onto `ratel_span_processor` (still exports every span — it owns the provider) and now raises, pointing at `ratel_span_processor`, when a `TracerProvider` is already registered globally, instead of silently no-op'ing. * Released as an independent PyPI unit under the `telemetry-py-v*` tag prefix. --- # Ratel Cloud Source: https://docs.ratel.sh/docs/cloud Coming Soon Ratel Cloud will turn Ratel telemetry into a hosted view of context usage, retrieval quality, and agent performance. Until then, start with [Ratel Core](/docs/core) to add retrieval and telemetry to your agent. [Architecture](/docs/core/architecture) Follow one turn from local retrieval through capability search and execution. [Progressive disclosure](/docs/core/progressive-disclosure) Learn the search, invoke, and skill-loading contract shared across Ratel. --- # Introduction Source: https://docs.ratel.sh/docs/local Ratel Local is one MCP server for your whole tool estate. It connects to your upstream MCP servers and exposes a small, searchable capability surface to Claude Code, Codex, Cursor, or any other MCP client. Your client sees `search_capabilities` and `invoke_tool`, plus `get_skill_content` when skills are configured and `auth` for [authorizing OAuth upstreams](/docs/local/oauth). Upstream tool schemas stay out of context until a search makes them relevant. Ratel Local is the gateway host in [Ratel's architecture](/docs/core/architecture). The capability contracts belong to [progressive disclosure](/docs/core/progressive-disclosure); Ratel Local owns host setup, upstream configuration, authentication, and operation. [Quickstart](/docs/local/quickstart) Start fresh with the plugin, or migrate an existing Claude Code or Codex setup. [Cursor](/docs/local/cursor) Add Ratel Local to Cursor manually through its standard `mcp.json` configuration. [MCP servers](/docs/local/mcp-servers) Configure scopes, add upstreams, and verify the running gateway. [Import or link](/docs/local/import-or-link) Move native MCP entries into Ratel Local or connect a host without migrating them. [Browser UI](/docs/local/browser-ui) Manage local configuration in a loopback-only browser app. [Managing skills](/docs/local/managing-skills) Move agent skills behind retrieval and wire the prompt-time preload hook. [OAuth upstreams](/docs/local/oauth) Authorize remote servers, inspect status, and recover from a 401. [Statusline & telemetry](/docs/local/statusline-telemetry) Inspect local events and show estimated context savings in Claude Code. [Embed as a library](/docs/local/library) Build and serve a Ratel Local gateway from your own Node process. [Troubleshooting](/docs/local/troubleshooting) Recover from host, upstream, authentication, scope, and telemetry failures. ## Ratel Local or an SDK? Use Ratel Local when your tools already live behind MCP servers and you do not own the agent loop. Use a [Ratel SDK](/docs/core/sdks) when you want in-process catalog control, local function tools, or framework wiring. Both expose the same capability-tool contract. Ratel Local ships as [`@ratel-ai/mcp-server`](https://github.com/ratel-ai/ratel-local). The CLI binary is `ratel-mcp` and requires Node.js 20 or newer. --- # Quickstart: Claude Code & Codex Source: https://docs.ratel.sh/docs/local/quickstart Ratel Local gives Claude Code or Codex one searchable catalog instead of every tool schema from every upstream server. The fastest way to start is the plugin: install it, then add your upstreams. If you're using Cursor, follow the [manual Cursor setup](/docs/local/cursor). Ratel Local 0.4.0 does not provide a Cursor import or link adapter. ## Start fresh with the plugin Use this path when you want plugin-managed startup and are adding upstreams directly to Ratel. > **Already have MCP servers configured?** > > Migrate your existing native entries into Ratel Local instead of adding them by hand. > Jump to [Migrate an existing MCP setup](#migrate-an-existing-mcp-setup) below. ### Install Ratel Local from its marketplace **Claude Code** ```bash claude plugin marketplace add ratel-ai/ratel-local claude plugin install ratel-mcp@ratel ``` Run `/reload-plugins` in Claude Code, or restart it. **Codex** ```bash codex plugin marketplace add ratel-ai/ratel-local codex plugin add ratel-mcp@ratel ``` Then start a new session. Older Codex CLIs without `plugin add`: run `codex`, enter `/plugins`, and install **Ratel MCP** from the **Ratel** marketplace. The plugin starts `@ratel-ai/mcp-server@latest serve --auto-config` over stdio. ### Add an upstream to Ratel The plugin does not install the global CLI, so use `npx` for a zero-install setup: ```bash npx -y @ratel-ai/mcp-server@0.4.0 mcp add \ --scope user context7 -- npx -y @upstash/context7-mcp npx -y @ratel-ai/mcp-server@0.4.0 mcp list ``` Restart the host or start a new session after changing the configuration. The first `npx` launch may download packages and probe the upstream. If it stalls, see [First-run networking](/docs/local/troubleshooting#first-run-networking). ## Migrate an existing MCP setup Use this path when Claude Code or Codex already has one or more native MCP servers. ### Install the Ratel Local CLI Node.js 20 or newer is required. ```bash npm install --global @ratel-ai/mcp-server@0.4.0 ratel-mcp --version ``` The binary is `ratel-mcp`. The separate `ratel` command belongs to an older package and does not support the Codex-aware import flow. ### Import the host's MCP servers Run the wizard from the project where you use those servers: **Claude Code** ```bash ratel-mcp mcp import --agent claude-code ``` **Codex** ```bash ratel-mcp mcp import --agent codex ``` Select the upstreams to migrate, approve the Ratel config write, then approve replacing the selected native entries with a Ratel Local entry in each affected host scope. The wizard preserves scopes and stores backups under `~/.ratel/backups/`. See [Import or link](/docs/local/import-or-link) for dry runs, conflict strategies, and manual rollback. > **No native MCP servers yet?** > > Seed Context7 in the host, then run the import command again: > > **Claude Code** > > ```bash > claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp > ``` > > **Codex** > > ```bash > codex mcp add context7 -- npx -y @upstash/context7-mcp > ``` ### Confirm Ratel Local and restart **Claude Code** ```bash claude mcp get ratel-mcp ``` Look for `Status: ✔ Connected`, then restart Claude Code. **Codex** ```bash codex mcp get ratel-mcp --json ``` Confirm that the server is enabled, then start a new Codex session. ## Verify `search_capabilities` In either setup, ask the agent to make the capability call explicitly: ```text Call Ratel's search_capabilities tool with: {"query":"look up current React framework documentation","topKTools":3,"topKSkills":1} Return the raw result. ``` The host should show a `search_capabilities` call. The raw result includes tool hits grouped by upstream, a separate skills bucket, and each hit's complete input schema. See [Progressive disclosure](/docs/core/progressive-disclosure) for the authoritative request and response contract. You are now using Ratel Local. The host keeps the small capability-tool contract in context; upstream schemas stay behind Ratel until a search makes them relevant. ## Next steps [Manage MCP servers](/docs/local/mcp-servers) Add upstreams, understand scopes, and run Ratel Local yourself. [Import or link](/docs/local/import-or-link) Compare migration, host linking, conflict handling, and rollback. [Set up Cursor](/docs/local/cursor) Add the gateway to Cursor through its standard `mcp.json` configuration. [Troubleshoot a setup](/docs/local/troubleshooting) Diagnose missing tools, host restarts, authentication, and version drift. [See how Ratel Local fits](/docs/core/architecture) Follow a request from the host through retrieval, discovery, and invocation. [Use a Ratel SDK](/docs/core/quickstart#write-the-code-yourself) Use the TypeScript or Python SDK when you own the agent loop. --- # Cursor Source: https://docs.ratel.sh/docs/local/cursor Cursor can run Ratel Local as a standard stdio MCP server. The setup is manual: Ratel Local 0.4.0 has no Cursor import or link adapter. > **Do not use a Cursor agent flag** > > `ratel-mcp mcp import --agent cursor` and `ratel-mcp mcp link --agent cursor` > are not supported. Those commands only adapt Claude Code and Codex configuration. ## Add Ratel Local manually ### Add upstreams to Ratel Run from your project when you use project or local scope. ```bash npx -y @ratel-ai/mcp-server@0.4.0 mcp add \ --scope user context7 -- npx -y @upstash/context7-mcp npx -y @ratel-ai/mcp-server@0.4.0 mcp list ``` Use `--scope project` for shared project configuration or `--scope local` for private project overrides. See [MCP servers](/docs/local/mcp-servers#configuration-scopes). ### Add the Cursor MCP entry Create `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` to make the gateway available in every workspace. Follow Cursor's [MCP configuration reference](https://cursor.com/docs/mcp). ```json { "mcpServers": { "ratel-mcp": { "type": "stdio", "command": "npx", "args": [ "-y", "@ratel-ai/mcp-server@0.4.0", "serve", "--auto-config", "--project-root", "${workspaceFolder}" ] } } } ``` `--project-root` makes Ratel load the workspace's project and local configuration even when Cursor starts the process from another directory. User configuration always loads first; project and local entries override it by name. ### Restart and verify Open **Cursor Settings → Tools & MCP**. Confirm `ratel-mcp` is enabled and connected, then start a new chat. Cursor CLI users can inspect the same entry: ```bash cursor-agent mcp list cursor-agent mcp list-tools ratel-mcp ``` The tool list should include `search_capabilities` and `invoke_tool`. It also includes `get_skill_content` when Ratel loads at least one skill. ## Move existing Cursor servers Cursor's native MCP entries are not imported automatically. 1. Copy the current `.cursor/mcp.json` or `~/.cursor/mcp.json` somewhere safe. 2. Add each upstream to Ratel with `ratel-mcp mcp add`. 3. Add and verify the single `ratel-mcp` entry above. 4. Remove the migrated native entries from Cursor's `mcpServers` object. 5. Restart Cursor and begin a new chat. Leaving a migrated server both native and behind Ratel exposes duplicate tool schemas. Ratel's backup command does not cover Cursor files that you edit yourself. ## Verify the gateway Ask Cursor to call the discovery tool directly: ```text Call Ratel's search_capabilities tool with: {"query":"look up current React framework documentation","topKTools":3,"topKSkills":1} Return the raw result. ``` See [Progressive disclosure](/docs/core/progressive-disclosure) for the request and response contract. If Cursor shows the server as connected but the tools are missing, toggle `ratel-mcp` off and on, then start a new chat. Continue with [Troubleshooting](/docs/local/troubleshooting#gateway-absent-or-disconnected). ## Next steps [MCP servers](/docs/local/mcp-servers) Configure scopes, transports, and upstream entries. [Managing skills](/docs/local/managing-skills) Put reusable playbooks behind the same searchable gateway. [Troubleshooting](/docs/local/troubleshooting) Diagnose project-root, networking, version, and upstream failures. --- # MCP servers Source: https://docs.ratel.sh/docs/local/mcp-servers Ratel Local reads Claude-Code-shaped `mcpServers` configuration. It connects each upstream and registers its tools in one [Ratel catalog](/docs/core/architecture). Its server instructions list the aggregated upstreams and tell the model to search before falling back to a generic built-in capability. If one upstream fails to start, Ratel Local logs the failure and registers the rest. The gateway stays available. ## Configuration scopes Ratel Local merges three logical scopes. Later scopes win when the same server name appears more than once. | Scope | Path | Use it for | | --------- | ---------------------------------------- | ---------------------------------------------- | | `user` | `~/.ratel/config.json` | Servers available in every project | | `project` | `/.ratel/config.json` | Shared project configuration | | `local` | `/.ratel/config.local.json` | Private project overrides; add to `.gitignore` | The CLI defaults to `--scope user`. Use CLI commands instead of editing these files by hand. Project and local commands discover the project root from the current directory. Ratel prefers the nearest ancestor tree containing `pnpm-workspace.yaml`; otherwise it walks for `.git`, `.mcp.json`, or `package.json`. Run from inside the intended project. At runtime, entries merge in this order: user → project → local. A later entry replaces an earlier entry with the same name. The old `--scope global` alias is rejected; use `user`. ## Add an upstream The command shape mirrors `claude mcp add`. ```bash # stdio ratel-mcp mcp add --scope user airtable \ -e API_KEY=xyz -- npx -y airtable-mcp-server # HTTP ratel-mcp mcp add --scope user stripe \ https://mcp.stripe.com --transport http # SSE with a header ratel-mcp mcp add --scope project events \ https://example.com/sse --transport sse \ --header "Authorization: Bearer ${EVENTS_TOKEN}" ``` By default, `mcp add` connects once and stores the upstream's server instructions as its description. Pass `--description` to set one yourself or `--no-fetch-description` to skip the probe. For HTTP or SSE, skipping the probe also skips the initial OAuth attempt. `mcp add` refuses to replace a name in the target scope. Pass `--force` only when you intend to overwrite it; Ratel backs up the scope file first. Repeat `-e KEY=VALUE` for stdio environment variables or `--header "Name: Value"` for remote headers. ```json { "mcpServers": { "ev": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"], "description": "filesystem and shell utilities" }, "remote": { "type": "http", "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ${API_TOKEN}" } } } } ``` `type` defaults to `stdio`. HTTP and SSE entries expand `${VAR}` placeholders in `url` and `headers` from the process environment. Descriptions help the agent understand each upstream before searching. They are not sent over the upstream transport. ### Supported fields | Transport | Required | Optional | | ------------- | --------- | ----------------------------------------------------------------------------- | | `stdio` | `command` | `args`, `env`, `cwd`, `description` | | `http`, `sse` | `url` | `headers`, `description`, `clientId`, `clientSecret`, `callbackPort`, `scope` | Unknown transport types remain in configuration but are skipped with a warning when the gateway starts. HTTP and SSE OAuth fields are covered on [OAuth upstreams](/docs/local/oauth). ## Inspect and change servers ```bash ratel-mcp mcp list ratel-mcp mcp get airtable ratel-mcp mcp get airtable --scope user # Interactive when no field flags are supplied ratel-mcp mcp edit --scope user --name airtable ratel-mcp mcp edit --scope user --name airtable \ --description "Airtable CRUD" ratel-mcp mcp edit --scope user --name airtable \ --env API_KEY=new-value --env OLD_KEY= ratel-mcp mcp remove --scope user --name airtable ``` Without `--scope`, `mcp get` resolves the most specific matching entry: local, then project, then user. Mutating commands default to user scope and require `--name` for `edit` and `remove`. Field edits support `--description`, `--type`, `--command`, repeated `--arg`, repeated `--env KEY=VALUE`, `--cwd`, `--url`, and repeated `--header KEY=VALUE`. An empty env or header value deletes that key. Use `--entry-json` for an entire entry; it cannot be combined with field flags. Every write snapshots the affected configuration first. See [Import or link](/docs/local/import-or-link) for backup locations and host migration. Add `"skills": { "dirs": ["..."] }` at the top level to use custom skill directories. Without it, Ratel Local scans `~/.ratel/skills`. Prefer the commands on [Managing skills](/docs/local/managing-skills) when moving agent-owned skills. ## Run Ratel Local yourself Linked hosts and the plugin start Ratel Local automatically. Run it yourself when debugging or embedding it in another process. ```bash ratel-mcp serve --config ~/.ratel/config.json ratel-mcp serve ~/.ratel/config.json ratel-mcp serve --auto-config ratel-mcp serve --auto-config --project-root /path/to/project ``` `--auto-config` cannot be combined with `--config`. Without `--project-root`, it checks `RATEL_PROJECT_ROOT`, then `CLAUDE_PROJECT_DIR`, then walks up from the current directory. Auto-config always loads the user path. When it finds a project root, it then loads the project and local paths; a missing file is treated as an empty configuration. The startup log prints the resolved root and every path. On success, stderr includes: ```text [ratel] ready, N upstream server(s) configured ``` `N` is the number of configured entries, not the number that connected. Read the earlier stderr lines for `failed to register`, `requires authorization`, or `unsupported transport` messages. ## Verify the gateway ### Check the configuration Run `ratel-mcp mcp list`. Confirm each upstream appears with the expected transport and authentication status. ### Search from the host Ask the host to call `search_capabilities` with a query such as `read a file`. A result contains tool hits grouped by upstream server and a separate skills bucket. ### Invoke a hit Call `invoke_tool` with a returned `toolId` and the tool arguments nested under `args`. Unknown ids return a structured error without crashing the server. The full tool schemas and result shapes live on [Progressive disclosure](/docs/core/progressive-disclosure). At the MCP transport, every `tools/call` response JSON-serializes the result into `content[0].text`. Object results also appear in `structuredContent`. Failures return a structured `{ "error": "...", "isError": true }` payload instead of a protocol error. Ratel Local also registers the deprecated `search_tools` compatibility tool. New clients should use `search_capabilities`; the migration contract is on [Progressive disclosure](/docs/core/progressive-disclosure). If you own the agent process and want to ingest an MCP server into an in-process catalog, use the inverse SDK path for [TypeScript](/docs/core/typescript/register-mcps) or [Python](/docs/core/python/register-mcps). ## Next steps [Import or link](/docs/local/import-or-link) Move existing host entries into these scopes or add Ratel Local alongside them. [OAuth upstreams](/docs/local/oauth) Authorize HTTP and SSE servers and recover when credentials expire. --- # Import or link Source: https://docs.ratel.sh/docs/local/import-or-link Use `import` to move existing host servers behind Ratel Local. Use `link` when Ratel is already configured and you only need to connect the host. | Starting point | Command | Result | | ------------------------------------------- | ------------ | ---------------------------------------------------------------------------------- | | Native MCP servers should move behind Ratel | `mcp import` | Copies selected servers into Ratel, then replaces them in each affected host scope | | Ratel scopes are already configured | `mcp link` | Adds Ratel Local to applicable host scopes and leaves native servers untouched | | Fresh plugin setup | `mcp add` | Adds upstreams directly to Ratel; no host rewrite | > **Do not register Ratel Local twice** > > With version 0.4.0, installing the plugin and then accepting an import host rewrite > registers Ratel Local twice. Use CLI + import for migration or plugin + `mcp add` for a > fresh setup. ## Supported hosts and files Import and link have native adapters for Claude Code and Codex only. | Host scope | Claude Code | Codex | | ---------- | ---------------------------------------------------------- | ---------------------------------- | | User | `~/.claude.json` | `~/.codex/config.toml` | | Project | `/.mcp.json` | `/.codex/config.toml` | | Local | `projects[].mcpServers` in `~/.claude.json` | Not supported by the adapter | Without `--agent`, auto-detection chooses the first supported host with native entries: Claude Code, then Codex. Pass `--agent` when both are configured. Cursor and other MCP clients use [manual host configuration](/docs/local/cursor). ## Import native servers Run the wizard from the project where you use the native servers. ```bash ratel-mcp mcp import ratel-mcp mcp import --agent claude-code ratel-mcp mcp import --agent codex ratel-mcp mcp import --agent codex --dry-run ``` `--dry-run` prints planned writes and does not change files. The interactive flow may still start upstreams to read their instructions before it renders the plan. `--yes` selects every candidate, skips those probes and prompts, and keeps existing Ratel entries when names conflict. The wizard has two write stages: 1. Select native entries and move them into the matching Ratel scopes. 2. Replace those native entries with a `ratel-mcp` entry in each affected host scope. Each stage asks for confirmation. If you decline stage two, the new Ratel entries stay in place and the host remains unchanged. Run `mcp link` later to connect it. Entries you do not select stay native. Claude Code scopes are preserved. Codex preserves user and project scopes; its adapter has no local scope. When the same name appears at multiple scopes, the most specific candidate wins: local, then project, then user. ### Resolve name conflicts Choose how imports handle a server name that already exists in Ratel. | Strategy | Behavior | | -------------------- | ---------------------------------------------------- | | `add-missing-only` | Keep the Ratel entry and skip the incoming duplicate | | `replace-selected` | Ask which selected duplicates to replace | | `replace-from-agent` | Replace duplicates with the host versions | ```bash ratel-mcp mcp import --agent codex \ --conflict-strategy add-missing-only ``` `replace-selected` is interactive. It cannot be combined with `--yes` or `--dry-run`. > **Review converted entries before stage two** > > Ratel Local 0.4.0 runs `stdio`, `http`, and `sse`. Claude Code's > `streamable-http` alias and helper-specific fields are not normalized during import. > Codex-only controls such as `env_vars`, timeouts, tool filters, approval modes, and > OAuth tokens are not migrated. > > Stop after stage one, inspect with `mcp get`, correct > the entry, and reauthorize remote servers before rewriting the host. ## Before and after Stage one copies the selected entry into the matching Ratel scope: ```json { "mcpServers": { "context7": { "type": "stdio", "command": "npx", "args": ["-y", "@upstash/context7-mcp"] } } } ``` Stage two removes that native entry and adds one gateway entry. The generated command and absolute paths depend on your installation and scope chain. **Claude Code** ```diff { "mcpServers": { - "context7": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@upstash/context7-mcp"] + "ratel-mcp": { + "type": "stdio", + "command": "/absolute/path/to/ratel-mcp", + "args": ["serve", "--config", "/Users/you/.ratel/config.json"] } } } ``` **Codex** ```diff -[mcp_servers.context7] -command = "npx" -args = ["-y", "@upstash/context7-mcp"] +[mcp_servers.ratel-mcp] +command = "/absolute/path/to/ratel-mcp" +args = ["serve", "--config", "/Users/you/.ratel/config.json"] +enabled = true ``` ## Link without migrating ```bash ratel-mcp mcp link --agent claude-code ratel-mcp mcp link --agent codex ``` `link` writes a `ratel-mcp` entry in each applicable host scope. Each entry receives the correct configuration chain. Native MCP entries are not moved or removed. `link` supports `--yes` but has no dry-run mode. Use the interactive preview when you want to inspect every planned host write. Restart the host after an import or link. Then confirm the entry: **Claude Code** ```bash claude mcp get ratel-mcp ``` **Codex** ```bash codex mcp get ratel-mcp --json ``` ## Backups and rollback `add`, `edit`, `remove`, `import`, and `link` snapshot every file they change under a timestamped directory in `~/.ratel/backups/` before writing. ```bash ratel-mcp backup list ls -1 ~/.ratel/backups ``` `backup list` prints only the creation time, action, and file count. Open the matching `manifest.json` to find each `originalPath`, hashed `backupPath`, and `existedBefore` value. An import can create separate backup sets for its Ratel and host stages. There is no restore command. To roll back, stop the host, save the current file elsewhere, copy each required `backupPath` to its `originalPath`, and restart the host. When `existedBefore` is false, no older body exists. Save the new file elsewhere, then remove it to restore the original state. A failed multi-file stage restores files it already wrote, but it does not reverse an earlier completed import stage. ## Next steps [MCP servers](/docs/local/mcp-servers) Inspect scopes, add upstreams, and run the gateway directly. [Quickstart](/docs/local/quickstart) Follow the full Claude Code or Codex setup and verification path. [Cursor](/docs/local/cursor) Move Cursor entries manually and add one standard stdio gateway. [Troubleshooting](/docs/local/troubleshooting) Recover from conversion, scope, duplicate-registration, or rollback problems. --- # Browser UI Source: https://docs.ratel.sh/docs/local/browser-ui Ratel Local includes a browser UI for common configuration tasks. ```bash ratel-mcp ui ratel-mcp ui --port 5731 ratel-mcp ui --no-open ``` The default command chooses an available port on `127.0.0.1` and opens the browser. `--no-open` prints the URL instead. Stop the UI with Ctrl-C. Launch it from the project you want to manage. Project and local scopes are unavailable when Ratel cannot discover a project root from that directory. ## Walk through the UI ### Add or inspect an upstream Open **Tools**. Choose **Add source**, select user, project, or local scope, then configure stdio, HTTP, or SSE. Open an existing source to edit or remove it. Remote sources show an authorization action when OAuth is required. The flow uses the same local token store as `ratel-mcp mcp auth`. ### Import or link a supported host Open **Agent Links**, then choose Claude Code or Codex. Import previews native entries, scope placement, conflicts, and both write stages. Link previews the gateway entries without moving native servers. Review conflicts before applying. Cursor is not available here; follow the [manual Cursor guide](/docs/local/cursor). ### Manage skills and the statusline Open **Skills** to create a skill, move Claude Code or legacy Codex skill folders into Ratel management, edit a managed skill, or return it to its source. The Claude Code agent page can install or remove the Ratel-owned statusline. Codex and Cursor do not use this statusline integration. ### Verify and stop Confirm the expected sources and scopes, then restart the affected host. Keep the terminal process running while you use the UI; Ctrl-C closes the local server. ## What you can manage * View, add, edit, and remove MCP servers across user, project, and local scopes. * Start OAuth for remote upstreams. * Import MCP entries from Claude Code or Codex, or link either host to Ratel Local. * Create skills and move skills into or out of Ratel management. The UI has no restore action and does not copy snapshot contents back into place. Use `ratel-mcp backup list`, inspect the matching manifest, and follow the manual rollback steps on [Import or link](/docs/local/import-or-link#backups-and-rollback). ## Security boundary The server binds only to `127.0.0.1`. It does not listen on your LAN. The app shell and every API request require a per-session token embedded in the launch URL. Treat that URL as a credential for the lifetime of the process. Do not forward or publish it. Configuration writes use the same validation and backup path as CLI commands. ## Next steps [MCP servers](/docs/local/mcp-servers) Learn the scopes and configuration fields shown in the UI. [Managing skills](/docs/local/managing-skills) Understand activation, retrieval, and the Claude Code preload hook. [OAuth upstreams](/docs/local/oauth) Follow the token lifecycle and recover from authorization failures. --- # Managing skills with the CLI Source: https://docs.ratel.sh/docs/local/managing-skills Ratel Local serves skills through the same catalog as tools. The agent finds them with `search_capabilities` and loads a winner with `get_skill_content`. [Tools, MCP & Skills](/docs/core/tools-mcp-skills) owns the concept and retrieval model. This page covers the `ratel-mcp skill` commands that move folders and edit host settings. Ratel Local loads `//SKILL.md` from its configured skill directories. Set them with `skills.dirs`; otherwise it uses `~/.ratel/skills`. Frontmatter `name` and `description` are required. `tags`, `triggers`, and `stacks` are optional. Author-defined `triggers` join the indexed tags. Every CLI move is recorded in `~/.ratel/skill-manifest.json`, so it can be reversed. | Verb | Job | | --------------------------------- | -------------------------------------------------------------------- | | `activate` | move Claude Code / legacy Codex skills into the Ratel-managed folder | | `list` | show which skills Ratel currently manages | | `install-hook` / `uninstall-hook` | register / remove the prompt-time preload hook | | `preload-hook` | the hook entrypoint itself (run by Claude Code, not by you) | | `suggest` | rank skills for a prompt, to debug why one does or doesn't fire | | `deactivate` | move managed skills back where they came from | ## `skill activate` ```text ratel-mcp skill activate [--dry-run] [--yes] ``` Moves native skill folders from `~/.claude/skills` and `~/.codex/skills` into `~/.ratel/skills`. Each move is recorded in the manifest. > **Codex uses newer skill locations** > > Ratel Local 0.4.0 scans the legacy Codex folder `~/.codex/skills`. Current Codex > discovers personal skills from `~/.agents/skills` and project skills from > `.agents/skills`, so `activate` does not find them. > > To serve one through Ratel, move it > manually into `~/.ratel/skills` or another directory in `skills.dirs`, and record its > original path yourself. `deactivate` cannot reverse that manual move; do not leave a > second native copy behind. Ratel Local then serves those skills on demand instead of having the agent load all of them in every session. Claude Code is scanned first. A skill id found in both agents is taken from Claude Code and skipped for Codex. | Flag | Default | Effect | | ----------- | ------- | ------------------------------------------------- | | `--dry-run` | off | report what would move without touching any files | | `--yes` | off | skip the confirmation prompt | ```bash ratel-mcp skill activate --dry-run ``` ```text would activate deploy-runbook would activate debugging-checklist ``` Without `--dry-run`, the command confirms before moving. With nothing to do, it prints `no skills to activate (nothing new in ~/.claude/skills)`. A name already in the managed folder is skipped, never overwritten. The manifest is written atomically after each move. Activation always moves skills to `~/.ratel/skills`. Keep that directory in `skills.dirs` if you replace the default with custom directories. Run it after installing Ratel Local. Run it again whenever you add native skills. ## `skill list` ```text ratel-mcp skill list ``` Prints the manifest: one line per managed skill with where it came from. ```text Ratel manages 2 skill(s): deploy-runbook (from /Users/you/.claude/skills/deploy-runbook) debugging-checklist (from /Users/you/.claude/skills/debugging-checklist) ``` With an empty manifest it prints ``Ratel manages no skills (run `ratel-mcp skill activate`)``. Use it before deactivating skills or whenever you want to check what Ratel Local serves. ## `skill install-hook` / `skill uninstall-hook` ```text ratel-mcp skill install-hook [--scope user|project] [--yes] ratel-mcp skill uninstall-hook [--scope user|project] ``` `search_capabilities` is the pull path: the agent finds a skill when it searches. `install-hook` adds a push path for Claude Code. Its `UserPromptSubmit` hook runs `skill preload-hook` on every prompt and injects a pointer when one skill clearly wins. | Flag | Default | Effect | | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `--scope` | `user` | `user` targets `~/.claude/settings.json`; `project` targets `/.claude/settings.json` (errors when no project root is found) | | `--yes` | off | skip the confirmation prompt (install only) | ```bash ratel-mcp skill install-hook --scope user ``` It appends a `hooks.UserPromptSubmit` entry to the settings file: ```json { "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "ratel-mcp skill preload-hook", "timeout": 10 } ] } ] } } ``` The binary path resolves through `$RATEL_MCP_BIN`, then `PATH`, then an interactive prompt. Installation is idempotent and backs up the settings file before editing. Invalid settings JSON is refused rather than overwritten. `uninstall-hook` removes only entries carrying the `skill preload-hook` marker. Run `install-hook` once, right after your first `activate`. ## `skill preload-hook` ```text ratel-mcp skill preload-hook [--limit N] [--min-score X] ``` Claude Code runs this hook entrypoint on every prompt. You rarely invoke it yourself. It reads `prompt`, `cwd`, and `session_id` from stdin, then ranks skills against the prompt. It fires only for a clear lexical lead or a near-tie where exactly one skill matches the detected project stack. Otherwise it stays silent. | Flag | Default | Effect | | ------------- | ------- | ------------------------------------------------------------------------------------- | | `--limit` | `1` | widens the internal candidate pool only; the hook never points at more than one skill | | `--min-score` | `0` | drop hits below this raw lexical score | On a hit it writes the standard hook output to stdout: ```json { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Ratel: project-relevant skill(s) may apply to this task. Before writing code, load the relevant one(s) with the `get_skill_content` tool and follow them:\n- deploy-runbook — How to deploy: env vars, preview vs production, rollbacks. → get_skill_content(\"deploy-runbook\")" } } ``` Each skill is nudged at most once per session. Dedupe state lives under `~/.ratel/skill-preload/`. Project-stack detection is cached at `~/.ratel/skill-signal-cache.json`. Dependency manifest changes invalidate it. The hook fails open. Errors inject nothing and never block the prompt. ## `skill suggest` ```text ratel-mcp skill suggest --prompt "" [--cwd ] [--dir ]... [--limit N] [--min-score X] [--format json] ``` Runs the preload hook's ranking as a standalone debugging tool. Ranking uses [BM25](/docs/core/keyword-search) over each skill's name, description, and tags. The project stack from `--cwd` multiplies a matching skill's score by 1.6× and can reorder the results. The preload hook applies a separate clear-winner check before it injects a pointer. | Flag | Default | Effect | | --------------- | -------------------------------------------- | ---------------------------------------------------------------------- | | `--prompt` | required | the prompt text to rank against | | `--cwd` | — | project directory for stack detection | | `--dir` | config `skills.dirs`, else `~/.ratel/skills` | skill directory to rank; repeatable | | `--limit` | `5` | maximum results | | `--min-score` | `0` | drop hits below this raw lexical score (the stack boost doesn't count) | | `--format json` | text | print the raw `Suggestion[]` instead of lines | Without `--dir`, `suggest` reads `skills.dirs` only from `~/.ratel/config.json`, then falls back to `~/.ratel/skills`. It does not merge project or local config. Pass each project-specific directory explicitly with repeatable `--dir` to compare the same corpus that a project gateway serves. ```bash ratel-mcp skill suggest --prompt "build a login form" --cwd . --limit 3 ``` ```text auth-flows (score 2.31) How to add authentication: sessions, OAuth, form validation. ``` No hits prints `no matching skills`. With `--format json`, each hit includes `skillId`, `description`, `score`, and `stackMatch`. A true `stackMatch` means the ordering boost applied. ## `skill deactivate` ```text ratel-mcp skill deactivate [--dry-run] [--yes] ``` Restores each manifest-recorded skill to its source agent folder, then clears its manifest entry. Skills created directly in `~/.ratel/skills` stay put. Occupied destinations and missing sources are skipped, never overwritten. `--dry-run` and `--yes` behave as in `activate`. Run it when you want an agent to own its skills natively again, or to hand a skill back before editing it in place. > A corrupt `~/.ratel/skill-manifest.json` aborts `activate`, `deactivate`, and `list` with > an actionable error. Nothing moves until the manifest is fixed. ## A typical loop 1. `ratel-mcp skill activate` — move native skills behind the pull path: `search_capabilities` → `skills` → `get_skill_content`. 2. `ratel-mcp skill install-hook` — add the push path, so terse prompts still get a pointer to the single best skill. 3. `ratel-mcp skill suggest --prompt "..."` — dry-run ranking, then tune the skill's `description`, `tags`, or `triggers`. 4. `ratel-mcp skill list` / `ratel-mcp skill deactivate` — inspect what's managed, and reverse any move. Ratel Local also ships a browser UI with the same manage/stop-managing actions, bulk operations, and a new-skill form. See [Browser UI](/docs/local/browser-ui). ## Next steps [Tools, MCP & Skills](/docs/core/tools-mcp-skills) What a skill is, how ranking works, and how to write one that retrieves well. [MCP servers](/docs/local/mcp-servers) Configure upstream servers and the skill directories Ratel Local scans. [Progressive disclosure](/docs/core/progressive-disclosure) The `search_capabilities` / `invoke_tool` / `get_skill_content` surface your agent calls. --- # OAuth upstreams Source: https://docs.ratel.sh/docs/local/oauth HTTP and SSE upstreams can authorize through Ratel Local's loopback OAuth 2.1 and PKCE flow. `mcp add` starts the flow when the server requires it. ```bash ratel-mcp mcp add --scope user stripe \ https://mcp.stripe.com --transport http ``` Tokens persist at `~/.ratel/oauth/.json`. They are reused across host sessions. Ratel Local creates the directory with mode `0700` and token files with mode `0600` where the operating system supports those permissions. Treat the files as secrets. For an upstream without dynamic client registration, provide its client details: ```bash ratel-mcp mcp add --scope user private-api \ https://mcp.example.com --transport http \ --client-id "$OAUTH_CLIENT_ID" \ --client-secret "$OAUTH_CLIENT_SECRET" \ --oauth-scope "mcp:tools" ``` Use `--callback-port` when the provider requires a fixed loopback redirect port. > **Static client secrets are configuration** > > `--client-secret` is stored as plain text in Ratel config and can appear in config > backups. Restrict access to those files and prefer dynamic client registration when > the provider supports it. ## Check or renew authorization ```bash ratel-mcp mcp auth stripe ratel-mcp mcp auth ratel-mcp mcp auth --check ratel-mcp mcp list ``` `mcp auth ` tries a refresh token before opening a browser. Without a name, it authorizes every upstream currently marked as needing authentication. `--check` reports status from locally stored token metadata without contacting the provider. `mcp list` shows one of these values for each entry: | Status | Meaning | | ------------- | ------------------------------------------------------------------------------------------------ | | `ok` | An access token exists and its recorded expiry has not passed; this is not a live provider check | | `expired` | The recorded access-token expiry has passed | | `needs auth` | Start or repeat the authorization flow | | `unsupported` | Dynamic client registration was rejected; a configured static client may still work | | `n/a` | Authentication does not apply | At startup, Ratel Local checks stored tokens for HTTP and SSE upstreams. It refreshes a token within 60 seconds of expiry. A failed refresh clears the stored tokens so the next authorization starts cleanly. An access-token-only file cannot be refreshed at startup, even while its recorded access token is still fresh. Ratel Local marks that upstream as needing authentication. Run `ratel-mcp mcp auth ` to establish a complete token set. If dynamic client registration is unsupported, add the upstream again with `--client-id` and, when required, `--client-secret`. A named `mcp auth` attempt waits up to five minutes for the loopback callback after opening the browser. ## Recover from a 401 A live upstream 401 returns a structured `needs_auth` error from `invoke_tool`. The response includes a recovery hint and the upstream name when Ratel Local can infer it. The host can then call Ratel Local's `auth` capability tool. Pass an upstream name to authorize one server or omit it to handle every server that needs authentication. The tool returns a row per upstream with `authorized`, `skipped`, `failed`, or `unsupported`. Failed rows include the message in `reason` instead of becoming MCP protocol errors. The tool description lists the upstreams that currently need authentication. The model can see what to repair before it calls the tool. If a 401 persists: 1. Run `ratel-mcp mcp auth --check` and then `ratel-mcp mcp auth `. 2. Confirm the configured OAuth scopes and static client details match the provider. 3. Restart the Ratel Local gateway, then start a new host session so it reconnects. 4. Inspect [local telemetry](/docs/local/statusline-telemetry) for authorization and upstream errors. The shared capability result contract lives on [Progressive disclosure](/docs/core/progressive-disclosure). OAuth lifecycle events use the vocabulary on [Telemetry](/docs/core/telemetry). ## Use the browser UI The [Browser UI](/docs/local/browser-ui) can start the same upstream authorization flow. It remains bound to loopback and requires the session token in its launch URL. ## Next steps [MCP servers](/docs/local/mcp-servers) Configure HTTP and SSE transports and inspect upstream status. [Statusline & telemetry](/docs/local/statusline-telemetry) Find the local authorization events Ratel records. --- # Statusline & telemetry Source: https://docs.ratel.sh/docs/local/statusline-telemetry Ratel Local records local operational events as JSONL. Claude Code can use that stream to show session activity and estimated context savings in its statusline. [Ratel Core telemetry](/docs/core/telemetry) owns the event vocabulary and reliability contract. This page covers the files and commands Ratel Local operates. ## What gets written By default, `ratel-mcp serve` writes one JSON object per line to: ```text ~/.ratel/telemetry//.jsonl ``` `` is the gateway process's absolute working directory with `/` and `.` replaced by `-`. Each gateway launch creates a new timestamped session file. Events cover searches, invocations, upstream calls, authorization, and an estimate of each upstream's tool-definition payload. Control the sink at gateway startup: ```bash # Exact output file ratel-mcp serve --telemetry-file /secure/path/ratel.jsonl # Different root for per-project buckets RATEL_TELEMETRY_DIR=/secure/ratel-telemetry ratel-mcp serve --auto-config # Disable it ratel-mcp serve --telemetry off ratel-mcp serve --no-telemetry RATEL_TELEMETRY=off ratel-mcp serve --auto-config ``` The statusline discovers only the per-project bucket under `~/.ratel/telemetry`, or under `RATEL_TELEMETRY_DIR` when Claude Code inherits that variable. It does not discover an arbitrary `--telemetry-file`. > **Keep the project buckets aligned** > > The gateway writes to a bucket derived from its process working directory. The > statusline reads the bucket derived from Claude Code's `workspace.project_dir`. > > If the > paths differ, the statusline says `waiting for Ratel telemetry`. Start the gateway from > the project root, even when you also pass `--project-root`. ## Read the statusline `ratel-mcp statusline` reads Claude Code's statusline JSON from stdin and prints two rows. With ANSI colors removed, output resembles: ```text ● Claude Opus 50k / 200k · 25% 10m ⎇ main █████░░░░░░░░░░░░░ Ratel on saves ~1k (2 tools) ``` | Part | Meaning | | ---------------------------- | ------------------------------------------------------------------- | | dot and `Ratel on` | Claude Code has a Ratel MCP entry or enabled Ratel plugin | | model, context, time, branch | values from Claude Code's statusline payload and current Git branch | | bar | current context-window usage | | `saves ~…` | estimated upstream tool-definition tokens hidden behind Ratel | | `could trim ~…` | the same estimate while Ratel is not enabled in Claude Code | The savings number is an estimate, not measured provider billing. Ratel serializes the upstream tool definitions and estimates one token per four UTF-8 bytes. Older telemetry without payload size falls back to 130 tokens per tool. Missing telemetry prints `waiting for Ratel telemetry`. Malformed Claude input fails open with loading and unavailable lines; it does not block Claude Code. ## Install, replace, or remove it ```bash ratel-mcp statusline install ratel-mcp statusline install --force ratel-mcp statusline uninstall ``` Install and uninstall confirm before writing; pass `--yes` to skip confirmation. Install writes a command statusline with zero padding and a 30-second refresh interval to `~/.claude/settings.json`. It refuses to replace another tool's statusline unless you pass `--force`. Uninstall removes only a Ratel-owned setting. Other Claude Code settings and statuslines remain untouched. `mcp import` and `mcp link` also attempt installation after wiring Claude Code; they skip an existing non-Ratel statusline. ## Retention and privacy The JSONL files stay local; this sink does not send them over the network. They can still contain search text, tool identifiers, error details, and operational metadata. Protect the telemetry directory like other agent logs and review it before sharing. Ratel Local 0.4.0 does not prune old session files. Stop the gateway before deleting files you no longer need. Disabling the sink stops new local events; it does not delete existing files, so the statusline can continue showing the latest stored estimate. ## Next steps [Telemetry semantics](/docs/core/telemetry) Read the event schema, lifecycle, sampling, and reliability contract. [OAuth upstreams](/docs/local/oauth) Recover from authorization failures represented in the local event stream. [Import or link](/docs/local/import-or-link) See when host wiring installs the Claude Code statusline. --- # Embed as a library Source: https://docs.ratel.sh/docs/local/library `@ratel-ai/mcp-server` can run inside your process. Use it when you want Ratel Local's upstream aggregation but need to own startup, transport, or shutdown. This is the inverse of the SDK's `registerMcpServer`: the SDK ingests an MCP server into a catalog, while this package exposes a catalog as an MCP server. See the ingestion guides for [TypeScript](/docs/core/typescript/register-mcps) or [Python](/docs/core/python/register-mcps). ## Install ```bash pnpm add @ratel-ai/mcp-server@0.4.0 @modelcontextprotocol/sdk ``` The key entry points are summarized in the [`@ratel-ai/mcp-server` package reference](/docs/core/packages/typescript/mcp-server). ## Build and serve a gateway `buildGatewayFromConfig` connects the configured upstreams and builds their catalog. `createMcpServer` exposes that catalog's capability tools over an MCP transport. ```ts import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { buildGatewayFromConfig, createMcpServer, } from "@ratel-ai/mcp-server"; const gateway = await buildGatewayFromConfig({ mcpServers: { context7: { type: "stdio", command: "npx", args: ["-y", "@upstash/context7-mcp"], }, }, }); const server = await createMcpServer(gateway.catalog, { name: "my-gateway", version: "0.1.0", transport: new StdioServerTransport(), skillCatalog: gateway.skillCatalog, upstreamServers: gateway.upstreamServers, runAuthFlow: gateway.runAuthFlow, }); gateway.setListChangedNotifier(server.notifyToolListChanged); const shutdown = async () => { try { await server.close(); } finally { await gateway.close(); } }; process.once("SIGINT", () => void shutdown()); process.once("SIGTERM", () => void shutdown()); ``` The optional server fields mirror the CLI's `serve` wiring: * A non-empty `skillCatalog` registers `get_skill_content`. * `runAuthFlow` registers the `auth` capability tool. * `upstreamServers` lists aggregated upstreams in the server instructions. `buildGatewayFromConfig` degrades per upstream: it logs an unsupported transport, connection failure, or authorization requirement and continues building. Supply `logger` in its options to route those messages into your application's logging. `gateway.upstreamServers` contains connected upstreams plus entries marked `needsAuth`; unsupported and other failed entries are absent. It is not proof that every config entry started. ## Keep the tool list current The server handle exposes `notifyToolListChanged()`. Call it after a runtime change that alters the capability tools visible to the connected client. ## Close both layers `server.close()` closes the downstream MCP server and its transport. `gateway.close()` closes the upstream connections created by `buildGatewayFromConfig`. Always close both, and put `gateway.close()` in a `finally` block. Closing only the server can leave upstream child processes or network connections running. ## Next steps [MCP servers](/docs/local/mcp-servers) Learn the configuration shape consumed by `buildGatewayFromConfig`. [Architecture](/docs/core/architecture) Follow the shared catalog, retrieval, and invocation flow. [Progressive disclosure](/docs/core/progressive-disclosure) Read the capability-tool schemas exposed by the server. --- # Troubleshooting Source: https://docs.ratel.sh/docs/local/troubleshooting Start by separating three layers: the host must start Ratel Local, Ratel Local must load the intended config chain, and each upstream must connect. A green host entry proves only the first layer. ## Symptom map | Symptom | Likely cause | Start here | | -------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------- | | Two Ratel entries or duplicate tools | plugin and explicit host entry both enabled | [Double registration](#double-registration) | | Gateway is ready, but one upstream is absent | partial upstream startup | [Missing upstream](#one-upstream-is-missing) | | `ok` becomes a live 401 | local token metadata looked current, but provider rejected it | [Authentication failures](#authentication-failures) | | User servers load, project servers do not | wrong project root or start directory | [Project root](#wrong-project-root-or-config-scope) | | First `npx` launch stalls or exits | package download, proxy, certificate, or upstream probe | [First-run networking](#first-run-networking) | | A documented flag is rejected | another `ratel-mcp` version is executing | [Version mismatch](#version-mismatch) | | Config changed, tools did not | existing MCP connection or chat cached the tool list | [Restart requirements](#restart-requirements) | ## Gateway absent or disconnected Confirm Ratel's configuration, then run the same gateway manually from the project root: ```bash ratel-mcp mcp list ratel-mcp serve --auto-config --project-root "$PWD" ``` The second command occupies the terminal and writes diagnostics to stderr. Fix its first error before debugging the host. Stop it with Control-C before letting the host start its own copy. Inspect the host-facing entry: **Claude Code** ```bash claude mcp get ratel-mcp ``` Confirm it reports connected, then restart Claude Code. **Codex** ```bash codex mcp get ratel-mcp --json ``` Confirm it is enabled, then start a new Codex session. **Cursor** ```bash cursor-agent mcp list cursor-agent mcp list-tools ratel-mcp ``` Also check **Cursor Settings → Tools & MCP**. Toggle the entry off and on, then start a new chat. If the manual gateway starts but the host does not, compare the host's configured command, arguments, environment, and working directory with the successful terminal command. ## Double registration Ratel Local 0.4.0 can be started either by the Ratel plugin or by an explicit `ratel-mcp` host entry. Enabling both can launch two gateways and expose duplicate capability tools. Keep exactly one startup path: * Fresh setup: keep the plugin and add upstreams directly with `ratel-mcp mcp add`. * Migrated setup: keep the explicit entry written by `mcp import` or `mcp link`, and disable the plugin. * Cursor: keep one `ratel-mcp` entry across project and user `mcp.json` files. Duplicate tools can also come from duplicate upstream entries in one Ratel config — a different fix. Identify which gateway process serves the duplicates before removing anything, then fully restart the host. ## One upstream is missing Ratel Local continues when an upstream fails. The final message `ready, N upstream server(s) configured` counts config entries, not successful connections. Run the gateway manually and inspect lines before `ready`: ```bash ratel-mcp serve --auto-config --project-root "$PWD" 2>ratel-startup.log ``` | Log text | Action | | --------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `failed to register` | run the upstream command directly; fix its executable, args, `cwd`, or environment | | `requires authorization` / `needs re-authorization` | run `ratel-mcp mcp auth ` | | `unsupported transport` | change the entry to `stdio`, `http`, or `sse` | | no entry at all | inspect config resolution with [the project-root checks](#wrong-project-root-or-config-scope) | Then inspect the resolved entry: ```bash ratel-mcp mcp get ``` An import can preserve a host-specific transport alias or omit host-only controls. Review the converted entry after import stage one, before rewriting the host. See [Import fidelity](/docs/local/import-or-link#import-native-servers). ## Authentication failures `mcp list` and `mcp auth --check` read local token metadata; they do not call the provider. An `ok` row can therefore be followed by a live 401. ```bash ratel-mcp mcp auth --check ratel-mcp mcp auth ``` If authorization still fails: 1. Verify `clientId`, `clientSecret`, OAuth scope, and fixed callback port against the provider configuration. 2. If dynamic client registration is unsupported, supply the complete static-client entry with `mcp edit --entry-json`, or re-add the upstream. 3. Complete the browser callback within five minutes. 4. Restart the gateway and host so the upstream reconnects with the new token set. A failed startup refresh clears the unusable stored token. An access-token-only token file also needs a new authorization flow because Ratel cannot refresh it. See [OAuth upstreams](/docs/local/oauth) for token storage and 401 recovery. ## Wrong project root or config scope Ratel loads scopes in this order, with later entries replacing matching names: ```text ~/.ratel/config.json /.ratel/config.json /.ratel/config.local.json ``` Project discovery prefers the nearest ancestor with `pnpm-workspace.yaml`; otherwise it looks for `.git`, `.mcp.json`, or `package.json`. For `serve --auto-config`, an explicit `--project-root` wins, then `RATEL_PROJECT_ROOT`, `CLAUDE_PROJECT_DIR`, and the process working directory. Run from the intended project and read the startup lines listing the root and config paths: ```bash ratel-mcp serve --auto-config --project-root /absolute/path/to/project ``` Use absolute paths in host entries when workspace variables are unavailable. A project or local CLI mutation also depends on discovery, so run `mcp add`, `edit`, or `remove` from inside that project. ## First-run networking The first `npx -y @ratel-ai/mcp-server@0.4.0 ...` can download the package. `mcp add` also probes the new upstream for its description unless you pass one or use `--no-fetch-description`. Separate package access from upstream startup: ```bash npm view @ratel-ai/mcp-server@0.4.0 version npx -y @ratel-ai/mcp-server@0.4.0 --version ``` If either command fails, fix npm registry, proxy, DNS, or certificate access in the same environment the host inherits. If those pass but `mcp add` stalls, run the upstream command directly. Use `--no-fetch-description` only when you intentionally want to defer the connection and initial OAuth probe. For managed hosts, a first launch may need a restart after the download completes. Pin the same package version in every manual host entry to avoid fetching different releases. ## Version mismatch These pages document `@ratel-ai/mcp-server` 0.4.0. Find every binary that can win on `PATH` and compare it with any `npx` or plugin-managed command in the host config: ```bash which -a ratel-mcp ratel-mcp --version npx -y @ratel-ai/mcp-server@0.4.0 --version ``` If behavior differs, choose one installation, update the host entry to that command, and restart. Do not combine a plugin that launches its own package with an imported explicit entry. When debugging, keep `@ratel-ai/mcp-server@0.4.0` pinned rather than mixing a global binary, `@latest`, and 0.4.0. ## Restart requirements Most hosts cache MCP connections and their tool lists for a session. Restart after: * importing, linking, or manually changing a host MCP entry; * adding, editing, removing, or reauthorizing an upstream; * changing Ratel project scope, skills directories, or package version. **Claude Code** Exit and reopen Claude Code. After plugin installation, `/reload-plugins` can reload the plugin, but start a new conversation before verifying tools. **Codex** End the current session and start a new one. **Cursor** Toggle `ratel-mcp` in **Settings → Tools & MCP**, then start a new chat. Restart Cursor if the server process remains stale. Re-run an explicit `search_capabilities` call after restart. Host connection status alone does not prove every upstream joined the catalog. ## Recovery and deeper logs * Import or link problem: inspect `~/.ratel/backups//manifest.json` and follow the [manual rollback procedure](/docs/local/import-or-link#backups-and-rollback). There is no restore command. * Statusline says it is waiting: align the gateway working directory with Claude Code's project directory; see [Statusline & telemetry](/docs/local/statusline-telemetry). * OAuth or invocation failure: inspect the [local JSONL event stream](/docs/local/statusline-telemetry) (event meanings in [telemetry semantics](/docs/core/telemetry)), taking care not to share secrets or private query data. ## FAQ ### Do I need both the plugin and the CLI? No — they are alternative startup paths. The plugin starts the gateway itself and does not install the global CLI; run configuration commands with `npx -y @ratel-ai/mcp-server@0.4.0`. Install the global CLI for the [migration path](/docs/local/quickstart#migrate-an-existing-mcp-setup), and never keep both startup paths registered (see [Double registration](#double-registration)). ### Can native MCP servers stay alongside Ratel Local? Yes. Entries you do not migrate keep working natively; their schemas simply stay in the host's context without capability search. Just never leave the same server in both places — that exposes duplicate tool schemas ([Import or link](/docs/local/import-or-link)). ### How do I update Ratel Local? There is no self-update command. Reinstall the global package at the new version — imported host entries run that binary directly — and update the version pinned in manual host entries and `npx` commands to match; the plugin launches `@latest`, so it tracks releases on its own. Keep one version everywhere and restart the host ([Version mismatch](#version-mismatch)). ### Where does Ratel Local store its data? User-level state — configuration, managed skills, OAuth tokens, telemetry, and backups — lives under `~/.ratel/`; project and local scopes live in `/.ratel/`. The gateway runs on your machine and its telemetry sink writes local JSONL only; traffic still flows to the upstream servers you configure. See [Retention and privacy](/docs/local/statusline-telemetry#retention-and-privacy). ### Can I share one configuration across hosts and projects? Yes. Every documented startup path loads user scope `~/.ratel/config.json` first — the plugin and the Cursor entry through `--auto-config`, import and link entries through their generated `--config` chain — so Claude Code, Codex, and Cursor see the same user-scope upstreams. Add per-project servers at project or local scope; later scopes override earlier ones by name ([Configuration scopes](/docs/local/mcp-servers#configuration-scopes)). ### Is there a restore command? No. `ratel-mcp backup list` shows the snapshots under `~/.ratel/backups/`; rolling back is manual. Copy each `backupPath` in the set's `manifest.json` over its `originalPath`, and delete files whose `existedBefore` is false. Follow [Backups and rollback](/docs/local/import-or-link#backups-and-rollback). ### How do I remove Ratel Local completely? Reverse the moves first: `ratel-mcp skill deactivate`, `ratel-mcp skill uninstall-hook`, and `ratel-mcp statusline uninstall`. Then remove the `ratel-mcp` host entry or plugin, and restore any migrated native servers from backups ([rollback](/docs/local/import-or-link#backups-and-rollback)). Finally delete `~/.ratel/` and uninstall the global npm package if you installed one. ## Next steps [MCP servers](/docs/local/mcp-servers) Check configuration precedence, commands, and startup diagnostics. [Import or link](/docs/local/import-or-link) Review conversion limits, backups, and manual rollback. [OAuth upstreams](/docs/local/oauth) Inspect token status and repeat authorization safely.