MCP server#
Dormo provides a Model Context Protocol server. With it, an agent such as Claude Code, Claude Desktop, Cursor or Codex can create sandboxes, run commands, edit files, expose ports and drive a desktop, with no glue code.
| Mode | How | Auth |
|---|---|---|
| Local (stdio) | npx -y @dormo/mcp or dormo mcp |
env DORMO_API_KEY (optional DORMO_API_URL) |
| Self-hosted (Streamable HTTP) | npx -y @dormo/mcp --http --port 8787, or the Docker image |
Authorization: Bearer <api key> on every request |
| Hosted (Streamable HTTP), beta, coming soon | https://mcp.dormo.dev/mcp |
Authorization: Bearer <api key> on every request |
Every mode calls the public REST API, so each tool call is subject to the scopes of the API key you use. The hosted endpoint runs the same server as the self-hosted HTTP mode; it is not live yet. During the private beta @dormo/mcp is shared with beta participants directly; publication on npm is coming soon.
Use a restricted key#
Don't give an agent your admin key. Create a key limited to sandbox work and, optionally, to sandboxes with a given label:
curl -X POST https://api.dormo.dev/v1/api-keys \
-H "Authorization: Bearer $DORMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mcp-agent", "preset": "full-sandbox", "restrict": {"labels": {"owner": "mcp"}}, "expires_in_days": 30}'
Sandboxes created with this key automatically receive the label owner=mcp, and the key can only see and act on sandboxes with that label.
Setup#
Claude Code#
Local (stdio):
claude mcp add dormo --env DORMO_API_KEY=$DORMO_API_KEY -- npx -y @dormo/mcp
Remote (Streamable HTTP), against your own server or, once it is live, the hosted endpoint:
claude mcp add --transport http dormo http://127.0.0.1:8787/mcp \
--header "Authorization: Bearer $DORMO_API_KEY"
# hosted (beta, coming soon)
claude mcp add --transport http dormo https://mcp.dormo.dev/mcp \
--header "Authorization: Bearer $DORMO_API_KEY"
Claude Desktop / Cursor#
Add the local server to the client's MCP configuration (claude_desktop_config.json, or .cursor/mcp.json):
{
"mcpServers": {
"dormo": {
"command": "npx",
"args": ["-y", "@dormo/mcp"],
"env": { "DORMO_API_KEY": "dormo_sk_..." }
}
}
}
Cursor can also connect to an HTTP server with a static header:
{
"mcpServers": {
"dormo": {
"url": "http://127.0.0.1:8787/mcp",
"headers": { "Authorization": "Bearer dormo_sk_..." }
}
}
}
Claude Desktop's remote connectors sign in with OAuth, which the HTTP mode does not support yet (see Authentication). Use the local server there for now.
VS Code#
.vscode/mcp.json:
{
"servers": {
"dormo": {
"type": "http",
"url": "http://127.0.0.1:8787/mcp",
"headers": { "Authorization": "Bearer ${input:dormo-key}" }
}
},
"inputs": [{ "id": "dormo-key", "type": "promptString", "description": "Dormo API key", "password": true }]
}
Codex and other clients#
Codex (~/.codex/config.toml) reads the key from an environment variable:
[mcp_servers.dormo]
url = "http://127.0.0.1:8787/mcp"
bearer_token_env_var = "DORMO_API_KEY"
Any other MCP client works if it supports either stdio (run npx -y @dormo/mcp with DORMO_API_KEY in its environment) or Streamable HTTP with a custom Authorization header.
Self-hosting the HTTP server#
npx -y @dormo/mcp --http --port 8787 # listens on 127.0.0.1:8787, endpoint /mcp
npx -y @dormo/mcp --http --host 0.0.0.0 --read-only # all interfaces, read-only tools
The server has no API key of its own. Each MCP request must send Authorization: Bearer <api key>, and the tools of that request call the API with that key only. DORMO_API_KEY in the server's environment is ignored in this mode, so two users of one server never share a key. The server only checks that the header is present. The API decides whether the key is valid, so a revoked key gets tool results with isError: true and 401 unauthorized.
Docker#
The image is built from the SDK sources (build context sdks/):
docker build -f sdks/mcp/Dockerfile -t dormo-mcp sdks
docker run --rm -p 8787:8787 dormo-mcp # add flags after the image name, e.g. --read-only
The container listens on 0.0.0.0:8787, runs as a non-root user and has a HEALTHCHECK on /healthz. Because it binds to all interfaces, the Host check is off unless you pass --allowed-hosts (or DORMO_MCP_ALLOWED_HOSTS) with the host name clients use. Put it behind a reverse proxy that terminates TLS, because API keys travel in a header.
Configuration#
| Flag | Environment | Default | Meaning |
|---|---|---|---|
--http |
off (stdio) | Serve MCP over Streamable HTTP | |
--port N |
PORT |
8787 |
Listen port (0 picks a free port) |
--host H |
HOST |
127.0.0.1 (0.0.0.0 in Docker) |
Bind address |
--read-only |
off | Only the 5 read-only tools | |
--tools a,b |
all 15 | Tool allowlist | |
--max-body-bytes N |
1048576 |
Larger request bodies get 413 |
|
--request-timeout S |
660 |
Deadline for one MCP request, in seconds; a request over it gets 504 (exec waits up to 600 s) |
|
--allowed-origins a,b |
DORMO_MCP_ALLOWED_ORIGINS |
none | Browser origins that may call /mcp; a request with any other Origin header gets 403 |
--allowed-hosts a,b |
DORMO_MCP_ALLOWED_HOSTS |
localhost, 127.0.0.1, [::1] when bound to loopback; no check otherwise |
Host header values the server answers (DNS rebinding protection); any other Host gets 403. Set it to your public host name when you bind to 0.0.0.0 (Docker) |
DORMO_API_URL |
https://api.dormo.dev/v1 |
API base URL |
Endpoints and behavior#
POST /mcp: MCP messages (Streamable HTTP, protocol versions up to the current spec). The server is stateless: it issues noMcp-Session-Id, every request is handled independently, and replicas behind a load balancer need no sticky sessions. Responses are plain JSON, not SSE streams.GET /mcpandDELETE /mcpreturn405, because a stateless server has no server-initiated stream and no session to end. Clients handle this per the spec.GET /healthz:{"status":"ok", ...}for liveness and readiness probes.- Per-request narrowing:
/mcp?read_only=1and/mcp?tools=exec,read_fileshrink the tool list for that connection. They never widen what the server-wide--read-onlyor--toolsallows. - Timeouts: 15 s to receive headers, 30 s to receive the whole request, then the per-request deadline above.
- Logs: one JSON object per line on stderr, with
request_id(taken fromX-Request-Idor generated, and echoed in the response), status, duration, JSON-RPC methods, tool names andkey_fp, the first 12 hex characters of the key's SHA-256. API keys, tool arguments and tool output are never logged.
Authentication#
Today the only credential is a Dormo API key in the Authorization header. MCP's OAuth 2.1 flow (protected resource metadata, browser sign-in, audience-bound tokens) is planned for the hosted endpoint. It needs an authorization server on the Dormo side, which does not exist yet. Until then, clients that only support OAuth for remote servers should use the local stdio server.
Tools#
| Tool | Key parameters | API endpoint |
|---|---|---|
create_sandbox |
template, size, name, labels, auto_pause, always_on, priority |
POST /sandboxes |
list_sandboxes |
state, label, limit, cursor |
GET /sandboxes |
get_sandbox |
sandbox_id |
GET /sandboxes/{sandbox_id} |
pause_sandbox |
sandbox_id |
POST /sandboxes/{sandbox_id}/pause |
resume_sandbox |
sandbox_id |
POST /sandboxes/{sandbox_id}/resume |
fork_sandbox |
sandbox_id, name, labels, count (1–50, all-or-nothing; >1 returns {"data": [...]}), priority |
POST /sandboxes/{sandbox_id}/fork |
delete_sandbox |
sandbox_id |
DELETE /sandboxes/{sandbox_id} |
exec |
sandbox_id, command, cwd, timeout_seconds, background |
POST /sandboxes/{sandbox_id}/exec |
read_file |
sandbox_id, path |
GET /sandboxes/{sandbox_id}/files/content |
write_file |
sandbox_id, path, content |
PUT /sandboxes/{sandbox_id}/files/content |
list_files |
sandbox_id, path, recursive |
GET /sandboxes/{sandbox_id}/files |
expose_port |
sandbox_id, port, visibility |
POST /sandboxes/{sandbox_id}/ports |
desktop_stream_url |
sandbox_id, ttl_seconds, read_only |
POST /sandboxes/{sandbox_id}/desktop |
screenshot |
sandbox_id, scale, region |
GET /sandboxes/{sandbox_id}/screenshot (returned as an MCP image) |
computer_input |
sandbox_id, actions (same shape as input actions) |
POST /sandboxes/{sandbox_id}/input |
Behavior notes:
- Truncated output.
execin sync mode returnsexit_code,stdoutandstderr. Output larger than what fits in a tool result is truncated, and the tool says where the full log can be read (GET .../commands/{command_id}/stream). - Text files only.
read_fileandwrite_filehandle text. For binary files, use the REST API or signed URLs. - Wake on request. Tools that need a running VM wake a paused sandbox automatically.
Limiting what the agent can do#
The server accepts flags that remove tools from the list the agent sees:
npx -y @dormo/mcp --read-only # only list_sandboxes, get_sandbox, read_file, list_files, screenshot
npx -y @dormo/mcp --tools create_sandbox,exec,read_file,write_file,list_files,expose_port
For example, --tools without delete_sandbox stops the agent from deleting sandboxes. --read-only and --tools can be combined. API key scopes still apply.
The same flags work with --http. Over HTTP a client can also narrow its own tool list with ?read_only=1 or ?tools=a,b in the endpoint URL, for example https://mcp.dormo.dev/mcp?tools=exec,read_file once the hosted endpoint is live.
Docs for agents#
- A concise index of this documentation for LLMs:
https://dormo.dev/docs/llms.txt - The OpenAPI spec for generating clients: see API reference