Register MCP servers
Ingest an MCP server's tools with registerMcpServer: namespaced ids, the returned handle, error propagation, and many servers on one ranked surface.
Hand the catalog a connected MCP transport and Ratel calls tools/list, registers each upstream tool under a server-namespaced id (<name>__<toolName>), 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:
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 shutdownA local server spawned as a subprocess uses the stdio transport instead — everything else stays the same:
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 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 <name>__<toolName> — 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 <id> 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. To make the aggregation visible, pass the upstream list to the search_capabilities factory:
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. Each entry renders one line in the tool's description, via the exported formatUpstreamLine helper, as - <name> — <description> (<n> 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 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.