Reference
View as MarkdownMCP server
NebulaCtrl can expose a Model Context Protocol (MCP) server so an AI client, such as a coding agent, can do most of what you do in the dashboard: find out what is wrong, change a service, add a database, set up a domain, deploy, roll back. It is off by default.
The MCP server is a layer over the same services the dashboard, CLI and REST API use. Scopes, project restrictions, roles, audit entries and the deployment rules apply unchanged. An agent can do nothing a token with the same scopes could not do through the API, and a few things you can do by hand it can never do at all (see what it cannot do).
What an AI can do#
| You want to… | The client uses |
|---|---|
| Know the state of everything, or what is broken | get_overview, find_issues, get_recent_changes, search_resources |
| Find out why a service or a deployment failed | diagnose_service, explain_failed_deployment, diagnose_server, tail_logs |
| Change a service (image, port, health check, variables, domains, volumes) | stage_changes, then review_changes and apply_changes. With the config toolset on, single edits such as service_update, variable_set, domain_add and volume_add are listed too. |
| Add a database or a domain in one step | add_database, setup_domain |
| Check that a deployment really works | verify_service |
| Go back to the previous release | rollback_service |
| Back up and restore, run a one-off command, install a template | create_backup, restore_backup, run_command, plan_template, install_template |
| Reach anything else the API offers | find_operation, then call_operation |
The tools come in three layers:
- Workflow tools (15). One call does what takes several clicks:
get_overview,find_issues,diagnose_service,explain_failed_deployment,diagnose_server,tail_logs,search_resources,review_changes,stage_changes,apply_changes,rollback_service,add_database,setup_domain,verify_service,get_recent_changes. They fetch in parallel, and when one part fails they return the rest with anerrorslist instead of failing everything. - Single-purpose tools (about 60). Named
noun_verb(service_list,variable_set,server_get,database_create), with a short description and the four MCP hints (readOnlyHint,destructiveHint,idempotentHint,openWorldHint) set per tool, so a client can tell reading from deleting. Only theobserveones are listed by default; the rest are opt-in toolsets. Large inputs, such as a service'sconfig, point tofind_operation("updateService")for the full field reference. - The long tail through two tools.
find_operation(query, optionaltag,limitup to 15) lists the operations the client may call, with name, summary, input schema and scopes.call_operationruns one byoperationId: path, query and body arguments are one flat object validated against the operation's schema,ifMatchis an integer, and names such asprojectIdorserviceIdalso accept a selector. Operations on the deny list are refused withoperation_deniedand their reason; missing scopes answerforbiddenand name them; the production approval applies to everything.
Every result has structured content and a short text version, and lists are paged by cursor ({items, nextCursor}). detail: "concise" (the default) or "full" controls how much a read returns. Results are capped at 64 KB and say truncated when cut. A client can name things instead of passing ids: a project by slug or name, an environment as project/env, a service as project/env/service, a hostname, or a bare slug when it is unique. An ambiguous name fails with ambiguous_selector and the choices instead of guessing. Selectors need projects:read, so a deploy-only token must pass ids.
The server also publishes four prompts (troubleshoot_service, explain_failed_deployment, estate_health, ship_a_change) and three resources (nebula://overview, nebula://service/{id}, nebula://deployment/{id}), and its instructions tell the client where to start, how the staged workflow goes and the production rule.
Turn it on#
Set NEBULACTRL_MCP_ENABLED=true on the control plane and restart it. While it is off, /mcp answers 404. Settings → MCP shows whether it is on, the endpoint URL and a client configuration you can copy.
Connect a client#
The endpoint is /mcp on your control plane, using the Streamable HTTP transport. It accepts an API token only, as Authorization: Bearer nct_…. Sessions and tokens in a query string are refused.
-
In Settings → API tokens, create a token. Pick a preset (see scopes) and restrict it to the projects the agent should see.
-
Put the token in an environment variable on your machine, so it is not written into a config file you may commit:
export NEBULACTRL_TOKEN=nct_… -
Add the server to your client. Replace
nebula.example.comwith your control plane's address.
Claude Code#
claude mcp add --transport http nebulactrl https://nebula.example.com/mcp \
--header "Authorization: Bearer $NEBULACTRL_TOKEN"Or commit a project-scoped .mcp.json that reads the token from the environment:
{
"mcpServers": {
"nebulactrl": {
"type": "http",
"url": "https://nebula.example.com/mcp",
"headers": { "Authorization": "Bearer ${NEBULACTRL_TOKEN}" }
}
}
}Cursor#
In .cursor/mcp.json (project) or ~/.cursor/mcp.json (all projects):
{
"mcpServers": {
"nebulactrl": {
"url": "https://nebula.example.com/mcp",
"headers": { "Authorization": "Bearer ${env:NEBULACTRL_TOKEN}" }
}
}
}Codex#
In ~/.codex/config.toml:
[mcp_servers.nebulactrl]
url = "https://nebula.example.com/mcp"
bearer_token_env_var = "NEBULACTRL_TOKEN"Any other client#
Clients that read an mcpServers block with a URL and headers take the same shape as the Cursor example, with the token pasted in place of the variable. Settings → MCP generates this snippet with your real endpoint.
Offering more tools#
A client is offered the core and observe tools by default, which is enough to look, diagnose, stage a change, apply it and roll back (the editing happens through stage_changes). For an agent that edits configuration, add ?toolsets=core,config to the URL, for example in Claude Code:
claude mcp add --transport http nebulactrl "https://nebula.example.com/mcp?toolsets=core,config,observe" \
--header "Authorization: Bearer $NEBULACTRL_TOKEN"and the same URL in the url field of the Cursor and Codex configs. List every toolset you want: an explicit list is exactly those (see toolsets). Add data for databases and backups, servers for server management.
Toolsets#
A long tool list costs the model attention and context, so tools are grouped into toolsets. By default a client is offered core and observe only (about 32 KB of tool descriptions for an admin token); config, data and servers are opt-in:
| Toolset | Contains |
|---|---|
core |
The 15 workflow tools, find_operation, call_operation and whoami. Always the best place to start. |
config |
Projects, environments, services, variables, domains, volumes, links, previews, tasks, run_command, templates, Compose import, and clone and move (cloneService, cloneEnvironment, moveService through call_operation; a move into or out of production needs approval). |
data |
Databases, backups and restores (including volume backups and schedules). |
servers |
Servers, the fleet overview, and draining a server. |
observe |
Deployments, service and server metrics and analytics, the audit log and the notification channel list (the last two need admin). |
Three things decide what a client sees:
- Scopes. A tool is listed only when the token holds every scope it needs. A read-only token never sees
apply_changes. - Toolsets, set in the URL. There is no per-token setting. Without
?toolsets=the client listscoreandobserve. To opt in to more, name all you want:https://nebula.example.com/mcp?toolsets=core,config,data,servers,observe. An explicit list is exactly those toolsets (core,config,data,servers,observe; an unknown name answers400 invalid_toolsets), so?toolsets=corelists about 25 KB of core tools only. The URL cannot add anything the token's scopes do not allow. - The long tail. Tools that are not listed stay reachable. With no
?toolsets=,find_operationandcall_operationreach every operation the token may use that is not denied, with the same scope and approval checks. With an explicit?toolsets=, they are restricted to those toolsets too, so the URL doubles as a fence.
The staged workflow#
Changes to a service, its variables, domains, volumes and links only stage. Nothing reaches a running container until the changes are applied, the same as in the dashboard, where staged changes wait behind the "N changes to apply" bar.
- Stage.
stage_changestakes theservice, theifMatchgeneration the client last saw, and any ofconfig,variables,domainsandvolumes. A variable can be generated (generate: {length, charset}) or deleted. Each edit returns what changed:{staged, generation, appliedGeneration, change, pendingCount, next}. A service withgenerationaboveappliedGenerationhas staged changes. If someone else edited the service in between, theifMatchno longer matches and nothing is staged. - Review.
review_changes(aserviceor a wholeenvironment) shows every pending change field by field, with secrets masked. - Apply.
apply_changesis the only way from staged to running. It takesservices(each aserviceselector plus theexpectedGenerationthe client reviewed) or a wholeenvironment(project/env) together withconfirmPending, the number of staged services the client reviewed (whatreview_changeslists for that environment).waitSeconds(at most 50) waits for the result.noCache: truebuilds the git services of that call without cache (--no-cache --pull, no reuse of a build server's earlier image) for these deployments only (deploying without cache); it applies to services with staged changes, like the rest of the call. If someone changed a service after the review, the generation no longer matches; if the number of staged services differs, the call fails withplan_changed. Either way nothing deploys and the client reviews again. - Verify.
verify_servicerequests the service's own hostnames through the proxy and reports whether it answers. It refuses loopback, private and link-local addresses (unless it is the service's own server), follows no cross-host redirects and times out after 8 seconds.rollback_service(service,to:previous, a release id, a deployment id or a commit's first 7 or more characters;dryRunpreviews it) returns to an earlier release. A rollback restores the application image and configuration only; data is not rolled back.
A deployment that outlasts waitSeconds is still running. The result says so and names the next tool to call; it never reports success it has not seen. outcome_uncertain means a command was accepted but its server went offline before reporting, so check before retrying.
Secrets: variable_set can generate a value (generate: {length, charset}; length 1 to 1024, default 32; charset one of alnum, hex, base64, base64url, digits, lower), or take a value you typed. A secret value is never returned by any tool, never logged and masked in audit entries and in log output. service_update merges the config fields you give into the current config, so you send only what changes.
Production approval#
An AI client that changes a production environment needs a person to approve it. The check lives in the domain services, not in the tools, so a model cannot route around it (not even through call_operation), and it fails closed: if the approval store cannot be reached, the action is refused.
What is guarded, when the environment is production: deploy and apply, deploy staged changes, rollback, restart, stop, deleting a service, restoring a backup, an in-place volume restore, running a task or one-off command, deleting a volume, domain, environment or project, un-flagging an environment as production, upgrading a database, rotating credentials, enabling maintenance mode, changing scaling, deleting a template instance, importing a Compose file with deploy, cancelling an unfinished deployment, and draining a server that runs production services. Non-production environments need no approval.
When a guarded action is requested:
- Elicitation. If the client supports MCP elicitation (the 2026-07-28 protocol's multi round-trip flow), NebulaCtrl asks it to show you a form with an Approve checkbox that states exactly what will happen: the service, the environment, the changes and the strategy. You approve or decline in the client, and the call continues.
- The approval URL. Older clients, and clients that cannot elicit, get the tool error
confirmation_requiredwith the plan and a link,https://nebula.example.com/approvals/{id}. Open it signed in to the web app: the page shows the same plan with Approve and Decline. After you approve, the client repeats the same call and it goes through. Owners, Admins and Developers with deploy rights on the project can decide; Viewers can only read. An agent can never approve: the approval operations are not available over MCP. - Nothing a model can type. The model cannot pass a
confirm: truefor production. An approval is single-use and bound to the token, the tool, the exact arguments and the generations that were reviewed. It lives at most 10 minutes from the request. If the arguments differ on the retry, or the service's generation moved in between, it is void and the client has to ask again. A request you decline keeps refusing the same call for one minute.
Over plain HTTP the same refusals appear as 409 approval_required, 403 approval_declined and 503 approval_unavailable. Every outcome is written to the audit log as approval.requested, approved, declined, expired, refused, error or used.
Token settings#
When you create a token in Settings → API tokens, the Production changes section has two settings:
- Let MCP clients change production without approval (
allowProductionWithoutApproval) is for an unattended job such as a nightly deploy. It is off by default, only an Owner or Admin can turn it on, and turning it on is audited. It is honoured only while the token's creator is still an Owner or Admin. Keep it off for any token a model uses interactively. - Also ask for approval on REST calls (
requireProductionApproval) applies the same guard to the token's REST and CLI calls, for any member who creates it. REST tokens are not guarded by default, so CI and deploy hooks keep working. Signed-in browser sessions are never guarded by this.
Scopes and token presets#
Each tool needs the scopes of the REST operations it calls, and the service re-checks them on every call. logs:read is new: it gates reading runtime, build and task logs, both in tail_logs and in the diagnostic tools that include log excerpts, because logs can hold data a person would not paste into a chat. Existing tokens that could read projects received it when you upgraded, so nothing stops working.
| Scope | Lets the client |
|---|---|
projects:read |
Read projects, environments, services, deployments, variables (never secret values) and templates. |
servers:read |
Read servers, their metrics and proxy state. |
logs:read |
Read logs (tail_logs), and log-bearing diagnoses. get_task_run needs it together with projects:read. |
projects:write |
Stage changes to projects, environments, services, variables, domains and volumes; install templates (deploy: true also needs deployments:write); restoring a backup needs it together with deployments:write. |
deployments:write |
Apply changes, restart, stop, back up, run commands (run_command; a selector in it needs projects:read, so a deploy-only token passes ids). |
deployments:rollback |
Roll back. |
servers:write |
Change server settings. |
proxy:write |
Change proxy routes, certificates and ports. Never in an MCP preset. |
admin |
Everything the token's creator's role holds. |
The presets in Settings → API tokens:
- MCP read-only:
projects:read,servers:read,logs:read. Look and diagnose; change nothing. Start here. - MCP deploy: adds
deployments:write: apply changes that are already staged (say, from the dashboard), restart, run commands. It cannot stage edits itself. - MCP configure: adds
projects:writeas well, so the client can stage edits to services, variables, domains and volumes and then apply them. Production changes still need approval.
No preset includes deployments:rollback or proxy:write; add deployments:rollback under Custom if you want the client to roll back.
A token is also limited to its projects and by its creator's role, which is checked again on every use. See API tokens.
What it cannot do#
Some operations are never available over MCP, whatever the token holds, and find_operation does not list them. They would let a model (or whoever steers it through a prompt injection) take over the account, read a secret, or reach the host itself. You can still do all of them in the dashboard. Each one is denied for one of these reasons:
| Not available | Why |
|---|---|
Sign-in, two-factor, passkeys, password reset, OIDC, first-run setup (auth-plumbing) |
They exist for people signing in. A token never authenticates a person. |
Your own account: profile, password, two-factor and passkey management, switching or listing teams, accepting an invite, approving a production approval (session-only) |
Browser account management. An agent must not change how you sign in, and a principal must never approve its own production change: only a person in the web UI can. |
Create API tokens, enrollment tokens, team invites and member password reset links; platform admin links; enable or rotate deploy hooks and webhook secrets; start a GitHub App manifest (mints-credential) |
They return a credential, link or secret URL once. An agent holding one could escalate beyond its own scopes or add a machine to your fleet. |
Reveal a database export or public connection string, a volume backup recovery key, platform service credentials (reveals-secret) |
Secret values stay write-only. Copy them in the dashboard, which audits it. |
Write Cloudflare, Tailscale, DNS, registry, git, secret manager, backup destination, log drain, notification channel or MaxMind credentials; upload a TLS key; instance backup and instance settings; SSO provider setup; teams, member roles and removal, team security; platform services (instance-admin) |
A credential would pass through the model's context, and these settings change the platform for everyone or decide who may do what. Reading most of them stays available. |
The server unrestricted switch, the host shell switch, applying or reverting host hardening, upgrading a server's packages and rebooting it (host-root) |
They give or change root-level access to the host or widen what containers may do on it. The web terminal and file upload are not exposed over MCP at all. |
Following a log stream (streaming) |
MCP calls return one result. tail_logs returns a bounded, redacted tail instead. |
An HTML page, such as a maintenance-page preview (binary) |
A tool result is JSON or text. |
Raw deploy, deploy staged, rollback, log history endpoints (superseded-by-composite) |
apply_changes, rollback_service and tail_logs do the same job with the review, the production approval and the result handling the raw endpoint lacks. |
One exec path remains on purpose: run_command runs a command you name in a throwaway container of the service's image, with the service's environment. That is shell-equivalent for that service: it can read the service's secrets and change its data. It needs deployments:write, is subject to the production approval and is audited with the command line. Leave deployments:write off any token an agent uses on untrusted input.
Everything else is available, through a tool or through call_operation, behind its scopes and the production approval.
Limits and safety#
- Rate limit. 60 requests per minute per token. Beyond that the call fails with a rate-limit error; wait and retry.
- Output. A result is capped at 64 KB.
tail_logsreturns at most 500 lines, redacted. - Logs are untrusted text. A container can print anything, including text that looks like an instruction. Log and command output comes back as
{untrusted: true, nonce, source, notice, text, lines}, with the text between<untrusted-log nonce=…>markers and a fresh randomnonceon every call, so the client can tell it from tool output. It stays redacted. - Audit. Every call is written to the audit log as
mcp.tool_callwith the arguments redacted (anything that may hold a secret, a value or a command is replaced) and the client's name. The service's own audit rows carry the actor typemcpand the label "token via client". A network client cannot claim the MCP actor: it is set in-process, never by a header. - Retries. Mutating calls are idempotent: a repeated call returns the first result rather than deploying twice.
- Errors carry a
codeand a message.
How it compares to Coolify's MCP#
Coolify ships a first-party MCP server too, and its newest versions cover a lot: around 77 tools for servers, applications, databases, services, environment variables, deployments and scheduled tasks, plus prompts and resources. Both are scoped to a token and audited.
Where NebulaCtrl differs:
- Staging. Edits stage and
apply_changesships them after a review at a specific generation. Coolify changes a variable and then you deploy, without a diff to review. - Production approval. Ours is a person approving a plan that is bound to the exact arguments, over elicitation or a web page. Destructive tools in Coolify rely on a
confirmargument the model fills in. - Hints. Every tool states whether it reads, writes, deletes or repeats safely, tool by tool.
- Diagnosis. Workflow tools return the cause with the evidence (failing health check, last log lines, recent changes), including deployment logs, capped, redacted and marked untrusted.
- One surface, filtered. Scopes and toolsets decide what each token lists, and the rest is reachable through
find_operationrather than being absent.
Where Coolify is ahead: its server has been in use longer, and it also manages notification channels, S3 storage and git sources from MCP. We deliberately keep credential writes, token minting and host shells out of MCP, so those stay in the dashboard. (Based on Coolify's published source and docs at v4.5.0, October 2026.)
Renamed tools#
If a client or prompt still uses an older tool name, these are the replacements. The old names are gone, with no aliases.
| Old name | Now |
|---|---|
list_projects, list_environments, list_services |
project_list, environment_list, service_list |
get_service, get_deployments, get_deployment_status |
service_get, deployment_list, deployment_get (adds terminal and outcome_uncertain) |
list_servers, get_server_status |
server_list, server_get |
get_service_traffic, restart_service |
service_analytics, service_restart |
get_service_logs |
tail_logs (needs logs:read) |
plan_deployment |
review_changes |
deploy_service |
apply_changes |
rollback_deployment (with confirmReleaseId) |
rollback_service |
run_command, get_task_run, the four backup tools and the three template tools keep their names.
Machine-readable docs#
These docs are also available to agents as plain text: see llms.txt for an index, llms-full.txt for everything in one file, and the OpenAPI document at /api/v1/openapi.json.