Belune

MCP Server

Let an AI assistant inspect your projects, deployments, and logs — and, with a token that allows it, deploy and configure applications — through the Model Context Protocol.

Belune has a Model Context Protocol server at /mcp — one endpoint an MCP-compatible client (Claude, and others as they add support) can call to inspect your projects, applications, deployments, and infrastructure state. It authenticates with the same personal access tokens as the rest of the API. With a read token it can only look. With a deploy or write token it can also act — start a deployment, change an application's settings, start or stop it, and set its environment variables — and it can never delete or restore anything.

Connecting

The fastest way is Account → Connect an AI Assistant, which mints a read-scoped token and gives you a ready-to-paste command. The dialog's project picker optionally pins the token to one or more projects — leave it on "All projects" to give the client the same reach you have.

claude mcp add --transport http belune https://belune.example.com/mcp \
  --header "Authorization: Bearer belune_pat_..."

A read token is enough to inspect, and it is the safe default. To let the assistant make changes, mint a token with the scope the tools you want need — see Tools That Make Changes — from Account → Personal Access Tokens, and connect with that instead.

Both log tools strip terminal colour codes, as the dashboard does, and otherwise return output verbatim. Tools that read logs send them to whichever client you connect. Logs often contain connection strings and API keys — only connect a client you trust with that. This is the same warning the dialog shows, at the moment you actually connect something, rather than on every token you ever create.

To connect a different client, or build the command by hand, mint a token from Account → Personal Access Tokens and point your client at https://belune.example.com/mcp with Authorization: Bearer <token>.

Authentication

/mcp uses the same bearer tokens as everything else, with two differences from an ordinary endpoint:

  • No session. A dashboard session cookie is never accepted here, even though it works on every REST route — an MCP client is a machine, not a person at a browser, and the two credentials are kept apart. Mint a personal access token instead.
  • A scope per tool. The endpoint itself needs read or wider, so a metrics-only token cannot connect at all. Beyond that, each tool declares the scope it needs and is checked on every call — a read token connects and can look, but a call to a tool that needs deploy or write is refused before the tool runs. The refusal names what it saw: which scope the tool requires and which scopes the token has.

Everything else about a token — the 100 req/min rate limit, audit attribution, expiry — applies exactly as it does to the REST API. Read-only tools produce no audit log entries, the same way a GET never does. Every tool that changes something is audited, attributed to the token that made the call, with the client's address.

What It Can See

Reach is identical to what the same token could do through the REST API: an admin token sees every project on the install, a Member's token sees their own projects and any shared with them, and a project-pinned token is narrowed to its pinned projects — enforced on every tool call, not just the ones that look project-scoped. A tool that is refused for access reasons looks the same as one asking about something that does not exist.

The Tools

ToolReturns
list_projectsEvery project the token can reach
get_projectOne project by id
list_applicationsEvery application in a project
get_applicationOne application by id, including a pending source/config change
list_databasesEvery managed database in a project (never credentials)
get_databaseOne database by id (never credentials)
list_deploymentsAn application's deployments, newest first — status, build outcome, commit info, health-check result
get_application_logsThe last N lines of an application's live container output
get_deployment_logsThe last N lines of a deployment's build log — the first place to look when a deployment failed
list_domain_tls_statusEvery domain's observed TLS state, with why a pending or failed certificate hasn't issued
list_project_backupsRecent backup runs across a project's databases and application volumes

Every list and log tool is bounded — a default and a hard maximum, never "everything." get_application_logs and get_deployment_logs default to 200 lines (capped at 1000); list_deployments and list_project_backups default to 20 (capped at 100).

Tools That Make Changes

ToolScopeWhat it does
trigger_deploymentdeployStarts a deployment of an application now. Returns a pending deployment immediately; follow it with list_deployments and get_deployment_logs. Refused, with an explanation, while another deploy, build, reload, rebuild, or rollback is running for that application.
update_applicationwriteChanges an application's name, source repository or image, branch, root directory, Dockerfile path, build type override, or builder image.
start_applicationwriteStarts an application's existing stopped container. Does not build or deploy anything.
stop_applicationwriteStops an application's running container. The application goes offline; nothing is deleted.
set_application_env_varswriteSets environment variables on an application. See below.
set_application_resourceswriteReplaces an application's CPU and memory limits. Both are replaced together, and memory_limit is in bytes.
set_application_health_checkwriteReplaces an application's whole health-check configuration — http, command, or none.
set_application_runtimewriteReplaces an application's container hardening profile: read-only root filesystem, and minimal or standard capabilities.
set_database_resourceswriteReplaces a managed database's CPU and memory limits. The only tool that takes effect on a running container immediately.

start_application and stop_application need write, not deploy: deploy means "cause a deployment" and nothing more, so a token you hand to CI cannot take an application offline.

Four things hold for all of them:

  • There is no confirmation step. A dashboard button has a person behind it; a tool call does not. Each tool takes effect the moment it is called, which is why this set is deliberately limited to changes that are reversible or additive.
  • Saving is not deploying — with one exception. update_application, set_application_env_vars, set_application_resources, set_application_health_check and set_application_runtime save a change; the running container is untouched until a deployment applies it, and the application's pending_change shows one is waiting. set_database_resources is the exception: a database's limits are applied to the running container straight away.
  • Only what you name changes — within one tool. update_application changes only the fields passed; every field left out keeps its current value, and an empty string clears one that can be cleared. An assistant mentioning one field cannot wipe the others, and update_application cannot reach an application's environment variables, domains, resource limits, health check, hardening profile, git credentials, or type — those have their own tools, or none at all.
  • The set_* settings tools replace, they do not merge. set_application_resources, set_application_health_check, set_application_runtime and set_database_resources each write their whole area: a field you leave out goes back to its default rather than keeping its current value. Read the current state first — with get_application or get_database — and pass every field you want to keep. set_application_env_vars is the one that merges, and it says so.

Creating Things

create_application and create_database make the resource and stop there. An application is created not deployed — call trigger_deployment when you want it built and started — and a database is provisioned asynchronously, so it comes back as creating and becomes running shortly after. Poll get_database.

A name that already exists in the project is refused, not reused. The alternative — returning the existing resource — would leave an assistant unable to tell "I created this" from "this was already here", so it would report success for work it did not do. The trade is the mirror case: if a create succeeds but its reply is lost, the retry reports a conflict for something that did in fact get made. That is why the refusal names the conflicting resource and its id, so the next step is get_application or get_database rather than guesswork. Names are compared ignoring case and surrounding spaces.

Neither tool takes a credential. create_application has no field for a connected git account or a repository token, so only public repositories and public images work through it — choosing which credential an application uses is a decision for the dashboard, not for a tool call with no confirmation step. Database credentials are generated and stored encrypted, and no tool returns them.

Two further deliberate gaps: create_application sets no resource limits (use set_application_resources, so there is one way to set them), and create_database cannot create the other engine — an arbitrary container image run as a database, whose image, port, data directory and backup commands are assembled from that image's documentation.

create_domain adds a route and stores the domain with TLS off, and it cannot turn HTTPS on — not as a default, but at all: there is no field for it, so the request is rejected if one is passed. Enable TLS in the dashboard once the hostname's DNS points at this server.

That order is deliberate, and a certificate request is the one thing a domain write does that reaches outside your install. Let's Encrypt limits failed validations per hostname, per hour, so asking for a certificate before DNS resolves here spends that budget and delays the certificate that would otherwise have worked. A person adding a domain in the dashboard has just decided where DNS points; a tool call has not. list_domain_tls_status reports why a certificate is pending or failed.

DNS is not checked, so a typo produces a route that serves nothing — and since there is no tool to remove or edit a domain, clearing that up is a dashboard job. Editing a live domain is deliberately absent for the same reason: changing a hostname, path, port or TLS mode on a domain that is already serving traffic takes a working site down, which is not a blast radius that is acceptable without a confirmation step.

Quotas still apply. create_application is refused when a project is at its application limit, exactly as the REST endpoint is, so MCP is not a way around one.

Environment Variables and Secrets

Connecting an MCP client with a write token lets an LLM set your environment variables, including secrets. It can never read a secret back — revealing a secret value is available only to a dashboard session, never to a token. The non-secret values it sets are readable by anyone with read access to the application, as they are in the dashboard. Only connect a client you trust to write to your applications' configuration.

set_application_env_vars adds or updates only the variables you list. Every other variable on the application is left exactly as it is, and the tool never removes one — to remove a variable, use the dashboard. Passing one variable changes one variable. Values are stored encrypted, and the tool's reply names the keys it set but never repeats a value.

A variable the tool creates is a secret unless it is passed with is_secret: false, so a key the assistant did not label is hidden rather than shown. An existing variable keeps its current secret setting unless one is passed. If any entry is invalid — a bad name, a duplicate, or more than 100 in one call — nothing is written. The audit entry lists the variable names and never the values.

What It Cannot Do

A token has never been able to destroy anything, and going through MCP does not change that: there is no delete tool and no restore tool, and there never will be. No MCP tool deletes an application, a database, a project, a domain, an environment variable, a backup, or a volume, or restores any of them. The same goes for the install itself: tokens, users, settings, quotas, and backup destinations are not reachable through MCP at all, because install-wide configuration is not something a project-scoped token holds.

One change in that list deserves naming on its own. set_application_runtime can relax an application's container hardening — turn off the read-only root filesystem, or move it from minimal capabilities to Docker's default set. That is a real reduction in defence in depth, it survives until someone changes it back, and nothing about the running application looks different afterwards. It stays write rather than session-only because a write token can already do it through the REST API, and because a token that can repoint an application at a different image is already trusted with far more; but if you connect an assistant you would not trust with that decision, issue it a read token. no-new-privileges is always on either way, and no tool can grant privileged mode, host mounts, host networking, or device access — none of those is reachable from any token.

What MCP can change is the short list in Tools That Make Changes, each gated by the scope in that table. A read token cannot change anything, and a write token gains nothing by going through MCP that it could not already do through the REST API — MCP is narrower than the REST API, never wider. It also goes further than the REST API's own token boundaries in one respect: it exposes only these tools, not every endpoint a write token could otherwise reach.

Calling a Tool Directly

A client library handles the JSON-RPC envelope for you, but it's plain HTTP if you want to see it:

curl -X POST https://belune.example.com/mcp \
  -H "Authorization: Bearer $BELUNE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_projects","arguments":{}}}'

The Accept header must list both application/json and text/event-stream — that's the streamable HTTP transport's own requirement, not a Belune-specific one. The server always answers with a single JSON response here: the connection is stateless, so there is no session to open first and no stream to keep alive.

On this page