Add tools without code
Drop a server config into config.json. Its tools join the registry automatically — no rebuild, no plugin API.
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.
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.
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.
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:
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.refreshMcp().Each entry in mcp.servers[] is an McpServerConfig:
| Field | Required | Meaning |
|---|---|---|
name | yes | Unique 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. |
enabled | no | Whether 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. |
transport | yes | How to reach the server — stdio, streamable_http, or sse (see below). |
trust | no | approval_gated (default) or pure_read. Controls batching and approval. |
env | no | Per-server env var overrides (stdio only). Merged on top of the process env. |
description | no | One-liner shown in the TUI and logs. |
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.
Connects to a remote HTTP MCP endpoint.
{ "name": "search", "enabled": true, "transport": { "kind": "streamable_http", "url": "https://mcp.example.com/v1", "headers": { "Authorization": "Bearer sk-your-actual-token" } }}Connects over Server-Sent Events for servers that stream responses.
{ "name": "events", "enabled": true, "transport": { "kind": "sse", "url": "https://mcp.example.com/sse", "headers": { "Authorization": "Bearer sk-your-actual-token" } }}Headers here are literal too — see the note under streamable_http above.
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:
approval_gated — never pure_read.mcp.ghost.do_thing lands in the safe lane because the resolver is always installed.Once any MCP server is connected, four read-only meta-tools become available to the agent (they’re not registered when zero servers exist):
| Tool | Purpose | Args |
|---|---|---|
mcp.resource.list | List a server’s resources | server, optional limit (1–100) |
mcp.resource.read | Read one resource (clipped at 16,000 chars) | server, uri |
mcp.prompt.list | List a server’s prompt templates | server, optional limit (1–100) |
mcp.prompt.get | Render a prompt template | server, name, optional arguments |
All four are pure_read. The agent uses them to discover what a server offers before acting.
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:
activeTextProvider points at. Nothing a server samples is billed to you or leaves your machine.maxTokens is clamped to 4,096; a request that omits it gets 512. A server cannot ask for an unbounded generation.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.In the TUI’s MCP panel you can manage servers without restarting:
addServerLive() connects and registers it idempotently, then refreshMcp() rebuilds the grammar so the next inference sees the new tools.enabled at runtime.Status moves through disabled → starting → up (or down on failure), surfaced as badges in the panel.
down.mcp.resource.read is the exception — it allows 16,000.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.)/^[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.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.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.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.