Skip to content

MCP client

MCP (Model Context Protocol) lets Atomic Agent borrow tools from other programs. Point it at an MCP server — a GitHub helper, a database reader, a search service — and that server’s tools show up in the agent’s toolbox right next to the built-in browser and filesystem tools. The model can call them without you writing any glue code.

You configure servers in one place (config.json), Atomic Agent connects to them at startup, and their tools get qualified names like mcp.github.search_issues so they never collide with native tools.

What you can do with it

Add tools without code

Drop a server config into config.json. Its tools join the registry automatically — no rebuild, no plugin API.

Read resources & prompts

Beyond tools, the agent can list and read a server’s resources and fetch its prompt templates via built-in meta-tools.

Stay safe by default

MCP tools are approval-gated by default. Only servers you mark pure_read, and tools a server itself flags as read-only, skip the prompt. Hostile or noisy servers are fenced off.

Add servers live

In the TUI you can add or remove servers without restarting the runtime.

Quick start

Add an mcp.servers array to your config.json (under <stateDir>/config.json, default ~/.atomic-agent/):

{
"mcp": {
"servers": [
{
"name": "filesystem",
"enabled": true,
"transport": {
"kind": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
},
"description": "Local file server"
}
]
}
}

Restart the agent (or, in the TUI, add the server live with /mcp add). At startup the runtime connects every enabled server in parallel, discovers its tools, and registers them under mcp.filesystem.<toolName>. The model can now call them.

MCP servers are connected once, at startup. After you edit mcp.servers in config.json (by hand, with config set, or through PATCH /api/config), restart the agent or the serve process. PATCH /api/config only rewrites the file; it does not reconnect MCP servers. The TUI’s /mcp panel (add and remove) is the live path for mcp.servers.

How it works

Atomic Agent wraps the official @modelcontextprotocol/sdk in a single McpManager. On start(), the manager connects each enabled server, refreshes its catalog, wraps every discovered tool as a normal ToolDefinition, and registers it in the shared tool registry. From there an MCP tool behaves exactly like a native one — it flows through the same grammar, prompt descriptors, batching, and approval gates.

flowchart TD
    Config["config.json<br/>mcp.servers[]"] -->|bootstrap| Mgr["McpManager.start()"]
    Mgr -->|parallel, isolated| Conn["client.connect()<br/>per server"]
    Conn --> Cat["refreshCatalog()<br/>discover tools / resources / prompts"]
    Cat --> Reg["register mcp.<server>.<tool><br/>in ToolRegistry"]
    Reg --> Resolver["install trust-based<br/>resource-class resolver"]

    subgraph Invoke["Agent emits mcp.<server>.<tool>"]
        Emit["model tool call"] --> Class["resolve resource class<br/>(approval_gated | pure_read)"]
        Class --> Gate["approval gate / batch check"]
        Gate --> Call["client.callTool(rawName, args)"]
        Call --> Project["project heterogeneous<br/>MCP response → text + details"]
        Project --> Compress["compressToolResult<br/>(max 8 KB)"]
    end

    Reg -.-> Emit
    Resolver -.->|O(1) lookup| Class

Three things make this work cleanly:

  • Qualified names. Tools are registered as mcp.<server>.<rawName>. The server name is a kebab-case namespace (max 32 chars, no dots) so names never clash with native tools or each other.
  • Grammar & descriptors rebuilt on demand. When the first server connects, the runtime registers the MCP meta-tools and rebuilds the GBNF grammar plus tool descriptors so the model can actually emit the new names. Live-added servers trigger the same rebuild via refreshMcp().
  • Fail-closed trust. A dynamic resource-class resolver is installed at bootstrap even with zero servers, so unknown or hallucinated server names always land in the safe, approval-gated lane.

Configuring servers

Each entry in mcp.servers[] is an McpServerConfig:

FieldRequiredMeaning
nameyesUnique kebab-case namespace matching /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$/ — lowercase letters, digits and hyphens, at least 2 characters, max 32, starting and ending alphanumeric. Dots and colons are forbidden (they’d break GBNF tool-name literals and the mcp.<server>.<tool> splitter), so an npm-style package name is not a valid server name. Becomes the mcp.<name>.* prefix. A duplicate name is a hard config error — the runtime refuses to boot.
enablednoWhether to connect at bootstrap. Defaults to true, so a server you paste in without this key connects immediately. Set it to false to keep an entry on the shelf.
transportyesHow to reach the server — stdio, streamable_http, or sse (see below).
trustnoapproval_gated (default) or pure_read. Controls batching and approval.
envnoPer-server env var overrides (stdio only). Merged on top of the process env.
descriptionnoOne-liner shown in the TUI and logs.

Transports

Spawns a local process and talks to it over stdin/stdout. Best for tools you run on the same machine.

{
"name": "github",
"enabled": true,
"transport": {
"kind": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"cwd": "/optional/working/dir"
},
"env": { "GITHUB_TOKEN": "ghp_..." }
}

The cwd defaults to the agent’s working directory if omitted.

Because that inheritance is real, a token already in the agent’s environment reaches a stdio server without you copying it into config.json at all. That does not hold for the HTTP transports below.

Trust levels and safety

By default, every MCP tool is approval-gated: the model can’t run it without you approving the call (TUI modal, CLI prompt, Telegram or Discord button, or HTTP webhook). This is the safe default for tools you don’t fully control. MCP calls use the generic approval category, so at approval level 5 (--no-approval) they run without a prompt like everything else.

There are two exceptions to the prompt:

  • Servers you mark pure_read. If a server only reads data and you trust it, mark it pure_read. Its tools then opt into parallel batching alongside other read-only tools like os.fs.read: faster, but no approval prompt.
  • Tools the server itself flags as read-only. A tool whose MCP metadata carries annotations.readOnlyHint: true runs without an approval prompt even on a default (approval_gated) server. Only that exact flag counts; destructiveHint: false does not skip approval. Such tools still run solo, outside parallel batches, because batching follows the server’s trust.
{
"name": "docs-reader",
"enabled": true,
"trust": "pure_read",
"transport": { "kind": "stdio", "command": "my-docs-mcp", "args": [] }
}

The trust model is deliberately fail-closed:

  • An unspecified, disabled, failed, or absent server resolves to approval_gated, never pure_read. (A tool that a connected server annotates with readOnlyHint: true still skips the prompt, as described above.)
  • Approval-gated tools can’t ride in a multi-call batch. If the model tries, the batch is trimmed and it retries the call solo.
  • Even a hallucinated mcp.ghost.do_thing lands in the safe lane because the resolver is always installed.

Resources and prompts

Once any MCP server is connected, four read-only meta-tools become available to the agent (they’re not registered when no MCP server, including Composio, is configured):

ToolPurposeArgs
mcp.resource.listList a server’s resourcesserver, optional limit (1–100)
mcp.resource.readRead one resource (the model sees up to about 8,000 characters)server, uri
mcp.prompt.listList a server’s prompt templatesserver, optional limit (1–100)
mcp.prompt.getRender a prompt templateserver, name, optional arguments

All four are pure_read. The agent uses them to discover what a server offers before acting.

Sampling (optional)

Some MCP servers want to call back into a language model — for example, to summarize something mid-task. Atomic Agent supports this: when a server issues a /sampling/createMessage request, the runtime routes it to llama-server.

What the runtime guarantees instead:

  • It always goes to llama-server. Sampling is sent to the llama-server Atomic Agent is set up to use (the managed daemon, or localModels.url in external mode), never to the cloud provider in activeTextProvider, so a cloud provider never bills you for it. If no llama-server is running (for example a cloud-only setup), the server’s sampling request fails.
  • Token budget is capped. A server’s requested maxTokens is clamped to 4,096; a request that omits it gets 512. A server cannot ask for an unbounded generation.
  • It never touches your session. Sampling always runs on slotId: -1, never the main agent or reflection slot. That avoids KV-cache collisions, but means the request is free-form — no grammar is attached, so the server owns any structure it expects.
  • Only text crosses over. MCP image, audio, and resource content blocks are dropped when the messages are flattened into a prompt; an image-only request degrades to empty content.
  • Capability is decided at construction time. If you boot with zero servers and add the first one live, that server won’t get a sampling handler. Configure at least one server up front if you need sampling.

Composio rides on the same client

The connected-apps integration (Composio) uses the same MCP client. When a Composio API key is configured (read from the COMPOSIO_API_KEY environment variable by default; the name is configurable via composio.apiKeyEnv), Atomic Agent mounts Composio’s hosted tool-router endpoint as one more MCP server over Streamable HTTP. It does not appear in mcp.servers[], but it counts as a server: it enables sampling and the meta-tools just like a configured one.

Live control (TUI)

In the TUI’s MCP panel (open it with /mcp) you can add and remove servers without restarting:

  • Add (n in the list, or /mcp add): paste a server config; addServerLive() connects and registers it idempotently, then refreshMcp() rebuilds the grammar so the next inference sees the new tools.
  • Remove (d on a row, or /mcp remove <name>): disconnects, unregisters the tools, and rebuilds the resolver.
  • Other keys: j/k move the cursor, Enter opens a server’s detail view, r refreshes status, and a toggles auto-refresh.

There is no restart or enable/disable toggle in the panel today. To restart or disable a server, edit its enabled flag in config.json and restart the agent.

Status moves through disabled → starting → up (or down on failure), surfaced as badges in the panel.

Behavior worth knowing

  • One bad server can’t break the others. Connections happen in parallel and failures are isolated. A server that won’t start just shows down.
  • Responses are clipped. Tool-call, prompt and resource content is delivered to the model capped at about 8,000 characters, so a noisy server can’t balloon your prompt.
  • Timeouts are fixed. A server has 15 seconds to connect and 60 seconds to answer a tool call. Neither is configurable per server today, so a slow backend surfaces as a timeout rather than a hang.
  • Every MCP tool costs tokens on every turn. MCP tools are injected into the stable prompt prefix at tier frequent, meaning the full argument schema is rendered — not a one-line summary. That’s deliberate: at the previous rare tier smaller local models could see MCP tools but almost never called them, because using one first required an extra tool.view round-trip. The trade is that a chatty server with dozens of verbose schemas inflates the prefix for every turn, whether or not its tools get used. There is no per-server tier override today, so the only lever is not connecting the server. (Individual schemas are truncated at 2,000 chars and summaries at 200, which caps a single tool but not the total.)
  • Invalid tools are dropped silently. A raw tool name must match /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/; anything that fails (or fails to round-trip through name parsing) is skipped fail-closed rather than registered broken. There’s no warning in the logs — a tool you expected simply won’t be in the registry, so check mcp.resource.list and the TUI catalog when something’s missing.
  • Duplicate server names are fatal. Two entries sharing a name fail config validation outright rather than one silently shadowing the other. The runtime keeps a second layer of defence for live-added servers, where a name that’s already connected is dropped with an mcp.duplicate_server_dropped warning instead of replacing the running one.
  • Names with dots still parse. A raw tool name like os.read_file works — mcp.server.os.read_file parses to server server, tool os.read_file. The server name is the part that must not contain dots.
  • Live-add is idempotent. Calling add twice with the same name is a no-op; check the added return value rather than assuming.