pterodactyl-mcp
README.md
# @bippy-s-ai-helpers/pterodactyl-mcp
A stdio MCP (Model Context Protocol) server for the Pterodactyl game panel,
covering both API trees in one server:
- the client tree (`/api/client`, authenticated with a `ptlc_` client API
key), and
- the application tree (`/api/application`, authenticated with a `ptla_`
application API key),
speaking plain JSON-RPC 2.0 over stdio: no network listener, no HTTP
framework, no config files. The MCP client spawns this binary and talks to
it over stdin and stdout; every HTTP request happens inside the server
process toward the panel.
## Features
- **105 tools over both Pterodactyl API trees** (65 client tree, 40
application tree at panel v1.15.x): account surfaces, server core,
databases, backups, schedules and tasks, network allocations, files
(including upload, download, and raw contents), startup variables,
settings, subusers, an 8-tool console suite over the wings websocket, and
the admin tree (users, nodes and allocations, locations, servers, server
databases, nests and eggs).
- **Default-deny destructive gate**: 21 tools classified as destructive
(deletes, reinstalls, suspends, power kill, and the subuser access-grant
trio) are registered but deny by default with zero panel requests until
you explicitly allowlist them.
- **Tree toggles to control context cost**: expose only the client tree,
only the application tree, or both. Every disabled family registers
nothing, so a partial deployment keeps the tools/list payload small.
- **Lean dependency set**: the MCP SDK, `ws`, and `zod` only. No HTTP
framework, no dotenv, no runtime config files.
- **Deterministic generated docs**: `docs/endpoint-reference.md` (every
mapped and doctrine-excluded panel endpoint with tool attribution, gate
class, and route-file citation) and `docs/census.md` (registry counts,
LOC census, test counts) ship in the repo and regenerate byte-identically
with `npm run docs`.
- **Strictly typed client core**: Fractal-envelope parsing, typed error
classes for 401/403, 404, 422, 429, 5xx, non-JSON bodies, and network
failures, pagination with a hard page cap, and a console bridge with a
bounded in-memory tail ring.
## Prerequisites
- Node.js 18 or newer and npm.
- A reachable Pterodactyl panel (v1.15.x; built and tested against 1.15.1).
- An application API key (`ptla_` prefix) for the admin tree, and/or a
client API key (`ptlc_` prefix) for the client tree; at least one is
required, and only the tree(s) whose key you configured are exposed.
- An MCP-capable client (opencode works for sure) that can spawn a local
stdio MCP server.
## Installation (humans)
### Step 1: get the code
Either clone with git:
```sh
git clone https://github.com/Bippy-s-AI-Helpers/pterodactyl-mcp.git
cd pterodactyl-mcp
```
Or download the repository as a zip / copy the tree manually and `cd` into
the folder.
### Step 2: install and build
```sh
npm install
npm run build
```
`npm run build` compiles `src/` (TypeScript, strict, NodeNext) into
`dist/index.js`; that file is the entrypoint your MCP configuration below
launches.
### npm scripts
| Script | What it does |
|---|---|
| `npm run build` | TypeScript build (strict, NodeNext) into `dist/` |
| `npm run typecheck` | `tsc --noEmit` over src, test, and scripts |
| `npm run test` | the full vitest suite (mock panel + stub wings socket, loopback only) |
| `npm run lint` | eslint over src, test, scripts |
| `npm run smoke` | spawns the real `dist/index.js` and verifies the JSON-RPC handshake, the exact 105-tool registry, the 21 gate annotations, and the description/annotation doctrine |
| `npm run docs` | regenerates `docs/endpoint-reference.md` and `docs/census.md` (deterministic; reruns are byte-identical) |
| `npm run verify` | build, then typecheck, lint, test, smoke in that order |
### Step 3: configure your MCP client (no keys pasted into config)
#### opencode style entry
Values that carry secret material reference files instead of inlining
their contents; see the key handling note below the table.
```jsonc
"mcp": {
"pterodactyl": {
"type": "local",
"command": ["node", "~/.config/opencode/repos/pterodactyl-mcp/dist/index.js"],
"enabled": true,
"environment": {
"PTERODACTYL_URL": "https://panel.example.com",
"PTERODACTYL_CLIENT_KEY": "{file:~/.config/opencode/secrets/PTERODACTYL_CLIENT_KEY}",
"PTERODACTYL_APPLICATION_KEY": "{file:~/.config/opencode/secrets/PTERODACTYL_APPLICATION_KEY}",
"PTERODACTYL_TREES": "client,application"
}
}
}
```
Contract notes: `type` is required (`local`); `command` is an array of
strings, never a single string; `enabled` gates the whole entry; use an
absolute path to `dist/index.js` if your client does not expand `~`.
#### generic JSON MCP client entry
```json
{
"mcpServers": {
"pterodactyl": {
"command": "node",
"args": ["/absolute/path/to/pterodactyl-mcp/dist/index.js"],
"env": {
"PTERODACTYL_URL": "https://panel.example.com",
"PTERODACTYL_CLIENT_KEY": "your-client-api-key",
"PTERODACTYL_APPLICATION_KEY": "your-application-api-key",
"PTERODACTYL_TREES": "client,application"
}
}
}
}
```
For generic clients the two standard patterns are: environment variables
exported into the client's process environment before it spawns the
server, or a dotenv-style file that the loading shell (or the client, if
it supports `envFile`) applies before launch. Never commit such a file;
see `SECURITY.md`.
### Environment variables
The server reads configuration from the environment only. There is no
dotenv and no config file; a dotenv-style loader invoked by the launching
shell is fine, but the server itself never opens one.
| Variable | Required | Default | Meaning |
|---|---|---|---|
| `PTERODACTYL_URL` | yes | none | Panel base URL, absolute `http` or `https`, trailing slash trimmed |
| `PTERODACTYL_CLIENT_KEY` | one key minimum | none | `ptlc_` client API key for the `/api/client` tree; a wrong prefix fails startup with a clear error |
| `PTERODACTYL_APPLICATION_KEY` | one key minimum | none | `ptla_` application API key for the `/api/application` tree; a wrong prefix fails startup |
| `PTERODACTYL_TREES` | no | every tree whose key is set | Comma list of `client` and/or `application` to expose; an unknown name or a tree without its key fails startup |
| `PTERODACTYL_DESTRUCTIVE_ALLOWLIST` | no | empty (deny all destructive) | Comma list of gated tool names permitted past the destructive gate; unknown names are dropped with one warning and can never enable anything; a change requires a restart |
| `PTERODACTYL_ALLOW_KILL` | no | `false` | Permits the `kill` power action; see the gate section. Accepts `true`, `1`, `yes` (and the false spellings `false`, `0`, `no`, empty) |
| `PTERODACTYL_TIMEOUT_MS` | no | `30000` | Per request HTTP timeout in whole milliseconds (positive integer) |
| `PTERODACTYL_CONSOLE_TAIL_LINES` | no | `500` | Console read window tail cap in lines (whole number between 1 and 5000) |
Startup prints exactly one redacted configuration line to stderr (key
prefixes and lengths only, plus the tree list); key material is never
logged, echoed into errors, or returned by any request path. stdout carries
only JSON-RPC. A failed startup prints one line per configuration error to
stderr and exits 1.
### Key handling
- Keys enter the server only through environment variables, at spawn time
from your MCP client.
- In opencode, the supported pattern is a `{file:PATH}` reference: opencode
reads the file at load time, trims it, and substitutes the raw value for
the entry (a missing file fails the config load loudly, which is the
intended behavior). Keep the key files themselves out of any repository
and chmod them `0600` (and `0700` on the containing directory).
- In generic MCP clients, pass the key values in the `env` map of the
entry, or export them into the client's process environment (a
dotenv-style file sourced by your shell works; keep it out of every
repository, keep it `0600`).
- The raw key values never appear in any configuration you commit, in any
log, in any error message, or in any tool result. The startup line
prints only the prefix and the length of each configured key. Key
material moves strictly inside the `Authorization` header of panel
requests, so panel error bodies can never quote it back.
- Changing any entry your client uses to launch this server (including any
`{file:}` referenced key file) requires the client to reload MCP
configuration, and in a typical MCP client configuration is read once at
startup: after changing the entry or the key files, restart the client
(in opencode, a full restart) before the change is live.
## Agentic installation (fresh build agent, follow verbatim)
1. Read this entire README section, plus `SECURITY.md`, before touching
anything.
2. Create the install directory and fetch the repo:
`mkdir -p ~/.config/opencode/repos && cd ~/.config/opencode/repos &&
git clone https://github.com/Bippy-s-AI-Helpers/pterodactyl-mcp.git &&
cd pterodactyl-mcp`. If the host has no git remote access, copy the
repository tree manually into that directory with `cp -r` from wherever
the source is available.
3. Run `npm install` (network access to the npm registry is required for
the dependencies only).
4. Run `npm run build`.
5. Run `npm run smoke` and require exit code 0 (it validates handshake,
tool registry, gate annotations, and redaction; no panel is touched).
6. `STOP HERE` at the user breakpoint. The two API keys are user-supplied
material: never fabricate, never echo, never write key-shaped values of
your own. Editing the user's MCP client configuration and restarting
the client are user-owned steps. Do not create the `mcp` entry, do not
set `PTERODACTYL_DESTRUCTIVE_ALLOWLIST` or `PTERODACTYL_ALLOW_KILL`
yourself, and do not restart the client. Report the built path
(the absolute path of `dist/index.js`) and hand the user steps 7 to 9.
7. (User) install the two panel API key files at
`~/.config/opencode/secrets/PTERODACTYL_CLIENT_KEY` and
`~/.config/opencode/secrets/PTERODACTYL_APPLICATION_KEY` (raw values,
`0600`, `0700` directory).
8. (User) add the `mcp` entry from the installation section above, with
the absolute path from your report.
9. (User) restart the client and confirm the server answers; the one
redacted line on stderr confirms the loaded configuration.
## Tool catalog summary and tree toggles
The registry closes at 105 tools:
| Tree | Family | Tools |
|---|---|---|
| client | account, activity list, permissions dictionary, servers list (the api-key, two-factor, ssh-keys, and account credential routes are doctrine-excluded) | 4 |
| client | server core (server view, resources, activity, stdin command, power) | 5 |
| client | databases | 4 |
| client | backups | 7 |
| client | schedules + tasks | 9 |
| client | network (allocations) | 5 |
| client | files (incl. upload, download URL, raw contents) | 13 |
| client | startup (variables) | 2 |
| client | settings | 3 |
| client | console suite (websocket bridge) | 8 |
| client | subusers | 5 |
| admin | users | 6 |
| admin | nodes + allocations | 9 |
| admin | locations | 5 |
| admin | servers | 11 |
| admin | server databases | 5 |
| admin | nests + eggs | 4 |
Total: 65 client tree + 40 application tree = 105.
`PTERODACTYL_TREES` semantics (code truth, `src/config.ts`):
- unset: every tree whose key is set is exposed (client only with just a
client key; both with both keys).
- set to a comma list of `client` and/or `application`: exactly those
trees are exposed; each named tree must have its key configured, and an
unknown name is a startup error.
- tree gating happens at registration: a disabled family registers no
tools at all, which shrinks `tools/list` and the context your client
spends on it.
### The 21 gated tools (deny by default)
All 21 register even while denied (discoverable failure: the call returns
a structured denial naming the enabling variable, with zero panel
requests). 13 client tree:
`server_power` (kill sub-flag), `database_delete`, `backup_restore`
(truncate sub-flag), `backup_delete`, `schedule_delete`,
`schedule_task_delete`, `allocation_delete`, `files_delete`, `files_pull`,
`settings_reinstall`, `subuser_create`, `subuser_update`, `subuser_delete`
(the access-grant trio is gated because granting panel access is
consequential regardless of the HTTP verb).
8 application tree: `admin_user_delete`, `admin_node_delete`,
`admin_node_allocation_delete`, `admin_location_delete`,
`admin_server_suspend`, `admin_server_reinstall`, `admin_server_delete`
(the force variant is covered by the tool's single allowlist entry),
`admin_server_database_delete`.
The complete endpoint table (all 111 panel endpoints: method, path, tool,
gate class, exclusion reason, citation) is generated at
`docs/endpoint-reference.md`; registry and test counts live in
`docs/census.md`.
## Security model
1. **Transport**: stdio only. The server never opens a network listener;
the MCP client spawns it as a child process.
2. **Tree gate at startup**: a tool family registers only when its tree is
enabled (`PTERODACTYL_TREES` plus the key present).
3. **Discoverable denial**: every gated tool that its tree permits is
registered even while denied; calling it answers with a structured
denial naming the enabling variable and makes zero HTTP requests.
4. **Immutable gate snapshot**: every gate decision reads a snapshot
resolved once at startup, never the runtime environment, so env changes
mid-session cannot flip a gate. Fail-closed: unknown or malformed
allowlist entries never enable anything and are reported once at
startup.
5. **Annotations as hints, never enforcement**: every tool carries
accurate `readOnlyHint` / `destructiveHint` / `idempotentHint`
annotations; the gate decides before annotations could matter.
`PTERODACTYL_DESTRUCTIVE_ALLOWLIST` semantics: by default empty and every
gated tool denies. Entries must name one of the 21 gated tool names above;
anything else (a read or write tool's name, a typo, an unknown family
tool) is dropped with exactly one stderr warning and can never enable
anything. The allowlist is restart-grade: after changing it, restart the
MCP server (restart your MCP client).
`PTERODACTYL_ALLOW_KILL` semantics: the `kill` action of `server_power`
additionally requires this sub-flag `true` even when `server_power` itself
is allowlisted; `start`, `stop`, and `restart` do not need it. The
snapshot is read once at startup, so enabling it is also restart-grade.
TLS verification status of the console bridge: the ticket exchange runs
through the normal client-tree REST client, and the wings upgrade is a
plain `wss`/`ws` connection with the library default TLS verification left
strictly on (no `rejectUnauthorized: false` and no certificate-relaxing
environment variable exists anywhere in the code). A wrong or self-signed
certificate fails loudly as a network error.
Keys and tickets: key material exists only in the environment and inside
request headers to the panel; the startup line prints prefixes and lengths
only; error messages carry method, path, status, and a truncated panel
message (never headers). Console tickets (short-lived JWTs from the panel)
exist only in process memory while a console session is connected. Console
sessions and their bounded tail rings live in memory for the server
process lifetime; nothing is persisted to disk.
## Testing posture
- Mock-first: every vitest suite runs against the in-process mock panel
and stub wings websocket server on loopback (`test/mock/`); no test
touches a real panel.
- The two route file copies under `test/fixtures/` are byte-identical
captures of the panel's route files with pinned sha256 digests; the
drift test reconciles all 111 endpoints, method, path, tree, and
citation against the route table, so a panel update that changes routes
fails with the named row.
- `npm run smoke` runs the real `dist/index.js` over real stdio with fake
key shapes and asserts the handshake, the exact 105-tool registry, the
21 gate annotations, and the short-description doctrine (every tool
describes when to call it, and carries annotations).
- `scripts/live-smoke.mjs` is the read-only battery driver for a run
against a real panel; it reads key material only from the files named by
`PTERO_KEY_FILE_APP` and `PTERO_KEY_FILE_CLIENT`, never prints values,
appends a defensive redactor between every transcript write, and its
default battery is strictly read-only (zero panel state changed). Point
it at the mock first (`scripts/mock-panel-start.mjs` boots the same mock
the test suite uses) before you ever aim it at a real panel.
## Troubleshooting
Startup errors (each prints one stderr line, exit 1):
- `PTERODACTYL_URL is required (panel base URL, for example
https://panel.example.test)` : the URL variable is missing or empty.
- `PTERODACTYL_URL must be an absolute http or https URL (got: ...)` :
malformed URL.
- `PTERODACTYL_CLIENT_KEY must start with ptlc_ (got a different prefix,
length N)` (and the mirrored `ptla_` wording for the application key) :
the variables were swapped or truncated. Verify each key against
Pterodactyl's key prefix conventions (`ptlc_` is a client key,
`ptla_` an application key).
- `At least one API key is required: PTERODACTYL_CLIENT_KEY (ptlc_, client
tree) and/or PTERODACTYL_APPLICATION_KEY (ptla_, application tree)` :
both missing.
- `PTERODACTYL_TREES has an unknown tree name (got: X; expected: client,
application)` : typo in the tree list.
- `PTERODACTYL_TREES names the client tree but PTERODACTYL_CLIENT_KEY is
not set` (and the application-tree mirror) : the tree list names a tree
with no key configured.
- `pteromcp gating: dropped unknown PTERODACTYL_DESTRUCTIVE_ALLOWLIST entries
(...)` : one warning line; the dropped names never enable anything.
Fix the names to the gated tool names above.
- In opencode a missing `{file:...}` key file fails the config load
loudly at startup: restore the file (raw value only) and restart.
Runtime:
- A gated tool call answers `DENIED (destructive gate): "<tool>" is
registered but its gate class is G (destructive), request row <method>
<path>, citation <route line>. By default PTERODACTYL_DESTRUCTIVE_ALLOWLIST
is empty and every gated tool is denied. To enable exactly this tool, set
PTERODACTYL_DESTRUCTIVE_ALLOWLIST to a comma list containing "<tool>" and
restart the MCP server. No panel request was made.` : expected
default-deny behavior; add the tool to
`PTERODACTYL_DESTRUCTIVE_ALLOWLIST` and restart the client.
- `DENIED (kill sub-gate): the "kill" action of "server_power" additionally
requires PTERODACTYL_ALLOW_KILL=true ...` : allowlisting `server_power`
covers start, stop, and restart; killing additionally needs the
sub-flag.
- 401/403 answers from the panel surface as authentication errors
(`PteroAuthError`-class text with method, path, and status): the key is
wrong, expired, or lacks the subuser permissions the route needs.
- 429 answers surface as rate-limit errors carrying the panel's
`Retry-After` hint.
- 502-style HTML from a reverse proxy answers as a non-JSON-body error
(the guard exists exactly for that case).
- Certificate failures on the console websocket surface as console connect
network errors; the bridge never relaxes TLS verification.
- MCP config loads at client startup only: after any server-side env
change, restart the client before retesting.
## License
MIT; see `LICENSE` and `CHANGELOG.md`.This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues