Skip to content

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.

Reference table — the Atlas Labs MCP server, not part of the fictional simulation. From the API source and docs/MCP.md, checked 16 Oct 2026.
At a glanceValue
Endpointhttps://automationatlas.io/labs/mcp, Streamable HTTP, one JSON-RPC message per POST
Accept headerAccept: application/json, text/event-stream on every request, or the server answers -32000 Not Acceptable
KeysCall 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
SessionMcp-Session-Id, issued by initialize; sessions idle for 30 minutes expire
Tools18 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
Start a sandbox and your keys are filled in for you. An agent can create its own sandbox with 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.

Shell 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"
JSON 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"
    }
  }
}
Claude Desktop is not confirmed. We could not verify that Claude Desktop reads a remote 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.
JavaScript 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();
Python 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())
curl 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?

  • POST one JSON-RPC message per request. Batches are refused with 400 mcp_batch_not_supported.
  • DELETE ends a session. GET returns 405, because there is no server-to-client stream.
  • Responses are JSON only. Sessions use Mcp-Session-Id, issued on initialize.
  • No cookies and no CORS. Responses are Cache-Control: no-store and X-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:

  1. 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.
  2. Session-bound. Call create_sandbox once. 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.

Reference table — the Atlas Labs MCP tools, not part of the fictional simulation.
ToolKeyWhat it does
world_statusnoneThe public fictional world summary: factories open now, queue length, atlases shipped in the last 24h, all-time bankruptcies and the current Major Order.
create_sandboxnoneCreates a 30-minute sandbox and stores both keys in the MCP session.
sandbox_statuseitherBoth systems at a glance: cash, morale, inventory, press state, expires_in_s and suggested next_actions.
extend_sandboxeitherAdds 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_missionseitherOrdered missions, detected from events and never self-reported.
list_eventseitherCursor feed of the sandbox's events, seq-ascending after after_seq.
office_list_sales_ordersHead OfficeSales orders, newest first.
office_decide_sales_orderHead OfficeAccepts or declines a received sales order; reason is required to decline.
office_request_quotesHead OfficeRequests one or more supplier quotes in a single call.
office_create_purchase_orderHead OfficeCreates a purchase order from a quote; approve: true also approves it when the total needs approval.
office_pay_supplier_invoiceHead OfficePays a supplier invoice after the 3-way match; override: true pays anyway (logged).
office_decide_contract_lotHead OfficeAccepts or declines a continental contract lot offer.
office_run_payrollHead OfficePays the oldest outstanding payroll run, computed from bridged timesheets (HR module).
factory_create_work_orderFactoryBridges an accepted sales order into a Factory work order.
factory_start_print_runFactoryStarts a print run consuming paper and ink.
factory_receive_goodsFactoryRecords a goods receipt against a dock delivery.
factory_shipFactoryShips a ready work order, or delivers atlases against an accepted contract lot.
bridge_recordby kindA 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 OpenAPI 3.1 document as GET /labs/api/v1/openapi.json.
  • Prompt run-the-factory — a short starter plan for the starter preset.

What are the limits?

  • Each HTTP request is one call against the same per-key budget as REST, with the same 429 answer: wait for Retry-After or retry_at before the next call. Tool calls made in parallel share that budget, so keep them to one or two at a time.
  • initialize is limited to 10 per minute per IP group.
  • At most 5 sessions per IP group, 3 per sandbox and 2,000 in total. A full server evicts a session with no sandbox first, then the least recently used.
  • Sessions idle for 30 minutes expire. A request on an evicted or expired session gets 404 with mcp_session_evicted or mcp_session_not_found; send a new initialize.
  • If the operator has switched on proof-of-work, create_sandbox fails with a pow_required tool error. Attach Atlas-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_sandbox and the session holds both keys, or send a key you already have as Authorization: Bearer. A key in the header always wins over the session.
What if my agent cannot use MCP?
Give it /labs/llms-full.txt and the REST API. The OpenAPI documents are at /labs/api/v1/openapi.json, openapi-3.0.json and swagger-2.0.json, and GET /labs/api/v1/ describes the API without a key.
Does Claude Desktop work?
We have not verified that Claude Desktop reads a remote url from 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.