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-onlylets the agent look and change nothing. Reading a space or its files still wakes the space when it sleeps.operateadds runs, commands and file writes in existing spaces.ciadds 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,
isErroristrueand the text starts with the status, for exampleHTTP 403: {"error":"this API key may not run","code":"api_key_action_forbidden"}. - A missing required argument returns
isError: truewith<name> is required. An argument the tool does not define returnsunknown 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.