MINARA

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 with MINARA_DATA_DIR. Personal credentials and the servers you want available in every repo. The legacy path <dataDir>/mcp.json is still read when mcp/servers.json is absent, so existing setups keep working.
  • Per-project: <repo>/.minara/mcp/servers.json. Resolved by walking up from cwd to 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.json is 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).

FieldApplies toNotes
urlremoteStreamable-HTTP endpoint, or SSE when the path ends in /sse.
headersremoteHTTP 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}".
commandlocal (stdio)Executable to spawn, e.g. npx or uvx.
argslocal (stdio)Arguments passed to command.
envlocal (stdio)Environment for the spawned process, merged over a safe default env (PATH/HOME/…). Credentials are ${VAR} references.
typeeitherOptional transport hint (http / sse / stdio). Usually inferred and can be omitted.
timeoutMseitherPer-call timeout. Default 30000.
labeleitherDisplay name for logs. Defaults to the id.
maxToolseitherHard cap on registered tools per server. Default 64.
includeeitherAllowlist of remote tool names. Drops everything else.
subagenteitherWhen 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:

  1. the custom slot of credentials.json — a secret you set in Settings -> API Keys (or via the agent's request_secret flow), then
  2. 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

  1. MCP_SERVERS env — 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.
  2. Merged server config — global <dataDir>/mcp/servers.json + per-project <repo>/.minara/mcp/servers.json (each falling back to the legacy mcp.json at the same level). Project entries override global ones on id collision; entries with distinct ids are unioned.
  3. v1 per-server URL envMCP_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.

On this page