ℹ️ This project was created with the help of AI.
The server keeps your Coolify API token in its own process environment and exposes
only abstract tools to the LLM agent (list_applications, deploy_application, …).
The token therefore never ends up in the model context or in tool arguments.
- Read any time: projects, servers, applications, databases, services, deployments, resources.
- Write & deploy double-gated: a global switch (
COOLIFY_ALLOW_MUTATIONS) and a per-callconfirm: true. Withoutconfirm, you only get a preview.
Disclaimer: This MCP server executes real actions against your Coolify instance. Use it at your own risk; the author assumes no liability for unintended deployments, data loss, or outages caused by LLM-driven tool calls.
- Requirements
- Installation
- Create a Coolify API token
- Configuration & example config
- Usage
- Tool overview
- Security model
- Troubleshooting
- Node.js ≥ 20 (
node --version) - A reachable Coolify instance (self-hosted or
https://app.coolify.io) - A Coolify API token
git clone git@github.com:thomaspietschmann/coolify-mcp.git
cd coolify-mcp
npm install
npm run buildThis produces dist/index.js — the file the MCP client starts.
After code changes, run
npm run buildagain (ornpm run devfor watch mode).
- Log in to Coolify → top right on your profile → Keys & Tokens.
- Tab API Tokens → Create New Token.
- Choose a scope:
read-only→ read access only (pairs withCOOLIFY_ALLOW_MUTATIONS=false).read:write(i.e. without the "read-only" checkbox) → required for deploy/write.
- Copy the token — it is shown only once.
Settings are provided via the env block of the MCP config — no .env required.
This keeps the token in the client config so it never reaches the model.
| Variable | Required | Default | Description |
|---|---|---|---|
COOLIFY_BASE_URL |
✅ | – | Base URL of the instance, without trailing slash, e.g. https://coolify.example.com |
COOLIFY_API_TOKEN |
✅ | – | Your API token |
COOLIFY_ALLOW_MUTATIONS |
– | true |
false ⇒ full read-only mode (all write/deploy tools off) |
COOLIFY_TIMEOUT_MS |
– | 30000 |
Request timeout in milliseconds |
Always give the path in
argsas an absolute path and adapt it to your clone.
Option A: create a .mcp.json file in your project directory:
{
"mcpServers": {
"coolify": {
"command": "node",
"args": ["/path/to/coolify-mcp/dist/index.js"],
"env": {
"COOLIFY_BASE_URL": "https://coolify.example.com",
"COOLIFY_API_TOKEN": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"COOLIFY_ALLOW_MUTATIONS": "true",
"COOLIFY_TIMEOUT_MS": "30000"
}
}
}
}Option B: add it via command:
claude mcp add coolify \
-e COOLIFY_BASE_URL=https://coolify.example.com \
-e COOLIFY_API_TOKEN=1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
-e COOLIFY_ALLOW_MUTATIONS=true \
-- node /path/to/coolify-mcp/dist/index.jsVerify, then use the tools:
claude mcp list # shows "coolify"
# inside a session:
/mcp # lists connected servers & toolsOpen the config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"coolify": {
"command": "node",
"args": ["/path/to/coolify-mcp/dist/index.js"],
"env": {
"COOLIFY_BASE_URL": "https://coolify.example.com",
"COOLIFY_API_TOKEN": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"COOLIFY_ALLOW_MUTATIONS": "true"
}
}
}
}Then fully restart Claude Desktop. The server appears under the 🔌 / tools menu.
For a configuration that can guaranteed make no changes, simply set:
"COOLIFY_ALLOW_MUTATIONS": "false"All write/deploy tools then return only a notice instead of performing an action.
There is a .env.example. For development only:
cp .env.example .env # fill in values
node dist/index.js # automatically reads .env if presentValues from the MCP env block take precedence over .env. The .env file is in .gitignore.
Once the server is connected, just talk to the agent — it picks the tools itself. Examples:
| You say … | The agent calls … |
|---|---|
| "Which apps are running in Coolify?" | list_applications |
"Show me details for app abc-123." |
get_application { uuid: "abc-123" } |
"Redeploy app abc-123." |
deploy_application { uuid: "abc-123", confirm: true } |
"Set env LOG_LEVEL=debug for abc-123." |
create_application_env { uuid, key, value, confirm: true } |
"Stop database db-9." |
stop_database { uuid: "db-9", confirm: true } |
Preview before execution: if the agent calls a write tool without confirm: true,
it only gets a preview back, e.g.:
PREVIEW (not executed) — Start deployment for 'abc-123'.
To execute, repeat the same call with confirm:true.
Only when repeated with confirm: true is the Coolify API actually called
(and Claude additionally shows its own permission prompt for destructive tools).
Two generic tools reach every Coolify API endpoint — the credentials are injected by the server, the LLM only supplies method, path and body:
coolify_get { path, query? }— arbitrary GET against/api/v1(read-only).coolify_mutate { method, path, body?, confirm: true }— arbitrary POST/PATCH/PUT/DELETE.
So creating projects, adding databases, editing service/database env vars, configuring storage, backups, scheduled tasks, etc. all work today, even where there is no dedicated tool.
The official Coolify OpenAPI spec is bundled with the server, so the LLM can look up the right path and fields before a free-form call (no internet needed):
coolify_api_overview { filter? }— compact list of all endpoints (method, path, summary); filter by substring like"database"or"project".coolify_api_endpoint { path, method? }— full detail for one endpoint: parameters and the request-body schema (field names, types, which are required) plus response codes.
Typical flow: coolify_api_overview → coolify_api_endpoint → coolify_mutate.
Note: the API is slightly narrower than the web UI. For example, creating a Docker-Compose app is not exposed by the API (only Dockerfile, Docker image, public/private Git repos).
Read (any time):
coolify_version, coolify_health, list_teams, get_current_team, list_private_keys (private key material redacted),
list_projects, get_project, list_servers, get_server, get_server_resources,
list_resources, list_applications, get_application, list_application_envs,
list_databases, get_database, list_services, get_service, list_deployments,
get_deployment, coolify_api_overview, coolify_api_endpoint, coolify_get.
Write / deploy (requires confirm: true):
deploy_application, start_application, stop_application, restart_application,
update_application, delete_application, create_application_env, update_application_env,
delete_application_env, create_application_public, create_application_dockerimage,
start_service, stop_service, restart_service,
start_database, stop_database, restart_database,
coolify_mutate.
- Token isolation: the API token lives only in the server process (from the MCP
envblock). It never appears in the LLM context, in prompts, or in tool arguments. - Secret redaction: every API response is passed through a central redactor before it
reaches the model. Secret-bearing fields —
private_key,*_password,*_secret*,*_token, log-drain API keys,internal_db_url/external_db_url— are replaced with"[redacted]", and SSH private keys embedded in deployment logs (raw PEM and their base64 encoding) are scrubbed. Non-secret data is kept, and environment-variable values stay readable (value/real_value) since managing env vars is an explicit purpose. - Global switch:
COOLIFY_ALLOW_MUTATIONS=falsedisables all write tools. - Confirm gate: every write action requires
confirm: true; otherwise just a preview. - Client prompt: destructive tools are marked via the MCP
destructiveHint, so the client additionally shows its own confirmation dialog.
| Symptom | Cause / fix |
|---|---|
| Server won't start, "Missing required environment variable(s)" | COOLIFY_BASE_URL and/or COOLIFY_API_TOKEN missing from the env block. |
Coolify error (HTTP 401) |
Token invalid/expired → create a new token. |
Coolify error (HTTP 403) |
Token has no write scope but a write tool was used. |
Coolify error (HTTP 404) |
Wrong UUID or the endpoint path differs → probe with coolify_get/coolify_mutate. |
Request timed out |
Instance slow/unreachable → increase COOLIFY_TIMEOUT_MS, check the URL. |
| Tools don't appear in the client | Is the args path absolute? Did npm run build run? Did you restart the client? |
npm testUses the built-in Node test runner (node:test) with fetch mocked — no network, no live
Coolify needed. Covered:
- Confirm/read-only gating (
test/server.test.ts): write tools return a preview withoutconfirm, perform the call withconfirm:true, and are refused whenCOOLIFY_ALLOW_MUTATIONS=false— driven through a real MCP client over an in-memory transport. - HTTP client (
test/client.test.ts): URL//api/v1building, query/body serialization, auth header,CoolifyErroron non-2xx, timeout handling. - Config parsing (
test/config.test.ts): required vars, slash stripping, flag/timeout defaults. - OpenAPI lookup (
test/openapi.test.ts): endpoint listing/filtering and$refresolution.
The openapi.json spec is only a lookup aid for the LLM — real calls always go to your live
instance, so a stale spec never breaks functionality, it only affects discovery hints.
Refresh it on demand:
npm run update:api # downloads the latest spec, validates it, overwrites openapi.json
# override the source if needed:
COOLIFY_OPENAPI_URL=https://your-instance/openapi.json npm run update:apiThen commit the changed openapi.json. A weekly GitHub Action
(.github/workflows/update-openapi.yml) also runs this and opens a PR when the spec changed.
This project — code and documentation — was created with the help of AI.
MIT