Skip to main content

MCP server

The Gateway serves a Model Context Protocol (MCP) server, so an agent can operate Orbit without the CLI. Every Gateway API operation is one tool. A tool call runs the same API request that the matching CLI command sends. Identity, access grants, validation, redaction, and Activity therefore apply to it in the same way. The Project’s development deploy steps are available as project-dev-deploy-step-list, project-dev-deploy-step-create, project-dev-deploy-step-update, and project-dev-deploy-step-destroy. Supply the numeric project; updates and removal also take the step name as step. Create and update accept required as a JSON boolean, so false stays false through MCP dispatch. process-update replaces an installed Process specification. Supply its numeric ID as process and the complete replacement fields; it keeps the Process identity and desired state. Both process-create and process-update accept Docker environment as an object such as {"EMPTY":"","LABEL":" literal "}, with string values preserved. Omit or set working_directory to null to use the Docker image default. Responses expose environment names with redacted values. Update in place defines omitted fields, refusals, and retry recovery. node-settings takes apps, the apps root. node-wireguard-key takes the numeric node ID and wireguard_public_key, a canonical base64 encoding of exactly 32 bytes. It dispatches PATCH /api/v1/nodes/{node}/wireguard-key to set an unmanaged peer’s desired public key and reconverge the hub. The API refuses Gateway Nodes, managed peers, missing WireGuard addresses, and collisions with another peer or the configured hub key. Activity input is empty. A projection failure can leave the new key stored; replaying the same key retries convergence rather than skipping it. Follow WireGuard key recovery if the change interrupts the caller’s management path. The gateway-release-rollback tool takes the retained release selector without a force input. It switches code only; database restore follows Gateway recovery.

Endpoints

Both endpoints speak MCP over streamable HTTP and offer the same catalogue. Their JSON-RPC envelopes are limited to 15 MiB (15,728,640 bytes), checked after active-peer authorization and before parsing. An oversized envelope receives HTTP 413 without invoking a tool. Dispatched API requests still apply their own limits and Node access checks. This envelope limit admits the full 10 MiB decoded Project Document upload, including base64 and JSON-RPC overhead. search_tools returns exact tool names with their input schemas. An empty query browses the catalogue. execute_tools takes {"calls": [{"name": "...", "arguments": {...}}]}, runs up to 25 calls in order, and stops at the first error. A nested empty object in a tool argument, such as {"options":{}}, stays an object through a direct tools/call and through execute_tools, and an empty list stays a list. The tool result text keeps that shape. /mcp/search limits the output of one call to 65,536 bytes. execute_tools stops when the combined results pass that limit, so a large list result can fail through /mcp/search and still work through /mcp. One MCP request has one 570-second command deadline, the same deadline as one API request. PHP-FPM ends a Gateway request after 600 seconds. All tool calls in one execute_tools batch share that deadline. A tool call cannot extend or reset it. Forward work stops 20 seconds before the deadline, which leaves that time for cleanup. A call that starts after the batch has used up its forward time returns the tool error command.deadline_exceeded (status 504) and does not run. The batch then stops at that call. Send long tools, such as gateway-release-smoke or instance-deploy, as separate requests.

Tool list changes

A client fetches the tool list when it connects and keeps it. A release that adds, removes, or changes a tool in tools.json, or an extension switch, changes the list on the Gateway at once. The server declares tools.listChanged as false, because it never sends notifications/tools/list_changed. A connected client keeps its list until it lists the tools again, which most clients do only when they connect. The Gateway never ends a session because the list changed. Every request is served against the current list:
  • A call to a tool that still exists runs as usual.
  • A call to a tool that was removed or renamed receives HTTP 400 with the JSON-RPC error -32602 Tool [name] not found. The message tells the client to list the tools again. The Gateway runs nothing.
  • A call whose arguments a changed operation rejects receives the operation’s usual tool error, such as validation.failed.
To see a new tool, reconnect the MCP server or start a new session. In Claude Code, use /mcp and reconnect orbit. /mcp/search lists the same two tools after every change, and search_tools always searches the current catalogue.

Identity and access

The caller is the Node whose WireGuard address the connection comes from. There is no token and no login.
  • A connection from an address that is not an active Node receives HTTP 403 with peer.identity_unknown, before it can list tools.
  • gateway-desired-fleet-state needs no access grant. Any active Node may read the desired fleet state.
  • Each tool call runs as an internal API request with the caller’s address. The API checks the caller and its access grants again. A missing grant returns the API’s 403 with node_access.required as the tool error.
  • The Gateway certificate chains to the Orbit root certificate authority (CA). Orbit installs that root in the system trust store only on a Node where it converges an Instance, Route, router, ingress, or custom proxy certificate. On any other machine, run orbit gateway:trust first, or point the client at the root CA file.

Tools

A tool’s name is the operation ID in the API reference, <family>-<verb>, such as node-list, instance-show, or process-restart. Its input is one object with the operation’s path parameters, query parameters, and JSON body fields. An input the API validates as a boolean is a JSON boolean in the tool schema. The caller sends that value as a JSON boolean. The Gateway serves each tool’s input schema exactly as bin/mcp-tools generated it in resources/mcp/tools.json, with empty schemas encoded as JSON objects ({}), not lists ([]). The server puts each value where the operation expects it. A GET tool is marked read-only, and a DELETE tool is marked destructive. project-document-storage-show and project-document-storage-update expose the Gateway-wide Project Documents storage configuration. Both require Gateway access and take no Project selector. Update accepts endpoint, region, bucket, and the paired write-only fields access_key_id and secret_access_key. Omitting both credential fields preserves them. Results contain only redacted configuration and cleanup status; show does not probe the provider. Update verifies access before saving and returns project_documents.storage_unavailable without provider diagnostics on failure. tool-scan reads the enrolled account’s Homebrew formulae, casks, and Vite+ global packages for one Node. Its only input is node_id. The call does not create a Tool row. tool-adopt records ownership of one installed package. Its inputs are node_id, manager, package, and optional version_constraint. The call does not install, update, or remove the package. A successful call returns the API’s JSON document as text, usually {"data": ..., "meta": {"request_id": ...}}. A failed call returns a successful MCP transport response whose tool result has isError: true. Its content carries the API error envelope with the status added; the HTTP response itself is not the API’s error status:
For a disabled extension, a direct tools/call returns isError: true and content containing status: 409 and error.code: extension.disabled; HTTP 409 is reserved for the underlying API request, not the MCP transport. The same tool error is returned by execute_tools when a stale call names a tool that was filtered from the catalogue, and execution stops at that call. The server checks the generated tool’s extension metadata before catalogue filtering can turn a stale direct call into a generic not-found error. instance-rename records an externally renamed development branch and can move the Instance’s own Route, including a generated Route. Send instance and at least one of branch or domain. The Gateway checks the local checkout before changing its records; the tool never runs git branch -m. See Instance rename. instance-deploy and instance-rollback stream progress over HTTP. As tools they run to completion and return their progress events in order under events. The last event carries the result: a result event whose status is succeeded or failed. Step output arrives as output events with base64 data_base64. Through both endpoints, these events may use half of the 65,536-byte /mcp/search output limit. When they would use more, the reply keeps the first 16,384 and the last 16,384 bytes of them. One {"type": "output_truncated", "dropped_bytes": N} event replaces the middle and gives the number of output bytes it dropped. Phase events and the result are never dropped, so a long deploy fits in an execute_tools reply. To read more of the output, call instance-deployment-show, which returns the stored deployment log. Machine callbacks and browser pages have no tool: channel signatures, the Node agent endpoints, live log streams, the Grafana access check, the Instance wake page, the Schedule completion report, the annotation event stream, the task agent stream, and the two GitHub App registration pages. bin/mcp-tools lists them. The tasks-* tools belong to the Gateway-switched tasks extension, except tasks-status, which is a core assistance view that remains available while tasks is off. Each tasks-status entry carries assistance_kind and assistance_question (Direction requests). The task definition tools are tasks-definition-list, tasks-definition-show, tasks-definition-create, tasks-definition-update, and tasks-definition-destroy. List and show accept any authorized peer. Create, update, and destroy need Gateway access. None of them starts a task. tasks-question-list accepts any authorized peer. tasks-question-close needs Gateway access and closes a stale question as answered or superseded (Close a question). While the tasks extension is off, the API refuses these operations with HTTP 409 extension.disabled. The proxycli-* tools belong to proxycli. The vault-* tools belong to Vaultwarden and need Gateway access; no vault-* result contains a secret. The conn-* tools serve the Conn layer; every active Node may call them without a node access edge. A disabled extension is omitted from both tools/list and /mcp/search; a direct tools/call or stale execute_tools call returns an MCP tool error with isError: true, content carrying status: 409 and error.code: extension.disabled, and does not execute the API route. tasks-create requires Gateway access. tasks-update and the tasks-subtask-* tools need access to the Node that holds the task’s Instance, or to the Gateway for a task without one. tasks-create, tasks-subtask-create, and tasks-subtask-update take each subtask’s typed deliverables as a deliverables array, and tasks-show returns them. tasks-complete takes the numeric group ID and requires Gateway access. It completes a settling task, or a running or reviewing task whose watched pull request has merged or closed. On the latter path it cancels open subtasks and stops their running agents and checks before workspace removal. Other tasks return tasks.not_settling; a failed stop returns tasks.subtask_interrupt_failed. The completion contract defines the durable receipt and cleanup retries.

Connect a client

Claude Code, from a Node or a WireGuard peer that trusts the Orbit CA:
A client that takes a JSON server entry:
A Node.js client reads extra CAs from NODE_EXTRA_CA_CERTS. Point it at the Orbit root CA file when the client does not use the system trust store. GitHub feedback consumption runs in the Tasks scheduler, not through a new tool. Existing task tools return its fixup identity and findings in the ordinary subtask fields. The reviewer allowlist is Gateway configuration, not an MCP input. Feedback adds no review-submission, dismissal, re-review-request, or merge tool. Regenerating the catalogue and task-action manifest must preserve that boundary.

Keep the catalogue current

bin/mcp-tools generates apps/gateway/resources/mcp/tools.json from docs/openapi.json. The same run writes apps/gateway/resources/tasks/actions.json, the operation ids marked x-orbit-task-action, and --check fails when either file is stale. Tool descriptions also come from the OpenAPI document. The tasks-create and tasks-update descriptions follow the Tasks contract, with Project ownership and the status, subtask, deliverable, and Coder notification requirements. The tasks-cancel description follows cancelling a stuck task: forced workspace removal also removes an eligible pending or failed exclusive Route of a source_resolved workspace, while an active or shared Route still refuses. Each generated operation records its extension slug, or null for an untagged (core) operation. The normal MCP server and OrbitSearchServer filter that metadata against the Gateway enabled set before building their catalogues. The route-create tool accepts either instance_id and domain for an app Route or node_id, domain, and one of upstream or process_id for a custom proxy. It does not accept project_id or cluster_id on creation. After a change to a route, form request, data class, or operation description, regenerate both:
Continuous integration runs bin/docs-openapi --check and bin/mcp-tools --check. A Gateway test fails when an API route has no tool or a tool has no route. The inputs to bin/docs-openapi are on API reference generation.

Why it works this way

These reasons explain the design. Check them before you propose a change.

Tools generated from the API description

More than a hundred hand-written tool classes would repeat the form requests’ rules and drift from them with every API change. Generating the catalogue from the OpenAPI document keeps one source. A weak tool description is fixed in the form request, the data class, or the CLI command text that API reference generation copies into the operation.

Tool calls go through the HTTP kernel

Identity, access grants, validation, redaction, and Activity live at the HTTP boundary. A tool that called Actions directly would need a second copy of each. An internal request with the caller’s address gets them all.

No token or OAuth

The API identifies a caller by WireGuard address. A second credential would create a second authorization model for the same actions.

No MCP server in the CLI

An MCP server that wraps CLI commands keeps the process start and the CLI install on the agent’s machine, and parses output that the API already returns as JSON.

No pushed or forced tool list refresh

A pushed notifications/tools/list_changed needs a stream that stays open: GET /mcp in the initialize revisions, or subscriptions/listen in the 2026-07-28 revision. Each open stream would hold one of the Gateway’s eight PHP-FPM workers, and PHP-FPM ends every request after 600 seconds. Sending the notification inside the reply to an ordinary request needs no held stream, but the stateless server would then send it on every later request of that session. Ending a session with HTTP 404 makes a spec-compliant client initialize again and list the tools, but a client that does not handle the 404 fails every call until it restarts. A stale list costs a client at most a reconnect; a failing session stops its work. The Gateway therefore serves every session and explains a missing tool in the error.

Typed tools, not one command tool

One tool that takes a command string would discard the input schemas, which are the main benefit of MCP for an agent. A client with a small context uses /mcp/search instead.