MCP server
RedundantDNS exposes every dashboard capability as an MCP tool at
<public URL>/mcp (for example https://app.redundantdns.com/mcp). It uses
the official MCP Go SDK over Streamable HTTP, stateless: each POST is
served on its own, with JSON responses, so any instance answers any request
and there is no session to keep (GET and DELETE answer 405).
Connecting from Claude
- In Claude (claude.ai or Claude Desktop): Settings → Connectors → Add
custom connector, URL
https://<your host>/mcp. - Claude discovers the authorization server from the
401challenge (WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource/mcp"), registers itself (dynamic client registration) and opens the consent page. - Sign in, pick the organization and the permissions, approve. Claude receives an access token (1 hour, refreshed automatically).
Clients whose redirect hosts are all in RDNS_OAUTH_TRUSTED_DOMAINS
(default claude.ai,claude.com) skip the consent screen after the first
approval of the same scopes for the same organization. Any other client
always asks.
With a personal access token
Clients that take a static header (Claude Code, scripts, CI) can use a personal access token created in Settings → API tokens:
claude mcp add --transport http redundantdns https://<your host>/mcp \
--header "Authorization: Bearer rdns_..."
A dashboard session cookie never authorizes /mcp.
Authorization
| Scope | Allows |
|---|---|
zones:read | read zones, records, status, delegation checks, alerts and the audit log |
zones:write | change zones, records and attachments, run reconcile/verify/adopt, resolve alerts, test alert channels |
connections:read | list provider connections |
connections:write | create and test provider connections |
A tool call needs its scope and the user's role in the organization
(the same rule as /v1: viewers read, editors change records, admins
manage connections, attachments and zone deletion). The organization is the
token's (a PAT is bound to one org; an OAuth grant to the org picked at
consent). The org and token IP allowlists apply, with the client IP
resolved behind trusted proxies (RDNS_TRUSTED_PROXIES).
MCP is a plan feature (Pro and above, see docs/api.md): on other plans
every /mcp request answers 402 plan_limit_reached with the plan that
unlocks it. Tool calls that hit a plan limit (a zone over the limit, a
third provider) return error plan_limit_reached: ... with the details.
Errors come back as tool errors (isError: true) with the API's error code,
for example error insufficientScope: the token lacks the connections:read scope
or error apexNsManaged: the apex NS set is managed by the platform.
Tools
| Tool | Scope | What it does |
|---|---|---|
zone_list | zones:read | list zones (attachments, NS plan, status; no records) |
zone_get | zones:read | one zone with records, attachments, capabilities and status |
zone_create | zones:write | create a canonical zone; parentDelegation (on by default when a parent zone of the org exists) writes its NS delegation into that parent |
zone_delete | zones:write | delete the canonical zone; confirmName must repeat the zone name (admins) |
record_list | zones:read | record sets of a zone (managedBy: "delegation" marks a child zone's delegation, read-only) |
record_upsert | zones:write | create or replace a record set (previousName/previousType rename one) and reconcile |
record_delete | zones:write | delete a record set and reconcile |
provider_connection_list | connections:read | provider connections (credentials never returned) |
provider_connection_create | connections:write | test and save a connection: BYO credentials, or mode: managed (admins; acceptManagedTerms: "<version>" accepts the Managed Provider Terms once for the org, otherwise the call fails with managed_terms_required) |
provider_connection_test | connections:write | re-test a stored connection |
zone_attach_provider | zones:write | attach a connection (creates or adopts the provider zone) (admins) |
zone_detach_provider | zones:write | detach; deleteRemote + confirmName deletes the provider zone (admins) |
zone_reconcile | zones:write | push the canonical zone to the providers now |
zone_verify | zones:write | compare providers with the canonical zone now |
zone_adopt_changes | zones:write | import one provider's records into the canonical zone |
zone_delegation_check | zones:read | check the delegation (parent NS vs NS plan) now |
zone_status | zones:read | sync state per attachment and the last delegation check |
audit_list | zones:read | latest audit events, optionally for one zone |
audit_export | zones:read | every audit event of a period (from, to, optional zoneId), oldest first; admins, Business plan and above |
alert_list | zones:read | alert history (drift, sync errors, delegation, providers down or degraded, zones not verified); filters zoneId, rule, state, limit |
alert_resolve | zones:write | resolve a firing alert by hand (editors); the channels are notified |
alert_channel_test | zones:write | send a test notification to an alert channel now (admins) |
billing_get | zones:read | the plan, billing status, limits with usage and the managed pass-through of the period (upgrades happen in the dashboard) |
Tool names match ^[a-zA-Z0-9_-]{1,64}$ (Claude rejects the whole list
otherwise); internal/mcp tests enforce it and that the catalog matches
this table.
Audit
Every tool call is written to the org's audit log as mcp.tool.call
(source mcp, target mcpTool/<name>) with a summary of the arguments,
the resulting HTTP status and error code, the auth kind (pat or oauth)
and the OAuth client id. Values under credential-like keys
(credentials, secret, password, token, privateKey...) are never
logged: only their field names. The action the tool performed (for example
zone.create) is audited as usual, with source mcp.
Design notes
- A tool does not re-implement the product: after its scope check it runs
the matching
/v1request in process as the caller, so validation, role checks, capability intersection and audit are exactly the API's. Adding a/v1capability means adding its tool ininternal/mcp/tools.go. - Each POST builds a fresh MCP server bound to the caller (the tool schemas are cached), which is what makes the endpoint stateless.
- DNS-rebinding protection of the SDK is off:
/mcponly accepts bearer tokens, and the check would reject a reverse proxy on the same host.
Try it with curl
curl -s -X POST https://<host>/mcp \
-H "Authorization: Bearer rdns_..." -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s -X POST https://<host>/mcp ... \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"zone_create","arguments":{"name":"example.com"}}}'