External MCP servers
The console is normally an MCP server producer: runspec serve publishes
each runnable as a leaf MCP tool for an outside agent to call. This page is about
the other direction — making the console's chat agent an MCP client, so tools
published by an external MCP server show up in the agent's toolset alongside
your runnables and the built-in tools.
Declare the servers in config.toml under [mcp]; the console connects to each
at launch and folds its tools in. External tool calls run through the same
autonomy / confirm gate as everything else, and — like Telemetry and Phoenix —
the feature is behind an opt-in extra.
Install the extra
pip install "runspec-console[mcp]"
Without it, any configured servers are simply skipped (the Settings hint and
mcp_status report the SDK is missing). It's also part of
runspec-console[all].
Configure servers
The easiest way is Settings → MCP: a tab that lists your servers with a live
connection status (green connected · N tools / red error with the reason /
disabled), an Add/edit dialog for each, a Reconnect button, a section
on/off switch, and the connect/call timeouts. The edit dialog has the transport
picker, the
transport-specific fields (command/args/env or url/headers), a credential
dropdown (from your Credentials tab), the credential-header override, the proxy,
per-server timeout overrides, and the autonomy level. Saving validates the entry,
writes config.toml, and reconnects immediately. Each row also has an inline
Enabled switch, so a server can be toggled without opening its editor.
Everything the tab writes is plain config.toml, so you can also hand-edit it.
Each server is a [[mcp.servers]] entry. Three transports are supported:
[mcp]
# enabled = true # section kill switch (default true)
# call_timeout = 60 # default seconds per tool call (all servers)
# connect_timeout = 20 # default seconds to wait for the initial connect (all servers)
# A local server started as a subprocess (stdio).
[[mcp.servers]]
name = "files"
transport = "stdio"
command = "uvx"
args = ["mcp-server-filesystem", "/srv/data"]
# env = { TOKEN = "…" } # added to the child's environment
# autonomy = "confirm" # confirm (default) | autonomous | manual
# A remote server over streamable-HTTP, authenticated from a named credential
# and reached through this network's proxy.
[[mcp.servers]]
name = "search"
transport = "http"
url = "https://mcp.example.com/mcp"
credential = "cred-search-token" # keychain-backed; becomes Authorization: Bearer
proxy = "http://proxy.corp:8080" # this server's egress (see "Per-server proxy")
call_timeout = 300 # override just this server's per-call deadline
# connect_timeout = 45 # …and/or its connect wait; omit to inherit [mcp]
# A remote server over SSE with a custom auth header.
[[mcp.servers]]
name = "wiki"
transport = "sse"
url = "https://wiki.example.com/sse"
credential = "cred-wiki"
credential_header = "X-API-Key" # send the secret raw under this header
Fields:
| Field | Applies to | Meaning |
|---|---|---|
name |
all | Short id; required. Namespaces the tools (mcp_<name>_<tool>). |
transport |
all | stdio, http (streamable-HTTP), or sse. Inferred from command/url if omitted. |
command / args / env / cwd |
stdio | The subprocess to spawn. env is added on top of the console's environment. |
url / headers |
http, sse | The server endpoint and any auth headers. |
enabled |
all | Per-server toggle (default true). Also an inline switch on the row. |
call_timeout / connect_timeout |
all | Per-server overrides (seconds) of the section defaults above. A positive value overrides just this server's per-call / connect deadline; omit (or 0) to inherit the [mcp] default. Lets one slow server get a longer leash without loosening the fleet. |
autonomy |
all | confirm (default), autonomous, or manual for this server's tools. |
credential |
all | A named-credential id (Settings → Credentials) — see Credentials. |
credential_header |
http, sse | Header the secret is sent under (default Authorization). |
header_credentials |
http, sse | Array of { header, credential } for a server needing more than one secret header — see Multiple secret headers. |
proxy |
all | Per-server HTTP CONNECT proxy — see Per-server proxy. |
How the tools appear
Each external tool is exposed to the agent as mcp_<server>_<tool> — e.g. a
search_issues tool on a server named github becomes mcp_github_search_issues.
The name is sanitised to the tool-name grammar and namespaced so it never
collides with a runnable or a built-in. The tool's own JSON-Schema input is
passed through unchanged, and its description carries a (via MCP server '…')
note so the agent knows the call leaves the machine.
In [llm] tool_mode = "search" the external tools join the deferred long tail
(reached via describe_tool / use_tool) rather than the always-inline kernel,
so a large server doesn't bloat every turn's tool list.
Autonomy — trust is opt-in
An external tool runs on someone else's server, so by default every call is
confirm-gated: the operator approves it (and can edit the arguments first),
exactly like a side-effecting runnable. Set autonomy = "autonomous" on a server
you trust to let its tools run without a prompt, or autonomy = "manual" to make
them human-only (the agent can never call them). This mirrors the console's
"safe by default, opt in to trust" posture everywhere else.
In an unattended trigger or schedule agent turn with a non-empty
action_allow list, external MCP tools are governed like the other action tools:
name a tool in action_allow to keep it, or it's dropped from that turn.
Credentials
Rather than pasting a token into headers or env, bind a named credential
(Settings → Credentials) to a server with credential = "<id>". The secret lives
in the OS keychain (Windows Credential Manager / macOS Keychain / Secret Service)
and is resolved only at connect time — the config file holds just the id, and the
secret never appears in the config, mcp_status, or the logs. How it's applied:
- http / sse — becomes an
Authorizationheader:Bearer <secret>for atoken/passwordcredential, HTTPBasicfor auserpassone. Setcredential_header(e.g.X-API-Key) to send the raw secret under a different header instead. A header you set explicitly inheadersalways wins. - stdio — injected as the credential's name-derived env vars (the same
convention the runnable credential channel uses — e.g. a
tokencredential labelledDeploysetsDEPLOY_TOKEN), so the server process reads its token from the environment.
Because only the id is stored, a bound credential is seed/sync-safe: the
credential's metadata (id, label, kind, username) rides Config Sync / the
config seed in runspec_credentials.toml, while each install supplies the actual
secret in its own keychain. In the Settings → MCP tab the server's credential
is a dropdown populated from mcp_credential_options() (id + label + kind, never
secrets).
Multiple secret headers
The single credential fills one header. A server that needs more than one
secret header — say an Atlassian MCP server wanting a Jira PAT and a Confluence
PAT under two different headers — uses header_credentials: an array of
{ header, credential } bindings, each sending its named credential's raw secret
under that header, resolved from the keychain at connect time.
[[mcp.servers]]
name = "atlassian"
transport = "http"
url = "https://mcp.atlassian.example/mcp"
[[mcp.servers.header_credentials]]
header = "X-Jira-PAT"
credential = "jira-pat" # a keychain credential id
[[mcp.servers.header_credentials]]
header = "X-Confluence-PAT"
credential = "confluence-pat"
The same keychain/seed/sync guarantees apply — only the header name and the
credential id are stored, never the token. A literal headers entry (or the
single credential) of the same name always wins, and a binding whose credential
can't be resolved is skipped rather than breaking the connect. In Settings → MCP,
add these under Secret headers (a header name + a credential dropdown per
row); the field shows for http/sse servers. http/sse only — a stdio server takes
its secrets as env vars via credential instead.
Per-server proxy
Servers can live on different corporate networks, so the proxy is per server,
not global. Set proxy to an HTTP CONNECT proxy URL (http://proxy:8080; the
scheme is optional) — it mirrors the console's [ssh] proxy and Config Sync's
proxy. For an http/sse server the console reaches the endpoint through that
proxy; for a stdio server it's exported to the subprocess as HTTPS_PROXY /
HTTP_PROXY / ALL_PROXY (so a local server that itself calls out uses it too).
Leave it unset to use the machine's default egress.
Config seed & sync
[mcp] lives in config.toml, which is one of the files carried by Config
Sync and the config seed — so a white-label / air-gapped install can ship a
baseline set of MCP servers, and a team can distribute them from one versioned
source. Bound credentials come along too: runspec_credentials.toml (metadata
only) is also a synced/seeded file, and secrets stay in each machine's keychain.
See Team config — sync & rooms and the config-seed walkthrough
for how the layers combine (seed < git sync < local edits, with a preserve list
for machine-local keys).
Checking status
The agent's per-turn console context lists the connected servers and their tool
counts, so it knows the tools exist. Programmatically, the bridge exposes
mcp_status() (availability + a per-server summary of connected state, tool
count, and any connect error) and mcp_reload() (reconnect after editing
[mcp]). A server that is slow or down at launch simply lands disconnected and
shows in the status — it never blocks the console from starting.
Code-graph MCP for large repos
The Repos feature (Settings → Repos) gives the agent on-disk checkouts and
the repo_* tools, but code exploration there is a stateless grep + read loop:
repo_grep re-walks the tree on every call, repo_read is a bounded file read.
On a large codebase, answering a structural question ("who calls X", "what's
the blast radius of changing Y", "what does this service look like") is many tool
round-trips and a lot of tokens.
A local code-graph MCP server targets exactly this: it parses the repo once,
resolves symbols, and stores functions / calls / imports as a graph, then answers
structural questions from pre-computed edges. The response scales with the
answer size, not the codebase size — so on a big repo it can replace a long
grep/read chain with a single sub-kilobyte call, while staying fully local
(no network, no API keys for code). It's opt-in and complementary to the
built-in repo_* tools — its tools appear alongside them as
mcp_<server>_<tool>, through the same autonomy gate.
One evaluated example is codebase-memory-mcp (MIT, stdio, a persisted
SQLite index in ~/.cache, with a file-change watcher so an uncommitted
repo_write edit re-indexes). See docs/design/repo-codebase-graph.md for the
evaluation, a Graphify contrast, and the deeper "incorporate it" direction.
Wire it up
One-click (recommended): Settings → MCP → Install into a venv…. Give it a
server name and the pip package (codebase-memory-mcp); the console creates a
dedicated venv (kept out of the console's own environment, under the app-data
mcp-venvs/ dir), pip installs the package into it, and adds a stdio
[[mcp.servers]] entry whose command points at the venv's launcher. Save →
mcp_reload connects it. This is a generic installer — it works for any
pip-distributed stdio MCP server, not just this one.
By hand: put the launcher on PATH (pip install codebase-memory-mcp) and
add the entry yourself:
[[mcp.servers]]
name = "codebase-graph"
transport = "stdio"
command = "codebase-memory-mcp"
autonomy = "confirm" # its query tools are reads; set "autonomous" to auto-run them
enabled = true
Then, in chat, have the agent index the checkout once (the path is
{app_dir}/repos/<name>; repo_status lists the registered repos) and ask a
structural question — it will use the graph tools instead of the grep loop.
Things to know
- Autonomy is per server, not per tool. A code-graph server usually mixes
read tools (queries) with write tools (
index_repository/delete_project), but they share oneautonomysetting.confirmgates all of them;autonomousauto-runs all of them. (The built-inrepo_*tools have a proper read/write split; an external server can't express that yet.) - Index footprint. A persisted graph is tens of MB per repo — fine for a few, a real budget for a fleet operator with many registered repos.
- Supply chain. A third-party binary reads all of your code. Pick a local, no-telemetry server, and treat installing one as the opt-in decision it is — the console never bundles or auto-installs one for you.