Skip to content
All pages

MCP server

Connect an MCP client to moatbox with an API key so an agent outside moatbox can list spaces, start runs, run commands, edit files and take snapshots.

moatbox serves a remote MCP server. An agent on your laptop or in your CI can use it to drive spaces: start agent runs, follow them, run commands, read and write files, and save or restore snapshots.

https://app.moatbox.dev/api/mcp

The transport is Streamable HTTP. Each JSON-RPC message is one POST.

Authentication

The server accepts an API key and nothing else:

Authorization: Bearer mbx_…

A dashboard browser session does not work here. A request with no key, or a key moatbox does not know, gets 401 with WWW-Authenticate: Bearer realm="moatbox". An expired key gets 401 with code api_key_expired.

Create the key in Settings → API keys (see API keys). Give it only what the agent needs:

  • read-only lets the agent look and change nothing. Reading a space or its files still wakes the space when it sleeps.
  • operate adds runs, commands and file writes in existing spaces.
  • ci adds creating, waking and forking spaces.

Limit the key to the spaces or environments the agent should touch. A space key (MOATBOX_SPACE_KEY inside a space) also works and reaches only its own space.

Tools

tools/list returns only the tools your key may call. A tool whose route needs an action the key lacks stays hidden. A space key, for example, never sees create_space.

Tool What it does Key action
list_spaces List the spaces this key can reach, with slug, status and size. space.read
get_space Show one space: status, size, server, and whether runs can start. space.read
create_space Create a space in the key's team. It starts provisioning; poll get_space until it is running. space.create
wake_space Wake a sleeping space. space.wake
sleep_space Put a space to sleep. Its disk stays; running agents stop. space.sleep
fork_space Copy a space, disk and all, into one or more new spaces. space.fork
start_run Give an agent (claude, codex or opencode) a task in a space. run
list_runs List a space's runs, newest first. space.read
get_run Show a run: status, agent, task, result. space.read
get_run_timeline Show what a run did step by step: messages, tool calls, held actions. space.read
send_message Send a follow-up message to a running agent. run
stop_run Stop a running agent. run
run_command Run a shell command and return its exit code and output. moatbox stops the command after timeout_seconds: 1 to 600, 30 by default. detached returns at once with an id; a detached command takes 0 to 86400 seconds and has no limit by default. exec
get_command Status and output of a command run_command started. exec
list_files List a directory. Pass path, such as /workspace. file.read
read_file Read a text file. file.read
write_file Write a text file, replacing it when it exists. file.write
list_pending_approvals List actions agents took that wait for a person's approval. space.read
list_snapshots List a space's saved snapshots. snapshot.read
save_snapshot Save the space's disk as a named snapshot. snapshot.write, plus run, exec or file.write
restore_snapshot Roll a space back to a snapshot. moatbox keeps the current state for a while; bring it back from the dashboard. snapshot.write, plus run, exec or file.write

Each tool's input schema sets additionalProperties: false. Tools that take a space name it with space, the slug that list_spaces shows. File paths are absolute and sit under /workspace or /home/agent, with no .. in them.

The server marks tools with MCP annotations so your client can decide what to confirm:

  • readOnlyHint: true: list_spaces, get_space, list_runs, get_run, get_run_timeline, get_command, list_files, read_file, list_pending_approvals, list_snapshots.
  • destructiveHint: true: run_command, write_file, restore_snapshot.

The key's creator still needs the right team role. Creating and forking spaces, and saving snapshots, need an owner or admin behind the key.

Results

A tool call returns MCP text content. moatbox calls its own HTTP API for each tool and passes back the response body:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{"type": "text", "text": "{\"spaces\":[…]}"}],
    "isError": false,
    "structuredContent": {"spaces": […]}
  }
}
  • If the API succeeds with a JSON object, the result also carries the whole object as structuredContent.
  • When the API answers with status 400 or above, isError is true and the text starts with the status, for example HTTP 403: {"error":"this API key may not run","code":"api_key_action_forbidden"}.
  • A missing required argument returns isError: true with <name> is required. An argument the tool does not define returns unknown argument <name>.
  • An unknown tool name returns JSON-RPC error -32602.

Each call goes through the same checks as the HTTP API. A call that changes something adds an api_key.write entry to the team's audit log, like any other write the key makes.

Limits

Limit Value What happens
Requests per key 300 per minute 429 with Retry-After: 60
Request body 8 MiB 413, JSON-RPC error -32600
Tool output 200 KiB of text The text ends with … cut at N of M bytes; structuredContent is not cut
Output that is not valid UTF-8 none returned The text says how many bytes it was; copy the file with scp or the files API

The routes behind most tools cap their own body at 64 KiB. write_file uses the streaming upload route, so its content can use most of the 8 MiB. The whole JSON-RPC request counts toward that limit, including the envelope and JSON escaping.

Protocol

Item Behaviour
Versions through initialize 2025-11-25, 2025-06-18, 2025-03-26
Version with per-request headers 2026-07-28
Methods initialize, ping, tools/list, tools/call, server/discover
Capabilities tools only
Sessions None. moatbox sends no MCP-Session-Id.
Responses One application/json body per request, never an SSE stream
GET on the endpoint 405
Notifications (no id) 202 with no body
Batches (a JSON array) 400, JSON-RPC error -32600

initialize answers with the version the client asked for when it is one of the three initialize versions, and 2025-11-25 otherwise. After that, send MCP-Protocol-Version on each request. Without it moatbox assumes 2025-03-26. An unsupported version gets 400 with JSON-RPC error -32022 and data.supported listing the versions moatbox accepts.

On 2026-07-28, each request carries its version in params._meta["io.modelcontextprotocol/protocolVersion"]. moatbox then checks the headers against the body: MCP-Protocol-Version must equal that version, Mcp-Method must equal the method, and on tools/call Mcp-Name must equal the tool name (plain or in =?base64?…?= form). A mismatch gets 400 with JSON-RPC error -32020.

moatbox refuses requests that a browser sends from another site (403).

What the server leaves out

The MCP server covers day-to-day work in spaces. These stay in the HTTP API or the dashboard:

  • deleting spaces,
  • approving or denying held actions (a person decides in the dashboard; see Approvals),
  • API keys, webhooks, environments and team settings,
  • SSH, the desktop and tailnet access,
  • copying files between spaces, deleting snapshots, resets.

Client setup

Keep the key out of config files: put it in an environment variable, here MOATBOX_API_KEY, and reference the variable.

export MOATBOX_API_KEY=mbx_…

Claude Code

Add the server from the command line:

claude mcp add --transport http moatbox https://app.moatbox.dev/api/mcp \
  --header "Authorization: Bearer $MOATBOX_API_KEY"

Your shell expands the variable, so the key itself goes into the config. The default local scope keeps that config to you and the current project. To share the server through the project's .mcp.json without the key, write the file by hand; Claude Code expands ${VAR} in url and headers:

{
  "mcpServers": {
    "moatbox": {
      "type": "http",
      "url": "https://app.moatbox.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer ${MOATBOX_API_KEY}"
      }
    }
  }
}

Codex

Add the server to ~/.codex/config.toml, or to .codex/config.toml in a trusted project:

[mcp_servers.moatbox]
url = "https://app.moatbox.dev/api/mcp"
bearer_token_env_var = "MOATBOX_API_KEY"
default_tools_approval_mode = "writes"

bearer_token_env_var names the variable Codex sends as the bearer token. With writes, Codex asks you before it calls any tool that moatbox does not mark read-only.

Codex cannot ask you when it runs with approval_policy = "never", as in an unattended job, so it refuses the tools moatbox marks destructive: run_command, write_file and restore_snapshot. To let it call them, set default_tools_approval_mode = "approve" for the server. Codex then calls every moatbox tool without asking, so give that server a key with only the actions the job needs. To approve a single tool, set tools.<tool>.approval_mode instead.

Cursor

Add the server to ~/.cursor/mcp.json, or .cursor/mcp.json in a project:

{
  "mcpServers": {
    "moatbox": {
      "url": "https://app.moatbox.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer ${env:MOATBOX_API_KEY}"
      }
    }
  }
}

Cursor reads ${env:MOATBOX_API_KEY} from your environment.

Any other client

Point a Streamable HTTP client at https://app.moatbox.dev/api/mcp with the Authorization header. You can also call the server with curl:

curl -X POST https://app.moatbox.dev/api/mcp \
  -H "Authorization: Bearer $MOATBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "run_command", "arguments": {"space": "<space>", "command": "git status"}}}'

The initialize response includes server instructions that tell the agent where to start: list_spaces first, start_run to hand over a task, get_run and get_run_timeline to follow it.