Ratel Docs
TypeScript SDK

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 shutdown

A 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.

Next steps

On this page