Nodea — logo

DNS — API v1 and MCP | Nodea Documentation

The DNS module manages zones and records hosted on our authoritative PowerDNS cluster: list zones, view a zone with its full record set, and create, update or delete individual records. This document covers the REST API v1 surface (/api/v1/dns) and the 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 zones and view a zone with its records (index, show).
  • infra:write — create, update and delete records. A record is subordinate zone configuration, so deleting it does NOT require infra:delete (that tier is reserved for destroying whole resources such as devices or servers).

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

Ownership and visibility

The PowerDnsZone model has no policy class — row-level access is hard-scoped to user_id = token user. A foreign, system (user_id NULL, shared infrastructure) or unknown zone always yields 404 — the API never confirms that a foreign zone exists (no cross-tenant enumeration). The {record} parameter uses scoped route binding against {zone}, so a record belonging to another zone yields 404 before the controller runs.

Mutations mirror the web UI: the local row is written first (the database is the source of truth), then a synchronous push to PowerDNS follows. Transport errors are caught and logged — a failed push does not fail the request; state is reconciled by a scheduled job.

API v1 — endpoints

MethodPathScopeDescription
GET/api/v1/dns/zonesinfra:readThe user's zones, paginated; with a record count.
GET/api/v1/dns/zones/{zone}infra:readZone detail with the full record set (sorted by type, name).
POST/api/v1/dns/zones/{zone}/recordsinfra:writeCreate a record in the zone. Returns 201.
PATCH/api/v1/dns/zones/{zone}/records/{record}infra:writePartial record update (only submitted fields). Scoped binding.
DELETE/api/v1/dns/zones/{zone}/records/{record}infra:writeDelete a record (scoped binding). Returns 204.

GET /dns/zones — query parameters

  • search (string, optional, max 255) — matched against the zone name.
  • per_page (int 1–100, default 20), page (int ≥1).

Response: a DnsZoneResource collection — fields: id, name, kind, serial (SOA), dnssec (bool), records_count, last_synced_at, created_at, updated_at. show adds a records[] array (DnsRecordResource). Internal zone config (masters, nsec3param, the remote PowerDNS handle) stays server-side and is not serialized.

DnsRecordResource — record fields

id, zone_id, name (FQDN), type, content, ttl, priority (nullable), comment (nullable), disabled (bool), created_at, updated_at.

POST/PATCH — record body parameters

  • name (string, max 253) — accepts @ (zone apex), a bare label (suffixed with the zone) or a fully-qualified FQDN.
  • type (enum) — one of: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, PTR.
  • content (string, max 1000) — IP address, target host or text value.
  • ttl (int, 60–604800 s).
  • priority (int, 0–65535, optional) — for MX/SRV.
  • comment (string, max 255, optional).
  • disabled (bool, optional) — record created/marked as not served.

POST requires the full name/type/content/ttl. PATCH is partial — only submitted fields change (the rest stays intact), unlike the web UI which re-posts the whole record each time.

Error codes

  • 401 — missing/invalid token.
  • 403 — missing required scope.
  • 404 — unknown, foreign or system zone; a record from another zone (scoped binding).
  • 422 — validation (disallowed type, TTL out of range, name/content too long).

Examples — API

List zones with search

curl -s "https://app.nodea.io/api/v1/dns/zones?search=example&per_page=20" \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Create an A record

curl -s -X POST https://app.nodea.io/api/v1/dns/zones/42/records \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "www",
    "type": "A",
    "content": "203.0.113.10",
    "ttl": 3600
  }'

An MX record with priority

curl -s -X POST https://app.nodea.io/api/v1/dns/zones/42/records \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "@",
    "type": "MX",
    "content": "mail.example.com",
    "ttl": 3600,
    "priority": 10
  }'

Partial update (TTL only)

curl -s -X PATCH https://app.nodea.io/api/v1/dns/zones/42/records/1001 \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "ttl": 300 }'

MCP — tools

The MCP tools share the authentication and scope model with API v1 (Bearer + scope checked in ensureScope) and the features:hosting-suite flag (a tool on an inactive flag disappears from tools/list). Zones and records are addressed by integer ids; a missing, foreign or system id raises the same error (no cross-tenant enumeration). Write responses carry a synced flag — false means the push was deferred to the reconciler, not an error.

ToolScopeModeDescription
dns-zones-listinfra:readread-onlyList zones; search, page, per_page (max 50). Returns id/name/kind/serial/dnssec/count/last_synced.
dns-zone-getinfra:readread-onlyZone with every record (sorted type, name). Param: id.
dns-record-createinfra:writewriteAdd a record; zone_id, name, type, content, ttl, optional priority/comment/disabled.
dns-record-updateinfra:writeidempotent (full replace)Replace a record; name/type/content/ttl required every time (read them first via dns-zone-get).
dns-record-deleteinfra:writedestructiveDelete a record; requires confirm: true. The name stops resolving immediately.

Example — MCP (JSON-RPC)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "dns-record-create",
    "arguments": {
      "zone_id": 42,
      "name": "www",
      "type": "A",
      "content": "203.0.113.10",
      "ttl": 3600
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "dns-record-delete",
    "arguments": { "zone_id": 42, "record_id": 1001, "confirm": true }
  }
}