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

Every MCP tool is approval-gated unless you explicitly mark a server pure_read. Hostile or noisy servers are fenced off.

Add servers live

In the TUI you can add, remove, restart, or toggle 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). 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.

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 button, or HTTP webhook). This is the safe default for tools you don’t fully control.

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.

{
"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.
  • 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 zero servers exist):

ToolPurposeArgs
mcp.resource.listList a server’s resourcesserver, optional limit (1–100)
mcp.resource.readRead one resource (clipped at 16,000 chars)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 the local llama-server.

What the runtime guarantees instead:

  • It always runs locally. Sampling goes to the local llama-server client, never to a cloud provider, no matter what activeTextProvider points at. Nothing a server samples is billed to you or leaves your machine.
  • 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.

Live control (TUI)

In the TUI’s MCP panel you can manage servers without restarting:

  • 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 — disconnects, unregisters the tools, and rebuilds the resolver.
  • Restart — stop and start a single server.
  • Toggle — flip enabled at runtime.

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 and prompt content is projected to a single text output capped at 8,000 characters before compression, so a noisy server can’t balloon your prompt. mcp.resource.read is the exception — it allows 16,000.
  • 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.

Tool security & approval gates

How approval gates work across all dangerous tools, including MCP.

Configuration reference

Full config.json schema, including the mcp.servers[] block.

Tools overview

How the tool registry, batching, and resource classes fit together.

TUI guide

The MCP panel and live-control gestures.