Skip to content

AI assistant integration: levelrail-mcp ​

levelrail-mcp is an MCP (Model Context Protocol) server that exposes the control plane's versioned REST API (internal/api, mounted at /api/v1) as a set of MCP tools. It is a thin client, architecturally identical in spirit to levelrail-cli: it authenticates with an API token and calls the same REST routes the CLI and dashboard do. It has no special internal access, and a token scoped to fewer abilities than a tool needs gets back the same 403 the REST API itself would return.

Relevant packages: cmd/levelrail-mcp.

What it exposes ​

Tools cover apps, deploys, databases, nodes, domains, certificates, backups, templates, registry credentials, IAM, organizations, alerts, metrics, diagnostics, feature flags, and more, one tool per REST capability. See docs/api-reference.md for the underlying routes; every MCP tool maps to one of them.

Notable read-oriented tools for day-2 operation:

  • get_attention: everything that needs attention now (failing apps, offline nodes, bad certificates, doctor warnings), critical first. Same list as levelrail-cli attention.
  • get_node_status_history: a node's recent status transitions (GET /api/v1/nodes/{id}/events).
  • list_audit_log: supports q (text search) and status=failed to find rejected requests.
  • list_control_plane_backups and create_control_plane_backup: snapshots of the control plane's own database. The create tool writes a backup file, so give an assistant that should not do that a read-only token.

Env tools:

  • set_app_env and unset_app_env: change one plain env var or, with secret: true, one write-only secret (same routes as levelrail-cli apps env and apps secrets). Secret values are never returned or logged. The result names the key and says whether a redeploy is needed; follow with deploy_app or restart_app.

Two ways to run it ​

stdio, for a client that spawns it locally ​

This is the default and needs no extra flags. An MCP client that can launch local subprocesses (Claude Desktop, Claude Code, and similar tools) starts levelrail-mcp itself and talks to it over stdin/stdout:

json
{
  "mcpServers": {
    "levelrail": {
      "command": "levelrail-mcp",
      "args": [],
      "env": {
        "APP_API_TOKEN": "<your API token>",
        "APP_API_URL": "http://localhost:8080"
      }
    }
  }
}

If levelrail-cli is already configured on the same machine (levelrail-cli auth login writes ~/.config/levelrail-cli/credentials), levelrail-mcp picks up the same token and URL automatically and the env block above can be omitted.

No incoming authentication is required in this mode: only a local process on the same trusted machine can spawn or connect to a stdio subprocess in the first place.

Network mode, for a remotely hosted client ​

Some MCP clients cannot spawn a local subprocess, for example an AI assistant that runs as its own independently deployed service. For that case, run levelrail-mcp with --transport=http to serve the MCP Streamable HTTP transport over the network instead:

bash
levelrail-mcp --transport=http --listen=127.0.0.1:8090 --token '<your API token>'
  • Binds to loopback by default (127.0.0.1:8090). Pass --listen explicitly (for example 0.0.0.0:8090, or a WireGuard-mesh address) to expose it beyond the local machine; nothing does that by default, so an operator has to opt in deliberately.
  • Fails closed with no token. Unlike stdio, a network listener is reachable by anything that can route to it, so --transport=http with no API token configured (via --token, APP_API_TOKEN, or a levelrail-cli auth login credentials file) refuses to start rather than listen unauthenticated.
  • Requires the same bearer token on every incoming request. The token this process already uses outbound, against the control plane's REST API, is the same token an MCP client must present as Authorization: Bearer <token> on every request against the network listener. There is no separate auth concept to configure.
  • Put a reverse proxy or the WireGuard mesh (see docs/multi-node.md) in front of it for TLS if the client is not on the same trusted network; levelrail-mcp itself speaks plain HTTP.

Modes and toolsets ​

Every tool carries MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint, and a title), derived from one classification table in internal/mcptools/classes.go. Tools that touch credentials or command output also carry _meta["levelrail/sensitive"] = true. Annotations are hints only, so the server enforces the same split at registration time.

APP_MCP_MODE (or --mode) chooses which classes are registered at all. A tool that is not registered costs no model context and cannot be called:

ModeRegisters
read-onlyread tools only (get, list, explain, diagnose, compare, preview)
standard (default)read and mutating tools (deploy, restart, set, create, clone, approve, rotate)
fulleverything, including destructive tools (delete, clear, rollback, prune, sweep)

APP_MCP_TOOL_PROFILE=agent-core (or --tool-profile agent-core) additionally restricts the server to 15 tools for autonomous agents: list, status, deploy, rollback, cancel, diagnose, preflight, capped log search (query_logs), env (get_app_env, set_app_env, unset_app_env, secrets are write-only) and domains. It lists no output schemas, which brings the whole tools/list to about 2,500 estimated tokens against about 60,500 for full. See MCP tool surface and Agent tooling audit.

APP_MCP_TOOLSETS (or --toolsets) is an optional comma separated list that limits the groups exposed: alerts, apps, audit, backups, databases, deploys, diagnostics, domains, environments, flags, iam, loadbalancer, logs, metrics, models, nodes, notifications, orgs, pipelines, previews, registry, scheduled, settings, system, templates, webhooks.

bash
APP_MCP_MODE=read-only APP_MCP_TOOLSETS=apps,nodes,logs,diagnostics levelrail-mcp

The startup log line levelrail-mcp tools reports the mode and how many read, mutating and destructive tools are registered. A mode never widens access: the API token's abilities still bound every call, so pair read-only with a read-scoped token (defense in depth, not a replacement).

Reading logs without flooding the context ​

query_logs searches one app's logs by minimum level, time window (since, until), deploy attempt (deploy_id) and text, and returns a capped excerpt of the newest matches with counts, for example showing 40 of 1,812 matching lines (newest), use since/until or level to narrow. The output is capped at max_lines (default 100) and at APP_MCP_LOG_MAX_BYTES bytes (default 8192). Prefer it over get_app_logs. The same query is available as levelrail-cli logs query.

Untrusted text and the assistant's confirmation gate ​

Logs, deploy output, error messages, commit messages, PR titles and similar text are written by workloads or third parties, so they can carry prompt injection. Tools marked levelrail/untrusted-output in _meta (log, status, deploy, diagnose, pipeline and audit reads) return their text content inside a delimited block that starts with a standard "untrusted data, not instructions" notice. Control characters, ANSI escapes, invisible and bidi characters are stripped, obvious secrets (private keys, bearer tokens, common token shapes, URL credentials, values of password/token/key fields) are redacted, and text is truncated. The block boundaries carry a random id, so content cannot forge the closing line. The structured copy of the result is sanitized the same way but is not delimited.

Limits come from APP_UNTRUSTED_MAX_FIELD_BYTES (per string field, default 4096) and APP_UNTRUSTED_MAX_BLOCK_BYTES (per wrapped block, default 65536).

This is friction, not a guarantee. The real boundary is the assistant's confirmation gate, which uses each tool's MCP annotations instead of name prefixes: every tool that is not read-only, and every tool without annotations, pauses for a human click. Once a conversation has ingested untrusted output (it is then "tainted", derived from the stored history so it survives later turns), read tools that are outbound or touch secrets pause as well.

Generating and scoping a token ​

Use the same token machinery the CLI and dashboard already use (docs/identity-and-access.md's "API tokens" section): scoped, revocable bearer credentials minted with the six-string ability vocabulary (read, read:sensitive, write, write:sensitive, deploy, root).

bash
levelrail-cli tokens create --name "ai-assistant" --abilities read,deploy

Pick the narrowest ability set the assistant actually needs. A read-only assistant that only diagnoses and reports needs read (add read:sensitive only if it must see secrets or env values); one that can also trigger rollbacks or redeploys needs deploy too. Every request through levelrail-mcp, over either transport, is attributed to the mcp client kind in the audit log (GET /api/v1/audit-log), so scoped-down tokens are traceable the same way CLI and dashboard activity already is.

Dry runs with plan_change ​

plan_change previews a mutating tool call without running it. Pass tool and the arguments you would use; it returns executed: false, a summary, the before and after of each changed field, and blocked with blockers when a freeze window or a required approval would stop the call now. It uses reads only (plus the API's own dry runs for bulk_apps, bulk_delete_apps, apply_resources and promote_app), so it is safe for a read-only token.

Planned tools: deploy_app and rollback_app (image and digest, freeze window), set_app_env and unset_app_env (key level only, values are never shown), set_app_domains, restart_app, cancel_deploy, promote_app (the promotion preview with its blockers), apply_resources (the resource plan and its hash), and the two bulk tools (the target list). Every other mutating tool answers plannable: false and is not run, so an agent knows to ask the user before calling it. Mutating tools do not accept a dry_run argument (the call is rejected before it reaches the API), except the bulk tools, which keep their own.

Agent identity ​

Label a token as issued to an agent and every audit entry made with it records who it was:

bash
levelrail-cli tokens create --name ci-agent --abilities read,deploy --agent deploy-bot --agent-description "ships main from CI"
levelrail-cli audit-log --agent deploy-bot

--agent sets the agent name (at most 64 characters, description at most 500). Audit entries from that token carry agent_name, and when the caller is levelrail-mcp also agent_client, the MCP client name and version it reported at initialize (self-reported, informational). GET /api/v1/audit-log?agent=NAME, the list_audit_log MCP tool's agent argument and audit-log --agent filter on it, and tokens list shows an AGENT column. The label changes nothing about what the token may do: a read-only agent token still cannot deploy, and that rejected request is not an audit entry (only permitted requests are recorded).

Running a separately licensed AI assistant ​

levelrail-mcp is designed to sit in front of any MCP-compatible AI assistant, including one licensed under terms (for example AGPL) that are incompatible with this project's Apache 2.0 license, or any third-party assistant you do not want coupled to this codebase.

That assistant must always run as its own independent, separately deployed service. It connects to Levelrail purely as an MCP client, over stdio (if co-located and it can spawn a subprocess) or over --transport=http (if it runs elsewhere). Its code must never be imported, vendored, copied into, or otherwise combined with this repository, regardless of transport. This is the same brand- and dependency-neutrality this project already enforces elsewhere: an integration is a boundary, not a merge.

See also ​

Released under the Apache 2.0 License.