MCP server for managing Swarmpit Docker Swarm instances from any MCP-compatible client. 100% Swarmpit API coverage (79 endpoints).
The server runs locally and holds API tokens — they never enter the LLM conversation context.
Works with opencode (recommended), Claude Code, and any other MCP client.
Add to your .opencode.json:
{
"mcpServers": {
"swarmpit-prod": {
"type": "stdio",
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": {
"SWARMPIT_URL": "https://swarmpit.example.com",
"SWARMPIT_TOKEN": "your-api-token",
"SWARMPIT_REDACT": "sensitive"
}
}
}
}Add to your .mcp.json (project-level) or ~/.claude.json (global):
{
"mcpServers": {
"swarmpit-prod": {
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": {
"SWARMPIT_URL": "https://swarmpit.example.com",
"SWARMPIT_TOKEN": "your-api-token",
"SWARMPIT_REDACT": "sensitive"
}
}
}
}Get your API token from Swarmpit UI: Profile → API Access → Generate token.
One process can manage several Swarmpit instances. Every tool then takes an optional server argument naming the target — the tool set is registered once instead of once per instance, which matters because the full surface is ~79 tools and the model loads all of them for every MCP server you wire up.
Point SWARMPIT_CONFIG_FILE at a JSON file:
{
"default": "prod-eu",
"servers": {
"prod-eu": { "url": "https://swarmpit.eu.example.com", "tokenFile": "~/.config/swarmpit/prod-eu" },
"prod-us": { "url": "https://swarmpit.us.example.com", "tokenFile": "~/.config/swarmpit/prod-us" }
}
}{
"mcpServers": {
"swarmpit": {
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": { "SWARMPIT_CONFIG_FILE": "~/.config/swarmpit/servers.json", "SWARMPIT_REDACT": "sensitive" }
}
}
}Or without a file, using per-name env vars (SWARMPIT_<NAME>_URL, with - becoming _ and the name upper-cased):
"env": {
"SWARMPIT_SERVERS": "prod-eu,prod-us",
"SWARMPIT_DEFAULT_SERVER": "prod-eu",
"SWARMPIT_PROD_EU_URL": "https://swarmpit.eu.example.com",
"SWARMPIT_PROD_EU_TOKEN_FILE": "~/.config/swarmpit/prod-eu",
"SWARMPIT_PROD_US_URL": "https://swarmpit.us.example.com",
"SWARMPIT_PROD_US_TOKEN_FILE": "~/.config/swarmpit/prod-us"
}Then: list_services({ server: "prod-us" }). Omitting server uses the default. If several instances are configured and no default is set, the argument is required — a forgotten argument will never silently pick a cluster for you. swarmpit_info lists what's configured.
Read-only tools also accept "*" or a list of names, and return results grouped by server:
Useful for "which cluster runs this service?", comparing an image tag across regions, or checking whether a config exists everywhere — one tool call instead of one per cluster.
- Reads only.
list_*,get_*andservice_logsaccept fan-out. Every mutating tool (create_*,update_*,delete_*,scale_*,redeploy_*,rollback_*,stop_*,deactivate_*,pin_*/unpin_*,edit_*) takes a single name only — "apply this to every cluster" should not be expressible by accident. - Partial failure is reported, not fatal. An unreachable cluster, or one where the named resource doesn't exist, comes back as
{ "error": "..." }under its own key and is listed infailed. The other clusters' results still arrive. - Passing
"*"or a list always returns the grouped shape, even if it resolves to one server; passing a bare name returns the plain result.
Consolidate within a trust tier, isolate across tiers.
- One MCP server, several instances — the default for instances in the same trust tier (several prod clusters, several staging clusters). This is where the duplicated tool schemas actually cost you.
- Separate MCP servers — the right answer across trust tiers. Keeping prod in its own process means a test or play conversation cannot reach the prod token at all, no matter what the model is talked into. That is a stronger guarantee than a
serverargument the model chooses itself.
The two compose. One MCP server holding every prod cluster, a second holding every test cluster, gets you both the schema saving and a real boundary where it matters:
{
"mcpServers": {
"swarmpit-prod": {
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": { "SWARMPIT_CONFIG_FILE": "~/.config/swarmpit/prod.json", "SWARMPIT_REDACT": "sensitive" }
},
"swarmpit-test": {
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": { "SWARMPIT_CONFIG_FILE": "~/.config/swarmpit/test.json", "SWARMPIT_REDACT": "none" }
}
}
}A single-instance setup is unchanged in every respect: SWARMPIT_URL + SWARMPIT_TOKEN/SWARMPIT_TOKEN_FILE still work, and no server argument is added to any tool.
| Variable | Required | Description |
|---|---|---|
SWARMPIT_URL |
Yes¹ | Swarmpit instance URL (single-instance setup) |
SWARMPIT_TOKEN |
One of¹ | API token (with or without Bearer prefix). Avoid putting in .mcp.json — see token handling. |
SWARMPIT_TOKEN_FILE |
One of¹ | Path to a file containing the token. Takes precedence over SWARMPIT_TOKEN. Recommended for production use. |
SWARMPIT_CONFIG_FILE |
Yes¹ | Path to a JSON file defining several instances. Takes precedence over everything below. |
SWARMPIT_SERVERS |
Yes¹ | Comma-separated instance names, each configured via SWARMPIT_<NAME>_URL and SWARMPIT_<NAME>_TOKEN/_TOKEN_FILE |
SWARMPIT_DEFAULT_SERVER |
No | Instance used when a tool call omits server. Without it, the argument is required. |
SWARMPIT_REDACT |
No | Redaction mode: all (default), sensitive, or none. Applies to all instances in the process. |
SWARMPIT_REDACT_PATTERNS |
No | Comma-separated extra patterns to redact in sensitive mode (regex, case-insensitive) |
¹ Exactly one of the three forms: SWARMPIT_CONFIG_FILE, SWARMPIT_SERVERS, or SWARMPIT_URL. They are tried in that order.
| Mode | Env vars | Secrets/Configs data | User records |
|---|---|---|---|
all |
All values redacted | Redacted | Identity only (_id, username, role, email) |
sensitive |
Only names matching patterns | Redacted | Identity only |
none |
No redaction | Not redacted | Full record |
User records never contain a password hash (the Swarmpit server strips it), but in the non-none modes the API-token metadata (jti, mask) and CouchDB _rev are dropped too — list_users/get_user report only hasApiToken: true.
Warning:
nonemode sends all environment variables, secrets, and config data in full to the LLM provider. Only use this with a local model (e.g. via ollama/opencode) or on servers that definitely do not contain any sensitive environment variables or configs. Never usenonewith cloud-hosted LLM providers on production infrastructure.
Built-in sensitive patterns: pass, secret, token, key, auth, credential, private, dsn, connection_string.
Add custom patterns via SWARMPIT_REDACT_PATTERNS:
"env": {
"SWARMPIT_REDACT": "sensitive",
"SWARMPIT_REDACT_PATTERNS": "GRAFANA,RPC,ENDPOINT,DATABASE"
}| Tool | Description |
|---|---|
list_services |
List all services |
get_service |
Get service details |
service_logs |
Get service logs |
create_service |
Create a service |
update_service |
Update a service |
redeploy_service |
Redeploy (optionally with new tag) |
rollback_service |
Rollback to previous version |
stop_service |
Stop a service |
scale_service |
Scale replicas |
list_service_tasks |
List service tasks/containers |
delete_service |
Delete (requires confirm: true) |
update_service_env |
Set/remove env vars (supports $env references) |
get_service_env |
Get specific env var values by name |
get_service_compose |
Get compose YAML for a service |
get_service_networks |
Get networks attached to a service |
| Tool | Description |
|---|---|
list_stacks |
List all stacks |
get_stack |
Get stack services and compose file |
create_stack |
Create from compose YAML |
update_stack |
Update with new compose YAML |
redeploy_stack |
Redeploy all services |
rollback_stack |
Rollback all services |
deactivate_stack |
Stop all services in a stack |
delete_stack |
Delete (requires confirm: true) |
get_stack_tasks |
List all tasks in a stack |
get_stack_volumes |
List all volumes in a stack |
get_stack_networks |
List all networks in a stack |
get_stack_compose |
Get generated compose YAML |
get_stack_secrets |
List secrets in a stack |
get_stack_configs |
List configs in a stack |
create_stack_file |
Upload a compose file for a stack |
delete_stack_file |
Delete stack compose file (requires confirm: true) |
| Tool | Description |
|---|---|
list_networks |
List all networks |
get_network |
Get network details |
create_network |
Create a network |
delete_network |
Delete (requires confirm: true) |
get_network_services |
List services using a network |
| Tool | Description |
|---|---|
list_nodes |
List all nodes |
get_node |
Get node details |
get_node_tasks |
List tasks running on a node |
edit_node |
Edit node properties |
delete_node |
Remove a node (requires confirm: true) |
| Tool | Description |
|---|---|
list_tasks |
List all tasks |
get_task |
Get task details |
| Tool | Description |
|---|---|
list_volumes |
List all volumes |
get_volume |
Get volume details |
create_volume |
Create a volume |
delete_volume |
Delete (requires confirm: true) |
get_volume_services |
List services using a volume |
| Tool | Description |
|---|---|
list_secrets |
List all secrets (data redacted) |
get_secret |
Get secret details (data redacted) |
create_secret |
Create a secret (supports $env / $file references) |
delete_secret |
Delete (requires confirm: true) |
get_secret_services |
List services using a secret |
| Tool | Description |
|---|---|
list_configs |
List all configs (data redacted) |
get_config |
Get config details (data redacted) |
create_config |
Create a config (supports $env / $file references) |
delete_config |
Delete (requires confirm: true) |
get_config_services |
List services using a config |
| Tool | Description |
|---|---|
list_users |
List all Swarmpit users (redacted) |
get_user |
Get user details (redacted) |
create_user |
Create a user (password accepts $env/$file) |
edit_user |
Edit user properties (spec.password accepts $env/$file) |
delete_user |
Delete (requires confirm: true) |
Pass user passwords by reference so the plaintext never enters the conversation or the tool-call transcript:
{ "username": "svc", "role": "user", "password": { "$env": "NEW_USER_PASSWORD" } }{ "$file": "/path/to/password" } works too. A literal string is still accepted, but it is recorded verbatim in the model's context and in the session transcript.
| Tool | Description |
|---|---|
pin_service_to_dashboard |
Pin a service to the Swarmpit dashboard |
unpin_service_from_dashboard |
Remove a service from the dashboard |
pin_node_to_dashboard |
Pin a node to the dashboard |
unpin_node_from_dashboard |
Remove a node from the dashboard |
| Tool | Description |
|---|---|
get_nodes_timeseries |
Node CPU/memory/disk over time |
get_services_cpu_timeseries |
Service CPU usage over time |
get_services_memory_timeseries |
Service memory usage over time |
get_task_timeseries |
Task metrics over time |
| Tool | Description |
|---|---|
swarmpit_info |
Show connected URL and redaction mode |
Putting SWARMPIT_TOKEN directly in .mcp.json is convenient but risky: if anything (including Claude / the LLM) reads .mcp.json, the token leaks into the conversation and API logs. Two hardening steps you should take:
Add this to your project .claude/settings.json so Claude Code refuses to Read credential-holding files:
{
"permissions": {
"deny": [
"Read(.mcp.json)",
"Read(**/.mcp.json)",
"Read(.env)",
"Read(.env.*)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/*credentials*)",
"Read(**/secrets/**)",
"Read(**/*.pem)",
"Read(**/*.key)"
]
}
}Store the token in a file outside the repo and reference it by path. The token never appears in .mcp.json itself:
# Write the token to a restricted file
install -m 600 /dev/stdin ~/.config/swarmpit/lark.token <<<'your-token-here'{
"mcpServers": {
"swarmpit-lark": {
"command": "npx",
"args": ["github:swarmpit/mcp"],
"env": {
"SWARMPIT_URL": "https://swarmpit.example.com",
"SWARMPIT_TOKEN_FILE": "/Users/you/.config/swarmpit/lark.token",
"SWARMPIT_REDACT": "sensitive"
}
}
}
}For even stronger posture, have the MCP spawn with a secret manager:
{
"swarmpit-lark": {
"command": "op",
"args": ["run", "--", "npx", "github:swarmpit/mcp"],
"env": {
"SWARMPIT_URL": "https://swarmpit.example.com",
"SWARMPIT_TOKEN": "op://Private/Swarmpit Lark/token",
"SWARMPIT_REDACT": "sensitive"
}
}
}1Password's op run substitutes op://... references at process start. macOS users can do the equivalent with security find-generic-password in a wrapper script.
If your token has already been exposed (e.g. you saw it scroll through a log), rotate it in Swarmpit UI → Profile → API Access → regenerate.
Secrets in .mcp.json env are passed to the MCP server process but never sent to the LLM.
For large payloads (HTML pages, compose files, certs) use $file to have the MCP server read from disk instead of passing content through the LLM context:
create_config(configName: "my_dashboard", data: { "$file": "/path/to/index.html" })
create_secret(secretName: "tls_cert", data: { "$file": "/etc/ssl/server.crt" })
create_stack(name: "myapp", compose: { "$file": "/path/to/stack.yml" })
update_stack(name: "myapp", compose: { "$file": "/path/to/stack.yml" })
Saves context/credits on anything larger than a few hundred bytes. $env: references inside the file are still resolved before sending to Swarmpit.
Use $env references to set secrets without them entering the conversation:
update_service_env(id: "my-service", set: {
"NODE_ENV": "production",
"DB_PASSWORD": { "$env": "MY_DB_PASS" }
})
MY_DB_PASS is resolved from the MCP server's environment. Add it to .mcp.json env:
"env": {
"SWARMPIT_URL": "...",
"SWARMPIT_TOKEN": "...",
"MY_DB_PASS": "the-actual-password"
}When reading stacks, env var values are redacted according to the redaction mode. When updating, [REDACTED] values are automatically preserved from the current stack — only changed values are updated:
# Returned by get_stack (sensitive mode):
environment:
NODE_ENV: production # visible
DB_PASSWORD: [REDACTED] # redacted
# Sent to update_stack — only NODE_ENV changed:
environment:
NODE_ENV: staging # new value
DB_PASSWORD: [REDACTED] # preserved from current stackUse $env:VAR_NAME for new secrets in compose:
environment:
NEW_SECRET: $env:MY_SECRET # resolved locallyPreserved values are read from the live service state, not the stored stack file — the two diverge whenever a var is changed through update_service_env or the Swarmpit UI. If they disagree for a var being restored, update_stack says so in its result.
update_service applies the same protection: any env var left as [REDACTED] is restored from the live service before the write, so a get_service → edit one field → update_service round-trip can't overwrite secrets with the placeholder. Restored names are listed in the result as restoredVars.
update_service and update_stack take dry_run: true, which computes everything — [REDACTED] restoration, $env: resolution, null normalisation — and reports what would change without sending it:
// update_service({ id, spec, dry_run: true })
{
"dryRun": true,
"restoredVars": ["RPC_URL"],
"changes": {
"fields": { "replicas": { "from": 1, "to": 3 } },
"variables": { "changed": ["LOG_LEVEL"], "added": ["NEW"], "removed": ["OLD"], "unchanged": 4 }
}
}Env var values never appear in a preview — only names, and for stacks the diff is taken against the sanitized compose. You get a list of which secrets would change (secretsChanged) without any of them being printed into the conversation.
Two tool families return far more than fits comfortably in context, so both reduce server-side:
- Timeseries (
get_nodes_timeseries,get_services_*_timeseries,get_task_timeseries) acceptpoints(bucket count, default 12),agg(avg/max/min),match(filter series by name) andstats_only. Summary statistics are computed from the full series before downsampling, sostats.maxis the real peak, not the peak of an average. A 30-service / 720-sample payload comes back over 10× smaller by default. service_logsacceptsgrep(substring, or/regex/flags) andtail, applied before the result is returned. The reply reportstotal,returnedand what wasdropped, so a filtered view never looks like the whole log.
git clone https://github.com/swarmpit/mcp
cd mcp
npm install
npm run build # compile TypeScript
npm run dev # watch mode
npm test # run testsWhen developing locally, point .mcp.json at your local build:
{
"mcpServers": {
"swarmpit-dev": {
"command": "node",
"args": ["/path/to/mcp/dist/index.js"],
"env": {
"SWARMPIT_URL": "https://swarmpit.example.com",
"SWARMPIT_TOKEN": "your-token",
"SWARMPIT_REDACT": "sensitive"
}
}
}
}MIT