Attaching MCP Servers
Wire external MCP servers into the agent via `mcp/servers.json`
The agent attaches Model Context Protocol
servers at boot. Their tools land in the same ToolRegistry as the
built-in ones and ship with defer_loading: true by default (the
schema is only fetched when the model calls tool_search).
Transport is handled by the official
@modelcontextprotocol/sdk,
so the config format matches the standard mcpServers shape used by Cursor
and Claude Desktop. Three transports are supported: streamable-HTTP and SSE
(remote), and stdio (a local process the agent spawns).
Two locations
The server config is read from two places and merged:
- User-global:
<dataDir>/mcp/servers.json, default~/.minara/mcp/servers.json. Override the directory withMINARA_DATA_DIR. Personal credentials and the servers you want available in every repo. The legacy path<dataDir>/mcp.jsonis still read whenmcp/servers.jsonis absent, so existing setups keep working. - Per-project:
<repo>/.minara/mcp/servers.json. Resolved by walking up fromcwdto the nearest.minara/directory (sibling to.git/). Use this to pin a dev URL or a per-tenant secret for a single repo. The legacy<repo>/.minara/mcp.jsonis the same fallback.
On id collision the project entry wins, so a repo can override the
url, headers, or command for a server that's also declared globally.
Both files share the standard shape:
{
"mcpServers": {
"context7": {
"url": "https://mcp.context7.com/mcp",
"headers": { "CONTEXT7_API_KEY": "${CONTEXT7_API_KEY}" }
},
"exa": {
"url": "https://server.example.com/exa/mcp?api_key=${EXA_API_KEY}"
},
"grafana": {
"command": "uvx",
"args": ["mcp-grafana", "--disable-write"],
"env": {
"GRAFANA_URL": "https://grafana.example.com",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "${GRAFANA_TOKEN}"
}
},
"etherscan": {
"url": "https://mcp.example.com/etherscan/",
"timeoutMs": 60000,
"subagent": {
"systemPrompt": "You answer EVM block-explorer queries.",
"maxSteps": 10
}
}
}
}The JSON key is the server id (used as the mcp_<id>_* tool-name
prefix). An explicit "id" field inside the entry overrides the key
if you need to register the same server twice under different aliases.
Fields
An entry is either remote (url) or local (command).
| Field | Applies to | Notes |
|---|---|---|
url | remote | Streamable-HTTP endpoint, or SSE when the path ends in /sse. |
headers | remote | HTTP headers merged into every request. A credential is a ${VAR} reference, e.g. "Authorization": "Bearer ${MY_TOKEN}" or a custom header "X-Api-Key": "${MY_KEY}". |
command | local (stdio) | Executable to spawn, e.g. npx or uvx. |
args | local (stdio) | Arguments passed to command. |
env | local (stdio) | Environment for the spawned process, merged over a safe default env (PATH/HOME/…). Credentials are ${VAR} references. |
type | either | Optional transport hint (http / sse / stdio). Usually inferred and can be omitted. |
timeoutMs | either | Per-call timeout. Default 30000. |
label | either | Display name for logs. Defaults to the id. |
maxTools | either | Hard cap on registered tools per server. Default 64. |
include | either | Allowlist of remote tool names. Drops everything else. |
subagent | either | When set, the server's tools are wrapped into a single mcp_<id>_query tool driven by an internal agent loop. Use for fat servers where a persona + step budget keeps the main loop tidy. |
Invalid entries (neither url nor command, wrong types) are logged and
skipped — one bad entry won't keep the rest from attaching.
Secrets — ${VAR} references, never inline
Secrets never live in the config file as literal values. A credential is a
${VAR} reference, and VAR is resolved at connect time from:
- the
customslot ofcredentials.json— a secret you set in Settings -> API Keys (or via the agent'srequest_secretflow), then - the process environment (
process.env.VAR) as a fallback.
The custom slot wins, so a secret set through the UI applies on the next reconnect without exporting a shell variable. A reference works anywhere a credential goes — a header value, a URL query parameter, or a stdio env value:
{
"mcpServers": {
"my-server": {
"url": "https://api.example.com/mcp?api_key=${MY_SERVER_KEY}",
"headers": { "Authorization": "Bearer ${MY_SERVER_TOKEN}" }
}
}
}A reference that resolves nowhere is dropped (the header/env value is
omitted; a URL param becomes empty) so the server gets a clean
missing-credential rather than the literal ${VAR} text.
The tooling that writes this file (minara mcp add and the in-chat install
path) rejects a literal secret in a secret-named header or env var and
requires a ${VAR} reference, so a credential in those fields never reaches
disk through the agent. A secret placed in a URL query parameter can't be
reliably told apart from a normal parameter, so it is not rejected — always
use a ${VAR} reference there too.
Precedence
MCP_SERVERSenv — JSON array of server configs. Short-circuits both file locations. Used by tests and by container deployments where a secrets manager projects the blob in.- Merged server config — global
<dataDir>/mcp/servers.json+ per-project<repo>/.minara/mcp/servers.json(each falling back to the legacymcp.jsonat the same level). Project entries override global ones on id collision; entries with distinct ids are unioned. - v1 per-server URL env —
MCP_EVM_RPC_URL,MCP_ETHERSCAN_URL,MCP_SOLSCAN_URL,MCP_GOPLUS_URL. Each one is gated independently and is only consulted when tiers 1 and 2 are empty.
MCP_SERVERS=… stays reproducible even when a config file is on disk.
The env always short-circuits.
Adding a server from chat or the CLI
minara mcp add <id> <url> [--token-env ENV] connects to the server,
introspects its tools, and writes both a generated skill package and a
mcpServers entry. --token-env MY_TOKEN is sugar for a bearer header:
it writes "headers": { "Authorization": "Bearer ${MY_TOKEN}" } and
resolves the value from the custom slot (then the environment) both at
add time and at run time.
Observability
Boot logs publish under module: "app" with action mcp_initialized,
listing connected servers + tool counts. Per-server failures surface
as mcp_server_connect_failed.
In the REPL, pnpm dev -- status (or the corresponding HTTP gateway
endpoint) reports the live MCP status array.