@ratel-ai/mcp-server
v0.3.1
Published
MCP server library + ratel-mcp CLI: expose a Ratel tool catalog as a Model Context Protocol server. Manage upstream MCP servers, OAuth, and Claude Code import from one binary.
Maintainers
Readme
@ratel-ai/mcp-server is two things in one package:
- a library that takes a Ratel
ToolCatalogand exposes it as a Model Context Protocol server — the MCP client (Claude Desktop, an agent framework, an@modelcontextprotocol/sdkClient) seessearch_capabilities+invoke_tool(plusget_skill_contentwhen skills are configured) instead of every upstream's full tool list; - a CLI (
ratel-mcp) that drops the gateway between an MCP host (Claude Code, Cursor, ChatGPT) and an arbitrary set of upstream MCP servers — with Claude-compatible config UX, three-scope hierarchy, OAuth 2.1 / PKCE for HTTP+SSE upstreams, and a one-shotmcp importwizard for migrating an existing Claude Code MCP setup.
This is the inverse of @ratel-ai/sdk's registerMcpServer, which ingests an upstream MCP server's tools into a catalog. createMcpServer exposes a catalog as an MCP server.
Install
# CLI (global install)
pnpm add -g @ratel-ai/mcp-server
# Library (in a TS/Node project)
pnpm add @ratel-ai/mcp-server @ratel-ai/sdk @modelcontextprotocol/sdkOr skip the install and run the CLI on-the-fly:
npx -y @ratel-ai/mcp-server --helpAgent plugin marketplaces
The repo also ships a shared Ratel MCP plugin for Codex and Claude Code. It
starts the gateway over stdio with npx -y @ratel-ai/mcp-server@latest serve
--auto-config and bundles skills for setup, debugging, and tool-usage review.
Codex
Add the remote marketplace, then install Ratel MCP from the Ratel marketplace in Codex:
codex plugin marketplace add ratel-ai/ratel-mcpFor Codex local development from a checkout, run this from the repo root:
codex plugin marketplace add .Claude Code
Add the remote marketplace and install the plugin:
claude plugin marketplace add ratel-ai/ratel-mcp
claude plugin install ratel-mcp@ratelIf Claude Code is already running, restart it or run /reload-plugins inside
the session.
For Claude Code local development from a checkout, use . as the marketplace:
claude plugin marketplace add .
claude plugin install ratel-mcp@ratelCLI quickstart
ratel-mcp mirrors claude mcp add's flag layout — any invocation that works against Claude Code's CLI works here unchanged.
# Add an upstream (stdio)
ratel-mcp mcp add --scope user airtable -e API_KEY=xyz -- npx -y airtable-mcp-server
# Add an upstream (HTTP, with OAuth)
ratel-mcp mcp add --scope user stripe https://mcp.stripe.com --transport http
# List what's configured
ratel-mcp mcp list
# Import your existing agent MCP setup into ratel-mcp's scopes
ratel-mcp mcp import
ratel-mcp mcp import --agent codex
# Point an agent at the Ratel gateway without removing native MCP entries
ratel-mcp mcp link
ratel-mcp mcp link --agent claude-code
# Start the gateway over stdio (this is what linked agents spawn)
ratel-mcp serve --config ~/.ratel/config.jsonRun ratel-mcp <group> for the verbs in a group:
| Group | Verbs |
|---|---|
| mcp | add, remove, list, get, edit, import, link, auth |
| backup | list |
| (top-level) | serve, ui |
ratel-mcp mcp add — Claude-compatible
ratel-mcp mcp add [flags] <name> -- <command> [args...] # stdio
ratel-mcp mcp add [flags] <name> <url> # http / sse| Flag | Meaning |
|---|---|
| --transport stdio\|http\|sse | Force a transport. Inferred otherwise (URL → http, -- → stdio). |
| --scope user\|project\|local | Which scope to write to. Defaults to user. |
| --env KEY=VALUE / -e KEY=VALUE | Env var for stdio entries. Repeatable. |
| --header "Name: Value" | HTTP header for http/sse entries. Repeatable. |
| --client-id <id> / --client-secret <s> / --callback-port <n> / --oauth-scope <s> | OAuth client config for http/sse entries. DCR is preferred — pass --client-id only when the upstream doesn't support it. |
| --description <text> | Human description of the server. Wins over the auto-fetched upstream instructions. |
| --no-fetch-description | Skip the auto-probe — no connect, no description fetch, no OAuth flow. |
| --force | Overwrite an existing entry of the same name in the chosen scope. |
By default, mcp add connects to the upstream and stores its server-level instructions (per the MCP spec) as the entry's description. For http/sse upstreams it drives the OAuth 2.1 / PKCE flow inline (browser opens, tokens persist at ~/.ratel/oauth/<name>.json).
Three-scope hierarchy
ratel-mcp mirrors Claude Code's MCP scoping with three logical configs:
| Scope | Path | Notes |
|---|---|---|
| user | ~/.ratel/config.json | Per-user, applies everywhere. |
| project | <root>/.ratel/config.json | Committed alongside the repo. |
| local | <root>/.ratel/config.local.json | Per-user-per-project; add to your project's .gitignore. |
When you run ratel-mcp serve --config a.json --config b.json --config c.json, the configs are merged in order — last wins on mcpServers key collisions. The link command wires the right --config chain into Claude Code at each scope. The import wizard migrates selected native MCP entries into Ratel and can clean those imported entries out of the agent config as its second stage.
OAuth flow
HTTP and SSE upstreams that require OAuth authorization run through ratel-mcp's loopback PKCE flow. From the CLI:
ratel-mcp mcp add --scope user my-upstream https://mcp.example/mcp [--client-id <id>] [--callback-port <n>] [--oauth-scope "<s>"]— records the entry and drives the OAuth flow inline.ratel-mcp mcp auth my-upstream— refresh-first. If arefresh_tokenis on disk, rotates silently (no browser). Falls back to PKCE only when refresh fails.ratel-mcp mcp auth --check— read-only status report: tokens present, refresh availability, time-to-expiry.ratel-mcp mcp list— shows a single-line auth column per entry:ok/expired/needs auth/n/a.
When the gateway boots, every HTTP/SSE upstream with stored tokens runs through a proactive refresh. A 401 during a live invoke_tool returns { error: "needs_auth", upstream } so the agent can branch and call the auth MCP tool to recover.
Telemetry
ratel-mcp serve writes one JSON line per event to ~/.ratel/telemetry/<project-slug>/<ISO-ts>-<short>.jsonl by default — every search, invoke, gateway call, upstream MCP call, and OAuth event flows through the same JSONL (ADR 0009). Best-effort, sampleable, lossy on backpressure — query-log shaped, not oplog.
| Flag | Env | Purpose |
|---|---|---|
| --telemetry off | RATEL_TELEMETRY=off | Disable telemetry for this run. |
| --telemetry-file <path> | — | Override the JSONL path verbatim (no slugging). |
| — | RATEL_TELEMETRY_DIR | Override the default telemetry root. |
For summarizing the resulting JSONL stream, see @ratel-ai/cli's ratel inspect — it shares the on-disk format.
Backups
Every import, link, add, edit, and remove snapshots the files it touches into ~/.ratel/backups/<ISO>/ with a manifest.json. ratel-mcp backup list shows what's available.
Browser UI
ratel-mcp ui # starts a local UI on an ephemeral 127.0.0.1 port, opens your browser
ratel-mcp ui --port 5731 # bind a specific port
ratel-mcp ui --no-open # print the URL without launching a browserThe UI mirrors the CLI verbs across all three scopes: view/add/edit/remove servers, drive OAuth, import/link from Claude Code, and inspect backups. The server binds to 127.0.0.1 only and gates every request on a single-use session token printed in the launch URL. Stop it with Ctrl-C.
Library quickstart
import { ToolCatalog } from "@ratel-ai/sdk";
import { createMcpServer } from "@ratel-ai/mcp-server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const catalog = new ToolCatalog();
catalog.register({
id: "read_file",
name: "read_file",
description: "Read a file from local disk.",
inputSchema: { type: "object", properties: { path: { type: "string" } } },
outputSchema: { type: "object", properties: { contents: { type: "string" } } },
execute: async ({ path }) => ({ contents: await fs.readFile(path, "utf8") }),
});
const handle = await createMcpServer(catalog, {
name: "my-gateway",
version: "0.1.0",
transport: new StdioServerTransport(),
});
// later, on shutdown:
await handle.close();The MCP client connected to the other end will see search_capabilities and invoke_tool (and get_skill_content when skills are present). search_capabilities returns a tools bucket and a skills bucket. For backward compatibility the server also advertises the deprecated search_tools (its pre-0.2.0 tools-only result), so clients pinned to that name keep working; new clients should use search_capabilities. The catalog's tools are reachable through invoke_tool, never listed directly — that's the whole point (see ADR 0003 in ratel-ai/ratel).
buildGatewayFromConfig
Higher-level entrypoint that takes a parsed Ratel config (an mcpServers map mirroring Claude Code's shape) and spins up an upstream MCP Client per entry, registers each upstream's tools into a fresh catalog, and returns the catalog plus per-upstream metadata.
import { buildGatewayFromConfig, parseConfig } from "@ratel-ai/mcp-server";
const config = parseConfig(JSON.parse(await fs.readFile("./ratel-config.json", "utf8")));
const gateway = await buildGatewayFromConfig(config, {
logger: (m) => console.error(m),
});
// gateway.catalog -> ToolCatalog with every upstream tool registered
// gateway.upstreamServers -> [{ name, description?, toolCount }] for the search-tools description block
// await gateway.close() -> tears down every upstream clientIf any single upstream fails to start, buildGatewayFromConfig logs the failure and the rest still register — the gateway stays available. The handle exposes runAuthFlow() (refresh-first; PKCE fallback) for HTTP/SSE upstreams marked needsAuth, and setListChangedNotifier() so the MCP server can re-list after a successful flow.
Config shape
The config mirrors Claude Code's .claude.json mcpServers shape:
{
"mcpServers": {
"ev": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"],
"description": "filesystem & shell utilities"
},
"remote": {
"type": "http",
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer xyz" }
}
}
}type defaults to "stdio" when absent. description is optional metadata — used to seed the agent's awareness of each upstream via search_capabilities's description, never sent over the upstream transport. stdio and http are wired up by defaultTransportFactory; sse and unknown types are accepted by parseConfig but skipped at runtime by the default factory (provide your own factory for sse).
Result wrapping
Every tools/call response carries the gateway's return value as a JSON-serialized text block; plain-object returns are also surfaced as structuredContent:
{
"content": [{ "type": "text", "text": "{\"foo\":1}" }],
"structuredContent": { "foo": 1 }
}Arrays (e.g. the tool hits returned by search_capabilities) only travel in content[0].text, since MCP requires structuredContent to be a JSON object.
When invoke_tool drives a tool that was itself registered via registerMcpServer, the upstream's MCP-shaped result ({ content, structuredContent }) is nested inside our structuredContent one level deeper.
invoke_tool's and get_skill_content's error payloads ({ error: "...", isError: true } for unknown ids, bad args, or executor throws) are promoted to an MCP isError: true result by the server, so the host and model can tell a failed call from real content — the error field still carries the reason.
Examples
examples/claude-with-ratel/— Claude Code session fronted byratel-mcpas the only MCP server.
Build & test
pnpm install
pnpm build # tsc → dist/
pnpm typecheck
pnpm lint # biome
pnpm test # vitestCI runs all of the above on every PR.
License
MIT. Free to use, modify, and redistribute. See LICENSE.md.
Related
@ratel-ai/sdk— the TypeScript SDK withToolCatalog,searchCapabilitiesTool,invokeToolTool,registerMcpServer. Bundlesratel-ai-core(BM25 retrieval) via NAPI-RS.@ratel-ai/cli— the long-term Ratel artifacts CLI (telemetry inspection today).ratel-ai/ratel— overview, roadmap, ADRs, benchmark links.
