How to build an MCP server and use it with Atomic Agent
An MCP server is a small program that hands an AI agent a list of tools and runs them when asked. This page builds one from scratch with the official SDKs, connects it to Atomic Agent, and shows how to test and debug it. The example is a tiny notes server with two tools: list_notes, which only reads, and save_note, which writes to a file. Two tools are enough to show the part that matters in practice: which calls the agent runs on its own and which ones wait for your approval.
Quick answer. Write the server with @modelcontextprotocol/server (TypeScript) or mcp (Python), run it over stdio, and add one entry to mcp.servers in ~/.atomic-agent/config.json whose transport is {"kind": "stdio", "command": ..., "args": [...]}. Restart the agent, or add it live with /mcp add in the terminal UI. Its tools appear as mcp.notes.list_notes and mcp.notes.save_note.
1. Write the server
You need Node.js (a current LTS release). Create a folder and install the server package, Zod for the input schema, and tsx to run TypeScript without a build step:
mkdir notes-mcp && cd notes-mcpnpm init -ynpm pkg set type=modulenpm install @modelcontextprotocol/server zodnpm install -D tsxSave this as server.ts:
import { appendFile, readFile } from 'node:fs/promises';import { McpServer } from '@modelcontextprotocol/server';import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';import * as z from 'zod/v4';
const NOTES = new URL('./notes.txt', import.meta.url);const server = new McpServer({ name: 'notes', version: '1.0.0' });
// Read-only: the annotation lets Atomic Agent run it without asking.server.registerTool( 'list_notes', { description: 'Return every saved note, one per line.', inputSchema: z.object({}), annotations: { readOnlyHint: true }, }, async () => { const text = await readFile(NOTES, 'utf8').catch(() => ''); return { content: [{ type: 'text', text: text || 'No notes yet.' }] }; },);
// Writes a file: no annotation, so Atomic Agent asks before each call.server.registerTool( 'save_note', { description: 'Append one note to the notes file.', inputSchema: z.object({ text: z.string().min(1).describe('The note to save') }), }, async ({ text }) => { await appendFile(NOTES, text.replace(/\n/g, ' ') + '\n'); return { content: [{ type: 'text', text: `Saved: ${text}` }] }; },);
await server.connect(new StdioServerTransport());console.error('notes MCP server running on stdio');Note the last line uses console.error, not console.log. Over stdio, standard output is the protocol channel, so anything you print there breaks the connection. Log to standard error.
You need Python 3.11 or newer and uv. Create a project and add the SDK:
uv init notes-mcp && cd notes-mcpuv add "mcp[cli]"Save this as server.py:
from pathlib import Path
from mcp.server import MCPServer
NOTES = Path(__file__).with_name("notes.txt")mcp = MCPServer("notes")
@mcp.tool()def list_notes() -> str: """Return every saved note, one per line.""" return NOTES.read_text() if NOTES.exists() else "No notes yet."
@mcp.tool()def save_note(text: str) -> str: """Append one note to the notes file.""" with NOTES.open("a") as f: f.write(text.replace("\n", " ") + "\n") return f"Saved: {text}"
if __name__ == "__main__": mcp.run() # no argument means stdioThe type hints are the input schema and the docstrings become the tool descriptions. As with TypeScript, do not print() to standard output while serving; use the logging module, which writes to standard error.
This Python version does not mark list_notes as read-only, so Atomic Agent will ask before it runs too. The Python SDK supports tool annotations as well; see its tools documentation if you want the same behavior as the TypeScript example.
2. Test it before you connect it
Run the server inside the official MCP Inspector first. If a tool fails here, it will fail inside the agent too, and the Inspector shows the raw error.
npx @modelcontextprotocol/inspector npx tsx server.tsuv run mcp dev server.pyOpen the URL it prints, connect, open Tools, and call save_note with some text, then list_notes. You should see the note come back.
3. Connect it to Atomic Agent
Use absolute paths. The agent starts the server as a child process from its own working directory, not from your project folder.
{ "mcp": { "servers": [ { "name": "notes", "description": "My notes server", "transport": { "kind": "stdio", "command": "npx", "args": ["tsx", "/Users/you/notes-mcp/server.ts"], "cwd": "/Users/you/notes-mcp" } } ] }}{ "mcp": { "servers": [ { "name": "notes", "description": "My notes server", "transport": { "kind": "stdio", "command": "uv", "args": ["run", "--directory", "/Users/you/notes-mcp", "server.py"] } } ] }}Where this goes:
- By hand: merge it into
~/.atomic-agent/config.json. If you already have anmcp.serversarray, add only the inner object. Then restart the agent. Servers inconfig.jsonconnect once, at startup. - Terminal UI: open
/mcp, pressn(or type/mcp add) and paste just the inner object (from{"name": "notes", ...}). It connects right away. - Desktop app: Settings › Connections › MCP servers › Add server, paste the inner object, press ⌘Enter (Ctrl+Enter on Windows and Linux), then press Restart on the new row to connect it.
enabled defaults to true, so you do not need to write it. The name must be lowercase letters, digits and hyphens, 2 to 32 characters, starting and ending with a letter or digit, with no dots. It becomes the prefix of every tool name.
4. Try it in a chat
Check the server status first: in the TUI /mcp panel the row should read up; in the desktop app it should read running. Open the row to see its tools. Then ask:
Save a note that the staging deploy moved to Thursday, then list all my notes.
What happens, with the TypeScript version:
- The agent calls
mcp.notes.save_note. That tool has no read-only annotation, so you get an approval prompt first. In the TUI press Ctrl+Y to approve this one call; in the desktop app use the approval card. - The agent calls
mcp.notes.list_notes. That one carriesreadOnlyHint: true, so it runs without a prompt.
This is the rule for every MCP tool in Atomic Agent: approval-gated by default, skipped for tools the server marks readOnlyHint: true, for servers you mark "trust": "pure_read", at approval level 5 (full trust), and after you allow the category for the session. Do not mark a server pure_read if any of its tools can write, delete, send or spend. See Approval Gates.
Serving over HTTP instead
Stdio is the simplest choice for a server that runs on the same machine. If you want one server shared by several clients, both SDKs can serve Streamable HTTP (in Python, mcp.run(transport="streamable-http")). Connect it with:
{ "name": "notes", "transport": { "kind": "streamable_http", "url": "http://127.0.0.1:8000/mcp" }}Atomic Agent does not run the MCP OAuth flow. If your HTTP server needs authentication, have it accept a static header (for example Authorization: Bearer <token>) and put that header in transport.headers. Header values are sent exactly as written; ${VAR} is not expanded.
Troubleshooting
The server shows down or failed. Open the row to read the error. For stdio servers Atomic Agent appends the last lines the process wrote to standard error after server stderr:, which usually names the real problem (a missing module, a syntax error, a wrong path).
spawn npx ENOENT or uv: command not found. The command is not on the agent’s PATH. This happens most often with the desktop app on macOS: launched from Finder or the Dock, it does not inherit the PATH your shell sets up. Use the full path from which npx or which uv as command. On Windows, bare commands like npx and uvx are routed through cmd.exe for you.
connect timeout after 15000ms. A server gets 15 seconds to start and answer the handshake. The first npx -y some-package or uvx some-package run downloads the package and can take longer. Run the same command once in a terminal so it is cached, then restart the server.
tool call timeout after 60000ms. Each tool call has 60 seconds. Neither timeout is configurable per server today. Make slow tools return early with a status, or split the work.
A tool you wrote is missing. Tool names must match ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$. A name that does not match is skipped without a log line. Keep names to letters, digits, _, - and ..
The server breaks right after it starts. Something printed to standard output. Move every log line to standard error.
The agent never calls your tool. Small local models pick tools by their description. Say what the tool does and when to use it in one plain sentence, and keep the input schema short. Also check the tool count: every connected MCP tool is rendered into the prompt on every turn, so a server with many tools makes the model’s choice harder.
Large results look cut off. Tool output reaches the model capped at about 8,000 characters. Return summaries, or add limit and offset parameters.
FAQ
Do I need TypeScript or Python?
No. MCP is a protocol over JSON-RPC, and Atomic Agent talks to any server that speaks it over stdio, Streamable HTTP or SSE. The two official SDKs used here are just the shortest path. Official SDKs exist for several other languages; see modelcontextprotocol.io.
Can my server read secrets from the environment?
Yes, for stdio servers. A stdio server inherits the agent’s whole environment, including keys from ~/.atomic-agent/.env, and you can add per-server values in the entry’s env object. That also means it can read every other key the agent holds, so only run stdio servers you trust.
Can my server call the model back?
Yes. If your server sends an MCP sampling request, Atomic Agent answers it with its local llama-server, capped at 4,096 tokens, even when your chat model is a cloud provider (so sampling fails if no local llama-server is running). There is no approval prompt for sampling, and the handler is only installed when at least one MCP server is configured at startup. Details are in the MCP client reference.
Does my server need resources and prompts too?
No. Tools are enough. If you add resources or prompt templates, the agent can reach them through the built-in mcp.resource.* and mcp.prompt.* tools.
How do I share the server with my team?
Publish it as an npm or PyPI package. Then the config becomes "command": "npx", "args": ["-y", "your-package"] or "command": "uvx", "args": ["your-package"], the same shape the official servers in the MCP servers section use.
Checked on 22 Oct 2026 against Atomic Agent v0.6.10 and Desktop v0.0.4, @modelcontextprotocol/server v2 and mcp v2.
Get updates by email
One email when a new version comes out.