Nodea — logo

Servers — API v1 and MCP | Nodea Documentation

The Servers module is the largest part of the infrastructure API: the full lifecycle of a managed server — create, edit, delete, SSH connection test, note, clone, performance statistics, components (software installation), Ansible operation history, WAF status with rules, and the lifecycle of a paired Proxmox virtual machine (start / reboot / stop). This document covers the REST API v1 surface (/api/v1/servers) and the 17 MCP tools in this module.

Authentication and scopes

API v1 authenticates with a Passport personal access token via the Authorization: Bearer <token> header. Access tiers:

  • infra:read — list, show, statistics, components, operations, WAF status.
  • infra:write — create, edit + non-destructive actions (SSH test, note, clone, component install, VM start and reboot, WAF rule toggle).
  • infra:delete — delete a server (cascades to destroy the paired VM) and VM stop (powering off a production machine is disruptive).

The whole module is additionally gated by the Pennant flag features:servers — disabling the module hides it on both web UI and API/MCP. Rate limit: throttle:api-v1 = 240/min per token user.

Ownership and visibility (ServerPolicy)

Row-level access is enforced by ServerPolicy: view = owner OR accepted FOREIGN share (a collaborator can consume every read endpoint), manage = owner only (the OWNER pivot). All mutations require manage. A server that exists but isn't visible to the token yields 403 (policy); an unknown id yields 404 (route model binding). Listing uses User::servers(), so not-yet-accepted invitations never leak while accepted FOREIGN shares are included (parity with web).

Security — what does NOT enter the response

Field selection is explicit and deliberately omits everything sensitive:

  • ServerResource — omits the sshKey relation (encrypted public/private deploy-key pair, loaded on every query via Server::$with) and the raw info blob from the bootstrap script.
  • VirtualMachineResource — never serializes the VM root_password (encrypted, admin reveal only), the PVE cluster coordinates or the per-VM Proxmox user.
  • ServerComponentResource — masks secret-looking keys in data_rows (containing pass, secret or token********); manage actions persist db_pass and friends into the encrypted data blob, which never round-trips through the API.
  • ServerOperationResource — does not serialize the raw output blob (full Ansible JSON with possible playbook variables); the machine-readable status is the API contract.

API v1 — endpoints

MethodPathScopeDescription
GET/api/v1/serversinfra:readThe user's servers (owned + accepted shares), paginated; ownership.
GET/api/v1/servers/{server}infra:readServer detail (components + paired VM).
GET/api/v1/servers/{server}/statisticsinfra:readCPU/RAM/disk series (latest Prometheus samples).
GET/api/v1/servers/{server}/componentsinfra:readServer components and their status.
GET/api/v1/servers/{server}/components/{component}infra:readComponent detail (scoped binding) with data_rows.
GET/api/v1/servers/{server}/operationsinfra:readOperation history (Ansible), newest first; simplePaginate.
GET/api/v1/servers/{server}/wafinfra:readWAF status: has_agent, rules, latest 20 events.
POST/api/v1/serversinfra:writeCreate a server (creator = OWNER). Returns 201.
PUT/PATCH/api/v1/servers/{server}infra:writeEdit name/SSH port (IP immutable; owner-only).
POST/api/v1/servers/{server}/test-sshinfra:writeSSH connection test with the deploy key. {ok, message} or 502.
PATCH/api/v1/servers/{server}/noteinfra:writeSet/clear the note.
POST/api/v1/servers/{server}/cloneinfra:writeClone a server (create-server quota; VM detached). Returns 201.
POST/api/v1/servers/{server}/components/{component}/installinfra:writeInstall a component (ansible queue). Returns 202 + the operation.
POST/api/v1/servers/{server}/vm/startinfra:writeStart the VM (async). 422 when no VM paired.
POST/api/v1/servers/{server}/vm/rebootinfra:writeReboot the VM (synchronous PVE). 422/409/502.
POST/api/v1/servers/{server}/waf/rules/{rule}/toggleinfra:writeEnable/disable a WAF rule; enabled (bool). 422 without a fleet agent.
DELETE/api/v1/servers/{server}infra:deleteDelete the server + async destroy the VM. Returns 204.
POST/api/v1/servers/{server}/vm/stopinfra:deleteStop the VM (destructive — powers off a production machine). 422 without a VM.

Total: 18 endpoints (7 read, 9 write, 2 delete).

GET /servers — query parameters

  • search (string, optional, max 255) — matched against name and IP.
  • sort (enum: name|ip|created_at, default created_at) — hard whitelist.
  • direction (asc|desc; defaults to desc for created_at).
  • per_page (int 1–100, default 20), page (int ≥1).

ServerResource — fields

id (UUID), name, ip, ssh_port, status (lowercase: new|connected|installed|error), note, ownership (owner|shared, index only via the pivot), has_virtual_machine (bool), virtual_machine (object or null), components[] (when loaded), created_at, updated_at. The virtual_machine object: id, vm_id (Proxmox VMID), name, ip, status, last_error, plus optional plan, os_template, location.

POST/PATCH /servers — body parameters

  • name (string, required, min 3, max 100).
  • ip (required for POST, ipv4; immutable — absent from PATCH).
  • ssh_port (required, numeric).

Note: note (string, nullable). WAF toggle: enabled (bool, required). Install: the URL param {component} = a Component enum value (e.g. nginx, php83, mariadb1011, redis, postgres16, nodejs20).

Error codes

  • 401 — missing/invalid token.
  • 403 — missing scope or an existing, not-visible server (policy).
  • 404 — unknown id; a component from another server (scoped binding).
  • 409 — no PVE assignment on reboot (no_pve_assignment).
  • 422 — validation; no paired VM (no_virtual_machine); no fleet agent (no_fleet_agent).
  • 502 — failed SSH test / failed reboot (reboot_failed).

Examples — API

List servers, sorted

curl -s "https://app.nodea.io/api/v1/servers?sort=name&direction=asc&per_page=25" \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Create a server

curl -s -X POST https://app.nodea.io/api/v1/servers \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "web-01", "ip": "203.0.113.10", "ssh_port": 22 }'

Install a component

curl -s -X POST https://app.nodea.io/api/v1/servers/{id}/components/nginx/install \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Reboot the virtual machine

curl -s -X POST https://app.nodea.io/api/v1/servers/{id}/vm/reboot \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Stop the VM (destructive, infra:delete scope)

curl -s -X POST https://app.nodea.io/api/v1/servers/{id}/vm/stop \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

MCP — tools (17)

The MCP tools share the authentication and scope model with API v1 (Bearer + scope in ensureScope) and the features:servers flag. Servers are addressed by UUID; a missing or inaccessible id raises the same error (no cross-tenant enumeration — InteractsWithServers::serverFor defers to the view/manage policies). Secrets are masked exactly as in the API (the same resource shapes). Destructive tools (server-delete, server-vm-stop) require confirm: true.

ToolScopeModeDescription
servers-listinfra:readread-onlyList servers (owned + FOREIGN shares); search, page, per_page (max 50). Returns the ownership label.
server-getinfra:readread-onlyServer detail (components + VM). Param: id.
server-statisticsinfra:readread-onlyCPU/RAM/disk series. Param: id.
server-componentsinfra:readread-onlyComponent list; id, page, per_page.
server-operationsinfra:readread-onlyOperation history; id, component filter, pagination. No raw playbook output.
server-waf-statusinfra:readread-onlyWAF status: has_agent, rules, latest 20 events. Param: id.
server-createinfra:writewriteCreate a server; name (min 3), ip (ipv4), ssh_port. create-server quota.
server-updateinfra:writeidempotentEdit name/ssh_port (partial; IP immutable). Owner-only.
server-set-noteinfra:writeidempotentSet/clear the note; id, note (nullable).
server-test-sshinfra:writewrite (dials infra)SSH test with the deploy key; {connected, error}. Owner-only.
server-cloneinfra:writewriteClone a server; id. create-server quota; VM detached.
server-component-installinfra:writewriteInstall components; id, components[] (min 1, enum values). Ansible queue, staggered 10s apart.
server-vm-startinfra:writewriteStart the VM; id. no_virtual_machine when no VM.
server-vm-rebootinfra:writewriteReboot the VM (synchronous PVE); id.
server-waf-rule-toggleinfra:writeidempotentToggle a WAF rule; id, rule_id (max 64), enabled. Ships the full override set to the nodead agent.
server-deleteinfra:deletedestructiveDelete the server + async destroy the VM; id + required confirm: true. Owner-only.
server-vm-stopinfra:deletedestructiveStop the VM (offline until restarted); id + required confirm: true. Owner-only.

Breakdown: 6 read-only, 9 write, 2 destructive.

Examples — MCP (JSON-RPC)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "server-component-install",
    "arguments": { "id": "9b1f...uuid", "components": ["nginx", "php83"] }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "server-vm-stop",
    "arguments": { "id": "9b1f...uuid", "confirm": true }
  }
}