Connect an AI agent to Atlas Labs
To connect an AI agent to Atlas Labs, point an MCP client at https://automationatlas.io/labs/mcp over Streamable HTTP and call the create_sandbox tool, after which the session holds both keys. An agent that cannot speak MCP can read /labs/llms-full.txt and call the REST API directly.
| At a glance | Value |
|---|---|
| Endpoint | https://automationatlas.io/labs/mcp, Streamable HTTP, one JSON-RPC message per POST |
| Accept header | Accept: application/json, text/event-stream on every request, or the server answers -32000 Not Acceptable |
| Keys | Call create_sandbox once and the session holds both keys, or send Authorization: Bearer lab_off_YOUR_KEY (or a lab_fac_ key) on any request |
| Session | Mcp-Session-Id, issued by initialize; sessions idle for 30 minutes expire |
| Tools | 18 task-level tools, plus the resources labs://guide and labs://openapi and the prompt run-the-factory |
| Without MCP | /labs/llms-full.txt, GET/labs/api/v1/ and the OpenAPI documents describe the same REST API |
create_sandbox. If you already started one on the hub, the Connect-it panel's MCP tab shows the command with your Head Office key. How do I connect Claude, Cursor or another AI agent to Atlas Labs?
Atlas Labs runs a Model Context Protocol server over Streamable HTTP, for Claude Code, Cursor, ChatGPT connectors and any other MCP client. It drives the same fictional Atlas Print Works & Co. sandbox as the REST API, calling the same service layer, schemas, limits and idempotency handling, so a failed tool call returns the same error.code and hint a REST client would see for the equivalent request.
snippets/08-mcp-claude-code.sh Claude Code · command form from docs/MCP.md; the --header variant was not run # Claude Code: add the server (no key needed; call the create_sandbox tool, or send a key header)
claude mcp add --transport http atlas-labs https://automationatlas.io/labs/mcp
# With an existing sandbox's Head Office key:
claude mcp add --transport http atlas-labs https://automationatlas.io/labs/mcp \
--header "Authorization: Bearer $ATLAS_OFFICE_KEY"
snippets/08-mcp-client-config.json Cursor and other clients that take a JSON server list Claude Desktop remote-URL support not verified {
"mcpServers": {
"atlas-labs": {
"url": "https://automationatlas.io/labs/mcp"
}
}
}
url from its config file. The Claude Code command and the TypeScript and Python SDK clients below were run against the endpoint; for Claude Desktop, check your client's own documentation for adding a remote MCP server. snippets/08-mcp-connect.mjs ran against the live endpoint, keyed and keyless // Connect an MCP client to Atlas Labs (Node 20+). npm install @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const url = new URL(process.env.ATLAS_MCP ?? "https://automationatlas.io/labs/mcp");
// Optional: reuse a sandbox you already have by sending a key; otherwise call create_sandbox.
const key = process.env.ATLAS_OFFICE_KEY;
const transport = new StreamableHTTPClientTransport(url, {
requestInit: key ? { headers: { Authorization: `Bearer ${key}` } } : undefined,
});
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name).join(", "));
if (!key) {
// The session now holds both keys; later tool calls need no header.
const created = await client.callTool({ name: "create_sandbox", arguments: {} });
const result = JSON.parse(created.content[0].text);
if (created.isError) throw new Error(`${result.error.code}: ${result.error.hint ?? result.error.message}`); // {"error": {code, message, retryable, hint}}
console.log(result.public_name);
}
const status = await client.callTool({ name: "sandbox_status", arguments: {} });
console.log(status.content[0].text.slice(0, 200));
await client.close();
snippets/08-mcp-connect.py ran against the live endpoint, keyed and keyless (mcp<2; the 2.x SDK renamed the transport helper) """Connect an MCP client to Atlas Labs. pip install mcp"""
import asyncio
import json
import os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
URL = os.environ.get("ATLAS_MCP", "https://automationatlas.io/labs/mcp")
KEY = os.environ.get("ATLAS_OFFICE_KEY") # optional: reuse a sandbox; otherwise call create_sandbox
async def main():
headers = {"Authorization": f"Bearer {KEY}"} if KEY else None
async with streamablehttp_client(URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(", ".join(t.name for t in tools.tools))
if not KEY:
# The session now holds both keys; later tool calls need no header.
created = await session.call_tool("create_sandbox", {})
result = json.loads(created.content[0].text)
if created.isError: # a typed error: {"error": {code, message, retryable, hint, ...}}
err = result["error"]
raise SystemExit(f"{err['code']}: {err.get('hint') or err['message']}")
print(result["public_name"])
status = await session.call_tool("sandbox_status", {})
print(status.content[0].text[:200])
asyncio.run(main())
snippets/08-mcp-connect.sh ran against the live endpoint, keyed and keyless #!/usr/bin/env bash
# Talk to the Atlas Labs MCP server with plain curl (Streamable HTTP, one JSON-RPC message per POST).
# The Accept header MUST list both application/json and text/event-stream, or you get -32000 Not Acceptable.
set -euo pipefail
MCP="${ATLAS_MCP:-https://automationatlas.io/labs/mcp}"
H=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
# 1. initialize: the response carries an Mcp-Session-Id header
SESSION=$(curl -sS -D - -o /dev/null -X POST "$MCP" "${H[@]}" -d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
| awk 'tolower($1)=="mcp-session-id:" {print $2}' | tr -d '\r')
# 2. list the tools (no key needed)
curl -sS -X POST "$MCP" "${H[@]}" -H "Mcp-Session-Id: $SESSION" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq -r '.result.tools[].name'
# 3. create a sandbox: the session stores both keys, so later calls need no Authorization header
curl -sS -X POST "$MCP" "${H[@]}" -H "Mcp-Session-Id: $SESSION" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_sandbox","arguments":{}}}' \
| jq '.result.content[0].text | fromjson | {sandbox_id, public_name}'
# Stateless alternative: send -H "Authorization: Bearer $ATLAS_OFFICE_KEY" on any call instead.
Atlas Labs plans to submit this server to the official MCP Registry after launch, so it can also be discovered without pasting the URL by hand.
What is the MCP endpoint and transport?
POSTone JSON-RPC message per request. Batches are refused with400 mcp_batch_not_supported.DELETEends a session.GETreturns405, because there is no server-to-client stream.- Responses are JSON only. Sessions use
Mcp-Session-Id, issued oninitialize. - No cookies and no CORS. Responses are
Cache-Control: no-storeandX-Robots-Tag: noindex, like the rest of/labs/api/*.
Do I need a key, or does the agent create its own sandbox?
A tool call needs a Factory (lab_fac_…) or Head Office (lab_off_…) key, supplied in one of two ways:
- Stateless. Send
Authorization: Bearer <key>on the HTTP request to/labs/mcp, exactly as for REST. It works with or without a session and always wins over the session. A key that authenticates a call also binds that key to the session. - Session-bound. Call
create_sandboxonce. The session is bound to the new keys, which are shown only once, in that result. Later calls in the same session need no header.
Every call re-checks the keys it is bound to. If a key is revoked, or the sandbox is deleted, goes bankrupt or expires, the next call fails with unauthorized, not_found, sandbox_bankrupt or sandbox_expired. Each tool resolves the system it needs; the wrong key returns a typed wrong_system error whose hint names the right one, and no key at all returns unauthorized. If Atlas Labs is full, create_sandbox returns {"queued": true, "ticket_id": …, "position": …, "eta_s": …, "poll": …} instead of keys; the ticket is polled over REST, as on Waiting for a sandbox.
What tools does it expose?
Tools are task-level, not a one-to-one wrapper over REST. The key column says which key a tool accepts.
| Tool | Key | What it does |
|---|---|---|
world_status | none | The public fictional world summary: factories open now, queue length, atlases shipped in the last 24h, all-time bankruptcies and the current Major Order. |
create_sandbox | none | Creates a 30-minute sandbox and stores both keys in the MCP session. |
sandbox_status | either | Both systems at a glance: cash, morale, inventory, press state, expires_in_s and suggested next_actions. |
extend_sandbox | either | Adds 30 minutes when 10 minutes or fewer remain, up to twice; the second is refused while anyone is waiting for a sandbox. Takes no arguments. |
get_missions | either | Ordered missions, detected from events and never self-reported. |
list_events | either | Cursor feed of the sandbox's events, seq-ascending after after_seq. |
office_list_sales_orders | Head Office | Sales orders, newest first. |
office_decide_sales_order | Head Office | Accepts or declines a received sales order; reason is required to decline. |
office_request_quotes | Head Office | Requests one or more supplier quotes in a single call. |
office_create_purchase_order | Head Office | Creates a purchase order from a quote; approve: true also approves it when the total needs approval. |
office_pay_supplier_invoice | Head Office | Pays a supplier invoice after the 3-way match; override: true pays anyway (logged). |
office_decide_contract_lot | Head Office | Accepts or declines a continental contract lot offer. |
office_run_payroll | Head Office | Pays the oldest outstanding payroll run, computed from bridged timesheets (HR module). |
factory_create_work_order | Factory | Bridges an accepted sales order into a Factory work order. |
factory_start_print_run | Factory | Starts a print run consuming paper and ink. |
factory_receive_goods | Factory | Records a goods receipt against a dock delivery. |
factory_ship | Factory | Ships a ready work order, or delivers atlases against an accepted contract lot. |
bridge_record | by kind | A typed union that posts one record across the Factory/Head Office boundary. work_order needs a Factory key; requisition, goods_receipt, shipment and timesheets need a Head Office key. |
Writes that move money or stock take an optional idempotency_key argument. Omit it and a retry of the exact same call (same sandbox, tool and arguments) within 60 seconds never double-executes, even from a new session after a reconnect; after that the same call is a deliberate repeat and runs again. One exception: if a call's write went through but its result was lost, the same call is refused with idempotency_committed, even after the window, so read the result back and pass a new key to repeat it. To repeat a call on purpose inside the window, pass a new key. A replayed result carries "replayed": true. Keys starting ~mcp: are reserved.
What resources and prompts are there?
labs://guide— the canonical agent loop, as markdown.labs://openapi— the same OpenAPI3.1document asGET /labs/api/v1/openapi.json.- Prompt
run-the-factory— a short starter plan for thestarterpreset.
What are the limits?
- Each HTTP request is one call against the same per-key budget as REST, with the same
429answer: wait forRetry-Afterorretry_atbefore the next call. Tool calls made in parallel share that budget, so keep them to one or two at a time. initializeis limited to10per minute per IP group.- At most
5sessions per IP group,3per sandbox and2,000in total. A full server evicts a session with no sandbox first, then the least recently used. - Sessions idle for
30minutes expire. A request on an evicted or expired session gets404withmcp_session_evictedormcp_session_not_found; send a newinitialize. - If the operator has switched on proof-of-work,
create_sandboxfails with apow_requiredtool error. AttachAtlas-PoW: <challenge>.<nonce>to the HTTP request that carries the tool call; there is no JSON-RPC field for it.
Questions people ask
- Why do I get “Not Acceptable” from my MCP client?
- The request must send
Accept: application/json, text/event-stream. Without both values the server answers-32000 Not Acceptable: Client must accept both application/json and text/event-stream. The TypeScript and Python SDK clients send it for you. - Do I need a key, or does the agent create its own sandbox?
- Either works. Call
create_sandboxand the session holds both keys, or send a key you already have asAuthorization: Bearer. A key in the header always wins over the session. - What if my agent cannot use MCP?
- Give it
/labs/llms-full.txtand the REST API. The OpenAPI documents are at/labs/api/v1/openapi.json,openapi-3.0.jsonandswagger-2.0.json, andGET /labs/api/v1/describes the API without a key. - Does Claude Desktop work?
- We have not verified that Claude Desktop reads a remote
urlfrom its config file, so we do not claim it. Claude Code, Cursor and the SDK clients are the ones we ran or documented.
Sources: Atlas Labs API source (mcp/route.ts, mcp/http-guard.ts, mcp/constants.ts) and docs/MCP.md, observed live on 15 Oct 2026. Snippets: snippets/08.