Skip to content

Connecting to external MCP servers

Configure stdio/http/sse MCP servers, deferred loading, and the tool_search activation flow.

Revka is both an MCP server and an MCP client. As a client it can connect to any number of external Model Context Protocol servers — local processes over stdio, or remote services over HTTP or SSE — and surface their tools to the agent loop as if they were native Revka tools. This is how you add capabilities Revka doesn’t ship with: a filesystem server, a database server, an internal company tool, or any of the thousands of community MCP servers.

Read this page when you want to plug an external MCP server into your agent. It covers the [mcp] config section, the three transport types, how each external tool is wrapped, why tool schemas are loaded on demand, and how the model activates them with tool_search. If instead you want to expose Revka’s tools to other clients (Claude Code, Codex, Gemini CLI), see Revka as an MCP server.

When the daemon boots with [mcp] enabled = true, Revka reads each [[mcp.servers]] entry and connects to it. Connection is best-effort: if one server fails to start or respond, Revka logs the error and continues with the remaining servers — a broken server never blocks the agent. Each server’s initialize and tools/list results are read once, and every tool the server advertises is wrapped as a native Revka Tool and added to the registry.

To avoid name collisions across servers, every external tool is renamed <server_name>__<tool_name> — the server name from config, two underscores, then the tool’s own name. A read_file tool on a server named filesystem becomes filesystem__read_file.

The protocol is JSON-RPC 2.0 at MCP protocol version 2024-11-05.

External servers are declared in ~/.revka/config.toml under [mcp], with one [[mcp.servers]] block per server.

[mcp]
enabled = true
deferred_loading = true # default; load schemas on demand via tool_search
[[mcp.servers]]
name = "filesystem"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
env = { MCP_LOG = "debug" }
tool_timeout_secs = 120
[[mcp.servers]]
name = "remote-tools"
transport = "http"
url = "https://example.com/mcp"
headers = { "Authorization" = "Bearer mytoken" }
[[mcp.servers]]
name = "sse-server"
transport = "sse"
url = "https://example.com/sse"
KeyTypeDefaultMeaning
enabledbooleanfalseMaster switch for the MCP client. No external servers connect unless this is true.
deferred_loadingbooleantrueLoad tool schemas on demand via tool_search instead of putting every schema in the system prompt. Strongly recommended; see Deferred loading.
KeyTypeDefaultMeaning
namestringrequiredServer identifier; becomes the <name>__<tool> prefix on every tool.
transportstring"stdio"One of stdio, http, sse. See Transport types.
commandstring""Executable to spawn (stdio transport only).
argsarray[]Arguments passed to command (stdio only).
envtable{}Environment variables for the spawned process (stdio only).
urlstringunsetEndpoint URL (http and sse transports).
headerstable{}HTTP headers sent on every request (http and sse).
tool_timeout_secsinteger180Per-call timeout for this server’s tools. Hard-capped at 600.

The transport field selects how Revka talks to the server.

Spawns command (with args and env) as a child process and speaks JSON-RPC over its stdin/stdout. The child is terminated when the connection is dropped, so the process lifecycle is tied to the daemon. Each response line is capped at 4 MB.

[[mcp.servers]]
name = "filesystem"
transport = "stdio"
command = "/usr/local/bin/mcp-filesystem"
args = ["--read-only"]
env = { MCP_LOG = "debug" }

Use stdio for local tools you run alongside Revka — the most common case for community MCP servers distributed as npm or Python packages.

PhaseTimeout
Connect, initialize, and tools/list60 s (30 s at the transport layer)
Tool callstool_timeout_secs (default 180, hard max 600)

Each external tool is adapted into Revka’s tool model by McpToolWrapper, so it flows through the same registry, security policy, and agent loop as built-in tools — no special-casing downstream. The wrapper takes its name (<server>__<tool>), description, and parameter schema from the server’s initialize / tools/list response, and does three things on every call:

  • Strips the approved field. Revka’s security model injects an approved: bool argument into tool calls for supervised-mode gating. MCP servers don’t know about this field, so the wrapper removes it before forwarding to avoid an “unexpected argument” rejection.
  • Coerces string-encoded values. LLMs frequently emit "5" where the schema declares an integer, or "true" for a boolean. The wrapper coerces string-encoded numbers, booleans, arrays, and objects to the schema’s declared types before forwarding.
  • Never propagates errors. A failed MCP call returns a non-fatal ToolResult { success: false, ... } rather than an Err, so a misbehaving server surfaces as a tool error the model can react to, not a crash in the loop.

A single server can advertise dozens or hundreds of tools. If every schema went into the system prompt, the context window would balloon — and small or local models (Gemma, Llama) start hallucinating tool names once the set grows past ~100 definitions. Deferred loading solves this.

With mcp.deferred_loading = true (the default), external MCP tool schemas are not placed in the system prompt at startup. Instead the model sees a lightweight <available-deferred-tools> section listing only tool names, plus an instruction to call the built-in tool_search tool to load the full schema for any tool it wants to use. The tradeoff: deferred loading keeps the prompt lean at the cost of one tool_search round-trip before a new tool can be called. Eager loading (deferred_loading = false) skips that round-trip but spends context on every schema.

A small set of tools is loaded eagerly even under deferred mode, because the agent reaches for them constantly and a tool_search hop on every turn would be wasteful:

  • Local models keep an agent-lifecycle subset eager: create_agent, wait_for_agent, send_agent_prompt, get_agent_activity, list_agents, cancel_agent, resolve_outcome, get_workflow_context, save_plan, recall_plans, and compact_conversation (matched by the LOCAL_MODEL_EAGER_SUFFIXES suffixes).
  • The Operator seat (cloud or local) keeps that same lifecycle subset eager, plus the two Kumiho memory reflexes kumiho-memory__kumiho_memory_engage and kumiho-memory__kumiho_memory_reflect.

Everything else stays deferred behind tool_search.

Once a tool is activated, the model can usually call it by its bare suffix. If exactly one activated tool ends in a given suffix — say extract_text — and the model calls extract_text without the <server>__ prefix, Revka resolves it automatically. When the same suffix exists on two servers the reference is ambiguous and resolution fails, so the model must use the full prefixed name.

tool_search is the built-in tool that fetches full JSON schema definitions for deferred MCP tools and activates them for the rest of the conversation. It is how the model discovers and turns on external tools on demand.

ParameterTypeDefaultMeaning
querystringrequiredEither select:<name>[,<name>...] for exact selection, or free-text keywords for a ranked search.
max_resultsnumber5Maximum number of results returned in keyword-search mode.

Exact selection — prefix the query with select: and a comma-separated list of fully prefixed tool names to activate exactly those tools:

{ "query": "select:filesystem__read_file,git__status" }

Keyword search — pass free text; Revka ranks deferred stubs by how many query terms match and activates the best matches:

{ "query": "database query", "max_results": 3 }

tool_search returns a <functions>...</functions> block containing the full JSON schema for each activated tool — the same format the model reads native tools in. In select: mode, any names that couldn’t be resolved are appended as a Not found: x, y line. Once a tool is activated its schema persists for the conversation, so the model never needs to re-run tool_search for the same tool.

  1. The model reads a deferred tool’s name from <available-deferred-tools> (for example filesystem__read_file).

  2. It calls tool_search with { "query": "select:filesystem__read_file" } (or a keyword search).

  3. Revka returns the tool’s full schema in a <functions> block; the tool is now active.

  4. The model calls filesystem__read_file with real arguments. The schema stays active for the rest of the conversation.

Before committing a server to your config, you can verify it connects and lists tools. The gateway exposes a test endpoint that runs a full initialize + tools/list handshake and reports the result. This is the backend for the dashboard’s MCP editor Test button.

POST /api/mcp/servers/test
Authorization: Bearer <gateway-token>
Content-Type: application/json

Request body — the same shape as a [[mcp.servers]] entry, with an optional timeout_ms:

{
"name": "my-server",
"transport": "stdio",
"command": "/usr/bin/mcp-fs",
"args": ["--arg"],
"env": { "KEY": "val" },
"url": null,
"headers": {},
"timeout_ms": 30000
}

Success response:

{ "ok": true, "tool_count": 5, "tools": ["read_file", "write_file", "..."], "latency_ms": 234 }

Failure response:

{ "ok": false, "error": "connection refused", "latency_ms": 10 }

The handshake has a hard 10 s ceiling, and timeout_ms values below 1000 are clamped up to 1 second. For the full gateway-side MCP proxy and discovery routes, see MCP proxy & discovery API.