Skip to main content
Glama
README.md
# calagopus-mcp

A [Model Context Protocol](https://modelcontextprotocol.io) server for a running
[Calagopus](https://calagopus.com) panel — users, servers, nodes, nests and eggs,
database hosts, database agent hosts and templates, locations, mounts, roles,
backups and system health.

**Read-only out of the box.** Creating records and changing a server's power
state are opt-in, off by default, and every one of them requires an explicit
two-phase confirmation.

Built and verified against Calagopus **1.2.0**.

## What it can do

Ask things like:

- *"How many servers are on node X, and how much memory is still unallocated?"*
- *"Which servers use the Project Zomboid egg?"*
- *"Find the account for someone@example.com and list what they own."*
- *"Is the panel clock in sync and are all migrations applied?"*
- *"What database agent hosts are configured, and what's left on them?"*

## Safety

### Read side

| Property | How it is enforced |
|---|---|
| **Single-panel.** Cannot be redirected to another host. | Tools take API *paths*; a full URL is rejected, and `..` is refused. |
| **Secrets stay behind.** | Values under `password`, `token`, `client_secret`, `access_key`, `secret_key`, `connection_string` and friends are replaced before the response is rendered. Set `CALAGOPUS_ALLOW_SECRETS=true` to opt out. |
| **Bounded output.** | Responses are summarised by default and hard-capped at `CALAGOPUS_MAX_RESPONSE_CHARS`; truncation is announced, never silent. |

### Write side

Writes are off unless you turn them on, and there are only two switches:

```bash
CALAGOPUS_ALLOW_CREATE=true   # create nests, locations, nodes, servers, eggs
CALAGOPUS_ALLOW_POWER=true    # start / stop / restart / kill ONE server
```

Six things hold, in layers:

1. **Not registered means not reachable.** With both flags off, no mutating tool
   is advertised at all. There is no tool to call and no error path to argue
   past — the model simply cannot see one.
2. **Two-phase confirmation on every change.** A tool called without
   `confirm_token` changes nothing: it resolves the real target from the panel,
   renders what would happen, and returns a token. Only a second call carrying
   that token executes.
3. **Tokens are bound to their arguments.** A token is a fingerprint of the
   operation *and* every argument. A token issued for "stop server A" will not
   execute "stop server B", or "kill server A", or the same action with an
   edited limit. It is single-use and expires (5 minutes by default). This is
   what makes the preview meaningful: what you read is the only thing that
   token can do.
4. **No bulk anything.** Power actions take one server UUID — not a list. The
   panel *does* expose `POST /nodes/{node}/servers/power` accepting
   `{"type": "all"}`, i.e. power-cycle every server on a node in one request.
   This server never calls it; the route is absent from the write allowlist.
   Restarting five servers means five previews and five confirmations.
5. **`kill` is its own tier.** It additionally requires
   `acknowledge_data_loss: true`, because it SIGKILLs the container with no
   graceful shutdown and no world save. The preview says so in capitals.
6. **A transport-level allowlist.** `client.ts` will only POST to nine exact
   route patterns, checked at the point of request. There is no DELETE or PATCH
   method on the client at all, so **nothing can be deleted or modified** —
   only created, plus power state. A wiring mistake cannot reach a route nobody
   reviewed.

Every executed mutation is logged to stderr with a timestamp, the operation and
the resolved target.

Scope it further with the API key itself: grant only the permissions you want
reachable. The read tools need admin `*.read`; creates need the matching
`*.create`; power needs the server permissions `control.start`, `control.stop`
and `control.restart`.

### What a change looks like

```jsonc
// 1. No confirm_token — nothing is sent to the panel.
{ "server": "95b3a3a4-...", "action": "stop" }
→ {
    "status": "preview_only",
    "nothing_was_changed": true,
    "action": "STOP server \"Zomboid\" (95b3a3a4-...)",
    "target": { "name": "Zomboid", "current_status": "running",
                "node": "Integrated Node", "owner": "BerdiiNN" },
    "warnings": ["Asks the server to shut down gracefully, ... Players are disconnected."],
    "confirm_token": "cfm_...", "token_expires": "..."
  }

// 2. Same arguments plus the token — now it happens.
{ "server": "95b3a3a4-...", "action": "stop", "confirm_token": "cfm_..." }
→ { "status": "executed", ... }
```

## Setup

```bash
npm install && npm run build
```

Create an API key in the panel under **Account → API Keys** and grant it the
admin read permissions you want. Then register the server with your client.

Claude Code:

```bash
claude mcp add calagopus --env CALAGOPUS_URL=https://panel.example.com --env CALAGOPUS_API_KEY=c7sp_xxx -- node /absolute/path/to/calagopus-mcp/dist/index.js
```

Claude Desktop / any MCP client, in `mcpServers`:

```json
{
  "mcpServers": {
    "calagopus": {
      "command": "node",
      "args": ["/absolute/path/to/calagopus-mcp/dist/index.js"],
      "env": {
        "CALAGOPUS_URL": "https://panel.example.com",
        "CALAGOPUS_API_KEY": "c7sp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

For running the server or the smoke test by hand, a `.env` beside `package.json`
is read at startup (see `.env.example`). Anything already in the environment
wins, so an MCP client's own config is never overridden by a stale local file.

### Environment

| Variable | Default | Purpose |
|---|---|---|
| `CALAGOPUS_URL` | *(required)* | Panel base URL. A trailing `/api` or `/` is trimmed for you. |
| `CALAGOPUS_API_KEY` | *(required)* | API key, `c7sp_` + 43 characters. |
| `CALAGOPUS_TIMEOUT_MS` | `30000` | Per-request timeout. |
| `CALAGOPUS_MAX_RESPONSE_CHARS` | `60000` | Cap on a single tool result. |
| `CALAGOPUS_ALLOW_SECRETS` | `false` | Stop redacting secret-looking fields. |
| `CALAGOPUS_INSECURE_TLS` | `false` | Accept self-signed certificates. Process-wide — use only against a lab panel. |
| `CALAGOPUS_ALLOW_CREATE` | `false` | Register the create tools. |
| `CALAGOPUS_ALLOW_POWER` | `false` | Register the server power tool. |
| `CALAGOPUS_CONFIRM_TTL_MS` | `300000` | How long a confirmation token stays valid. |

## Tools

36 read tools, plus 6 write tools when enabled. Start with
`calagopus_panel_info`.

**Orientation**
`calagopus_panel_info` · `calagopus_search` · `calagopus_get_system_health` ·
`calagopus_list_endpoints` · `calagopus_get`

**Users** — `calagopus_list_users` · `calagopus_get_user` ·
`calagopus_get_user_servers` · `calagopus_get_user_activity` ·
`calagopus_list_roles` · `calagopus_get_activity`

**Servers** — `calagopus_list_servers` · `calagopus_get_server` ·
`calagopus_get_server_resource` *(allocations, variables, backups, databases,
database_instances, mounts, available_mounts, logs, install_logs)*

**Nodes** — `calagopus_list_nodes` · `calagopus_get_node` ·
`calagopus_get_node_resource` *(servers, allocations, available_allocations,
allocation_ips, backups, mounts, capacity, database_hosts,
database_agent_hosts, transfers, system_overview, system_stats, system_logs,
resources)* · `calagopus_list_locations` · `calagopus_list_mounts`

**Nests & eggs** — `calagopus_list_nests` · `calagopus_list_eggs` ·
`calagopus_get_egg` · `calagopus_get_egg_variables` ·
`calagopus_get_egg_servers` · `calagopus_list_egg_repositories` ·
`calagopus_list_egg_configurations`

**Databases** — `calagopus_list_database_hosts` ·
`calagopus_get_database_host` · `calagopus_list_database_agent_hosts` ·
`calagopus_get_database_agent_host` ·
`calagopus_get_database_agent_host_resource` *(instances, capacity, config,
system_overview, system_stats)* · `calagopus_list_database_agent_templates`

**Backups & SSO** — `calagopus_list_backup_configurations` ·
`calagopus_list_system_backup_policies` · `calagopus_list_oauth_providers` ·
`calagopus_list_extensions`

**Create** *(needs `CALAGOPUS_ALLOW_CREATE`)* — `calagopus_create_nest` ·
`calagopus_create_location` · `calagopus_create_node` ·
`calagopus_create_server` · `calagopus_install_egg_from_repository` ·
`calagopus_import_egg`

**Power** *(needs `CALAGOPUS_ALLOW_POWER`)* — `calagopus_server_power`

`calagopus_create_server` previews more than it is asked to: it resolves the
node, egg and owner by name, and checks the egg's declared variables so a
missing required value shows up in the preview rather than as a rejected create.

### `summary` vs `full`

List tools return a trimmed projection plus a `pagination` block. Raw panel
records are big — a server inlines its whole node, owner and egg (~15 KB each),
and an egg carries its install script and upstream copy (~13 KB) — so a page of
25 in full detail would be several hundred KB of context. Pass `detail: "full"`
on a single-record tool when you genuinely need the scripts or every field.

### Not covered by a dedicated tool?

`calagopus_list_endpoints` searches the panel's **live** OpenAPI document, so it
reflects that panel's version including routes added by extensions. Feed a path
you find into `calagopus_get`:

```
calagopus_list_endpoints { filter: "backup-configurations" }
calagopus_get { path: "/api/admin/backup-configurations/<uuid>/stats" }
```

## Interpreting errors

Errors come back in-band with a hint. The one worth recognising:

> **500 … error sending request for url (http://…)** — the panel is healthy but
> could not reach the daemon behind that endpoint. The node or database agent is
> offline, still booting, or has the wrong URL configured. Panel-stored records
> (`calagopus_get_node`, `calagopus_list_servers`, …) still read fine; only the
> live `system_*`, `capacity`, `resources`, `transfers` and log facets need the
> daemon up.

A **403** names the exact admin permission the key is missing.

## Testing

`scripts/smoke.mjs` connects over real stdio MCP, lists the tools, and calls
every one of them against a live panel — discovering UUIDs from earlier calls so
per-record tools run with real identifiers. It fails if any tool is left
unexercised.

```bash
CALAGOPUS_URL=http://localhost:8000 CALAGOPUS_API_KEY=c7sp_xxx npm run smoke
```

Facets that need a wings daemon or database agent are counted as *skipped*
rather than failed when that daemon is unreachable.

`scripts/safety.mjs` is the suite that matters for the write side. It launches
the server three times — read-only, create-enabled, power-enabled — and asserts
the guard rails: that no mutating tool exists by default, that a dry run changes
nothing, that a token is refused against a different server, a different action,
or on a second use, that `kill` demands acknowledgement, that no bulk power tool
exists, and that the read-side escape hatch has no method or body argument.

```bash
npm run safety              # guard rails only, no writes
SAFETY_LIVE=1 npm run safety   # also creates a real nest + location, named mcp-safety-*
npm test                    # both suites
```

Without `SAFETY_LIVE`, the suite performs **no writes at all** — it stops at
previews. `SAFETY_LIVE=1` additionally creates a real nest and location, and
spends one token on a real `start` to prove replay protection; the records it
creates stay behind, because this server cannot delete them by design. Remove
them from the panel yourself.

## Layout

```
src/
  index.ts          stdio wiring and conditional tool registration
  config.ts         environment parsing and validation
  client.ts         HTTP client, POST allowlist, error typing, hints
  confirm.ts        two-phase confirmation tokens
  format.ts         redaction, per-resource projections, size cap
  tools/
    common.ts       shared arg shapes, result rendering, error guard, twoPhase
    system.ts users.ts servers.ts nodes.ts eggs.ts databases.ts raw.ts
    create.ts       create tools   (gated on CALAGOPUS_ALLOW_CREATE)
    power.ts        power tool     (gated on CALAGOPUS_ALLOW_POWER)
```

Adding a read resource is normally: a projection in `format.ts`, and a
`registerTool` call in the matching `tools/*.ts`. Adding a *write* also means
adding its route to `POST_ALLOWLIST` in `client.ts` and a case to
`scripts/safety.mjs`.

## License

MIT

TDQS

A3.9/5.0

Scored across 36 tools

Disambiguation5/5

Every tool targets a specific resource/action pair; even similar names such as get_database_agent_host and get_database_agent_host_resource are clearly separated by the resource versus resource-facet distinction. With 36 tools, there is no pair that appears to duplicate another.

Naming Consistency4/5

Tool names overwhelmingly follow a consistent calagopus_ + verb + noun pattern (list_*, get_*). A few exceptions—calagopus_panel_info, calagopus_search, and calagopus_get—break the verb-noun pattern, but they are minor and the overall convention is predictable.

Tool Count2/5

36 tools is above the 25-tool threshold for a focused tool set. While the Calagopus domain is broad, the large number of narrow getters and listers makes the surface heavy and more likely to confuse an agent, even though each tool individually seems justified.

Completeness4/5

The read-only surface covers most of the panel domain: users, servers, nodes, eggs, locations, mounts, OAuth, roles, activity, backups, and database infrastructure. The raw GET escape hatch plus OpenAPI discovery helps cover edge cases, though a few single-resource getters such as get_mount or get_nest are missing, and there are no mutations by design.

Maintenance

ActivityMaintained
ResponsivenessNo issues