Skip to main content
Glama

CustomDomain™ MCP Server: custom domains for AI agents

This is the official CustomDomain™ product MCP server. Its only endpoint is https://mcp.customdomain.ai/mcp, listed in the official MCP registry as ai.customdomain/mcp, with the OAuth scopes domains:read, domains:connect, domains:disconnect and domains:purchase. No other host serves it, so a server at any other address that presents itself as CustomDomain™ is not this one.

The MCP server for custom domains: let Claude and other AI agents find, connect, diagnose and manage domains.

Status: Production · server 0.6.0 · release v0.6.0

live MCP registry protocol docs license

Website · Docs · Console · Product page · MCP docs · Tool reference · Provider coverage · Pricing

Install: Install in VS Code Install in VS Code Insiders · Claude Code, Claude Desktop and Cursor: Quickstart · GitHub Copilot cloud agent: configuration

Stars are how other developers find this server on GitHub. If it is useful to you, star the repository.

What it is

A hosted MCP server that gives an AI agent twenty domain tools

Who it's for

Agent builders, SaaS platforms and coding-agent users who need a customer on their own domain

Live at

customdomain.ai/mcp-server · endpoint https://mcp.customdomain.ai/mcp

Stack

Go 1.26 · JSON-RPC 2.0 over streamable HTTP · hosted, no datastore in the server

Status

Production · 0.6.0 in the official MCP registry · 20 tools · 178 DNS providers catalogued, 62 automatic

CustomDomain™ MCP is a hosted Model Context Protocol server that lets an AI agent put a person on their own domain: connect a domain they already own, set up its email records, watch it go live with HTTPS, and find out why it has not. It replaces the part of that job a human normally does by hand in a DNS control panel. There is nothing to install and nothing to keep running: point your MCP client at the endpoint above.

flowchart LR
  A["Agent<br/>Claude · Cursor · ChatGPT"] -->|"JSON-RPC over HTTPS"| M["mcp.customdomain.ai<br/>20 tools"]
  M -->|"credential forwarded"| C["Control plane<br/>api.customdomain.ai"]
  C --> R["Registrars<br/>search · price"]
  C --> D["DNS providers<br/>178 catalogued · 62 automatic"]
  C --> E["Edge<br/>TLS issued"]

The problem

Every SaaS product eventually needs a customer on their own domain: a white-label app host, a vanity link, a sending domain for email. The work is small and it is always the same: figure out who runs the customer's DNS, write the right records in the right shapes, wait for them to propagate, then get a certificate. Done by hand it is a support ticket. Done in code it is a DNS provider integration you did not want to own, times sixty.

Agents make this worse before they make it better. The obvious way to let an agent do the job is to give it DNS credentials and a way to write records, which means one confused turn or one poisoned web page can now edit a customer's zone. Nobody wants to hand a language model a SetRecords call on a production domain, and the alternative (an agent that reads instructions aloud and asks the user to go type them somewhere) is the support ticket again, with extra steps. That is not a permissions problem you can prompt your way out of; it is an argument-list problem, and it gets solved by changing what the agent is able to say.

Related MCP server: Bloomfilter

Who it's for

  • Teams building agents that ship things. A coding agent, a site builder, a deploy bot: anything whose output is a running app that needs an address a person will actually type. The agent connects the domain and polls it to live instead of stopping at the last mile with a paragraph of DNS instructions.

  • SaaS platforms with tenant domains. If your customers connect their own domain to your product, this is the same engine the CustomDomain™ widget and REST API run on, reached through the protocol your agents already speak.

  • Email and messaging platforms. Getting a sending domain authenticated (MX, SPF, DKIM, DMARC, in the right shapes, at the customer's provider) is one call here instead of a documentation page and a support queue.

  • Anyone with a domain portfolio to keep alive. Connections drift, certificates need records that still resolve, and an agent that can list, diagnose, recheck and re-apply is cheaper than a person doing it quarterly.

What it does

Twenty tools, one credential, no DNS records in any argument. Each of these is a tool call, not a workflow you have to assemble:

  • Check whether a domain can be registered, with live purchase and renewal pricing: search-domain-availability

  • Suggest names that are actually free, deterministically, from a set of keywords: generate-domain-suggestions

  • Register a domain through the resolved registrar, behind a fail-closed authorization gate. Currently off on the hosted service: create-domain-order

  • Connect a domain the customer already owns and get back the exact records that must exist: connect-domain

  • Detect the DNS provider before you ask anyone for anything, with per-rail verdicts and conflict and CAA pre-flight: discover-provider

  • Poll a connection to completion, from records written to certificate issued: check-connection-status

  • Explain why a domain is not live, record by record: diagnose-connection

  • Re-run the DNS check now once the person has fixed it: recheck-connection

  • Read the HTTPS certificate (expiry, failing renewals): check-certificate-status

  • Poll a registration order across two different ledgers without guessing which one applies: check-order-status

  • List the purchases made through the account: list-domain-orders

  • Set up email in one call (MX, SPF, DKIM and DMARC from a server-side template): add-email

  • Point a domain somewhere permanently with a 301: forward-domain

  • Repair a domain that drifted out of live, using a stored grant: reapply-connection

  • Take a domain down, after the person confirms which one: disconnect-domain

  • Inventory the whole account, page by page, so an agent can reconcile a portfolio, not just the job it started: list-connections

  • Check whether the workspace may connect new domains before a batch, or after a billing_required refusal: check-billing-status

  • Move a domain's nameservers to the application's DNS host, records copied first, mail included, so its records are kept correct from then on: migrate-nameservers

  • Follow that move to the end, and relay the step a person has to take: check-nameserver-status

  • Move the nameservers back, to the old ones or to the registrar's own DNS: rollback-nameservers

Full arguments, scopes and response shapes: TOOLS.md.

The receipts, all verifiable from outside: the server is published in the official MCP registry as ai.customdomain/mcp at 0.6.0, speaks protocol revision 2025-06-18, lists its version and tools without authentication at /.well-known/agent/mcp.json, and answers an unauthenticated call with a RFC 9728 challenge that names its own metadata document rather than a bare 401. Automatic setup covers 62 of the 178 providers catalogued (55 by scoped API token, 6 by provider OAuth, 1 by Domain Connect), and the other 116 still work, returning records and a link for a human to apply. The live split is at /v1/providers/census.

Every one of those tools was built against the same lesson: an agent that returns a console link and stops has not done the job. The tools return the authoritative record set itself, so the agent can tell a person exactly what to add, and the completion signal is a boolean rather than a status string an integration can misread.

Use cases

  • A coding agent shipping a side project. "Point my domain at this app." discover-provider, connect-domain, then check-connection-status until connected is true. The one human step is authorizing at the DNS provider, or adding the returned records.

  • A SaaS onboarding an enterprise customer. The agent calls discover-provider first, sees the customer runs a provider that cannot hold a root-domain alias, and warns before anyone authorizes anything.

  • An email platform. add-email writes MX, SPF, DKIM and DMARC from a template; the agent supplies the per-domain DKIM selector and nothing else.

  • A platform with a thousand tenant domains. list-connections with status=failed (following nextCursor until the list is complete), keep the rows with managed: true, reapply-connection on those, diagnose-connection on the rest and hand them to a human. That is a reconciliation loop an agent can run on a schedule, and it is the reason the inventory tool exists at all.

  • A support agent answering "why is my domain not working yet". diagnose-connection says which record is missing or wrong and why (a hostname typed into the Name field, a proxy in front), and recheck-connection confirms the fix without waiting for the next scheduled check.

  • A platform that keeps its users' domains working. When the application maintains domains, the agent asks the person, calls migrate-nameservers with confirm: true, and polls check-nameserver-status until done, relaying nextStep whenever the person has to act at their registrar. rollback-nameservers undoes it.

Buying a domain through MCP is built but not available on the hosted service: purchases are authorization-gated, and no purchase authorization callback is configured there, so create-domain-order answers rejected. Buy in the console, then connect the domain here.

Why an MCP server and not a REST API

There is a REST API, and it is the thing this server calls. The difference is what an agent is allowed to say. The REST API accepts DNS records; the MCP tools do not. Every record value is computed by the control plane from a vetted template, so the worst thing a prompt-injected agent can do here is connect the wrong domain, not write an arbitrary MX record into a customer's zone. The security model is the argument list. On top of it, a delegated agent token carries only the scopes a person approved, and removing a domain is its own scope and needs the domain named back in confirm. Paid registration is gated separately: create-domain-order needs the domain repeated in confirm_domain, and places an order only after the integrator's purchase-authorization callback approves it; an unconfigured callback denies every purchase. A nameserver move needs confirm: true, and an agent token cannot choose where the nameservers go: the application's setting decides.

The second reason is discovery. An agent that has never heard of this product can find it: the server is in the official MCP registry, it publishes an agent manifest and an OAuth protected-resource document at well-known paths, and an unauthenticated call answers with a challenge naming its protected-resource document rather than a bare 401. An OAuth-capable client follows that document to the authorization server, signs the person in and gets a scoped token with no key to copy. A REST API cannot introduce itself that way.


Quickstart

Most clients sign in with OAuth, so there is no key to copy: a browser opens, a person approves what the agent may do, and the agent gets a scoped token. Clients that cannot sign in use an API key from the console at app.customdomain.ai (API & SDK). Keys look like cd_live_... or cd_test_... (older sk_live_ / sk_test_ keys keep working). Keep a key in an environment variable, never in a file you commit.

Claude Code, sign in with OAuth:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp
# then run /mcp inside Claude Code and sign in

or with an API key from your environment:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
  --header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Claude Desktop: Settings → Connectors → Add custom connector. Enter the URL https://mcp.customdomain.ai/mcp, add it, then choose Connect and sign in (OAuth).

Cursor cannot sign in to a new server by itself yet, so it uses a key. Put this in your user-level ~/.cursor/mcp.json, not in a project's .cursor/mcp.json that gets committed, and set CUSTOMDOMAIN_API_KEY in the environment Cursor starts from:

{
  "mcpServers": {
    "customdomain": {
      "url": "https://mcp.customdomain.ai/mcp",
      "headers": { "Authorization": "Bearer ${env:CUSTOMDOMAIN_API_KEY}" }
    }
  }
}

Any HTTP client: the endpoint is plain JSON-RPC 2.0, so you can call a tool with curl:

curl -X POST https://mcp.customdomain.ai/mcp \
  -H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"discover-provider","arguments":{"domain":"example.com"}}}'

The console's MCP server page has copy-and-paste setup for these and for Codex, VS Code, ChatGPT and other clients, plus a connection test.

VS Code

The install badges at the top add this entry. You can also put it in .vscode/mcp.json yourself, or run MCP: Add Server:

{
  "servers": {
    "customdomain": { "type": "http", "url": "https://mcp.customdomain.ai/mcp" }
  }
}

The entry holds no key. The server answers the first request with a 401 whose WWW-Authenticate header names its protected-resource metadata, and that document names https://api.customdomain.ai as the authorization server (authorization code with PKCE, and dynamic client registration). A client that follows the challenge, as VS Code documents it does, opens a browser for a person to sign in and approve the scopes. To use an API key instead, add a header that VS Code asks for once and stores as a secret:

{
  "servers": {
    "customdomain": {
      "type": "http",
      "url": "https://mcp.customdomain.ai/mcp",
      "headers": { "Authorization": "Bearer ${input:customdomain-key}" }
    }
  },
  "inputs": [
    { "type": "promptString", "id": "customdomain-key", "description": "CustomDomain™ API key", "password": true }
  ]
}

GitHub Copilot cloud agent

Copilot cloud agent (the coding agent that works on issues and pull requests) does not support remote MCP servers that sign in with OAuth, so it sends the credential as a header. In the repository, open Settings → Copilot → MCP servers, paste this configuration and save it:

{
  "mcpServers": {
    "customdomain": {
      "type": "http",
      "url": "https://mcp.customdomain.ai/mcp",
      "headers": { "Authorization": "$COPILOT_MCP_CUSTOMDOMAIN_AUTH" },
      "tools": ["*"]
    }
  }
}

Then add an Agents secret named COPILOT_MCP_CUSTOMDOMAIN_AUTH (Copilot only passes secrets whose names start with COPILOT_MCP_). Its value is the whole header value, Bearer followed by one of:

  • an API key from the console's API & SDK page (cd_live_...), which acts for the whole workspace, or

  • APPLICATION_ID:CLIENT_SECRET for an application created with POST /v1/applications, which returns the secret once. The server exchanges the pair for a 15 minute token on every request, the same exchange POST https://mcp.customdomain.ai/token does with the client_credentials grant, and the agent acts for that one application. An application created in the console never shows its secret, so use an API key for those.

"tools": ["*"] lets the agent call every tool without asking, including disconnect-domain, which takes a domain offline. To keep the agent to reading and diagnosis, list tools instead, for example ["discover-provider", "check-connection-status", "diagnose-connection", "list-connections", "check-certificate-status"].

Client support

Client

How it connects

Notes

Claude Code

Native streamable HTTP

claude mcp add --transport http; sign in with /mcp, or pass a key header

Claude Desktop

Custom connector

Settings → Connectors → Add custom connector; signs in with OAuth

Cursor

Native HTTP with headers

~/.cursor/mcp.json with the key from an environment variable; no sign-in yet

ChatGPT

Developer-mode app

Signs in with OAuth; it cannot send an API key

Anything else

POST /mcp, JSON-RPC 2.0

Follow the 401 to sign in, or send a Bearer in the Authorization header

Tools and the three conventions

Three rules run through every tool, and they are the things integrators get wrong.

No tool accepts a DNS record. add-email takes mail settings (an MX host, a DKIM selector) and the template turns those into records. forward-domain takes a redirect target. Neither takes a record.

connected: true is the completion signal. Status strings are informative; the boolean is what you branch on, and it stays stable if the status enum ever grows.

A call that cannot be taken back carries the person's yes. disconnect-domain takes the connectionId and confirm, the domain the person confirmed; create-domain-order repeats the domain in confirm_domain; migrate-nameservers and rollback-nameservers take confirm: true. A call without it is refused before anything happens.

There is also no ownership-challenge step: no tool asks for or checks a TXT verification record. Control of the domain is proven by the rail that writes the records (an OAuth authorization at the provider, a one-click apply, or a scoped API token), and on the manual path the control plane simply waits for the records to appear in public DNS.

Authentication

POST /mcp accepts four bearer credentials.

Credential

Issued by

Acts for

API key cd_live_ / cd_test_ (legacy sk_live_ / sk_test_)

The console, API & SDK. Every workspace has a default key; a key you create is shown once.

The whole workspace

JWT from POST /token

client_credentials exchange using application_id + client_secret. Expires in 15 minutes.

One application

CLIENT_ID:CLIENT_SECRET

The shortcut for hosts that cannot run the token exchange; the server exchanges it on each request

One application

Delegated agent token

OAuth at https://api.customdomain.ai: dynamic client registration, authorization code with PKCE (S256), a person approves the scopes

One application, only the approved scopes

curl -X POST https://mcp.customdomain.ai/token \
  -u "<APPLICATION_ID>:<CLIENT_SECRET>" \
  -d "grant_type=client_credentials"
# -> { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900 }

APPLICATION_ID and CLIENT_ID are the same value under two names.

Integrator credentials (an API key, a /token JWT, the CLIENT_ID:CLIENT_SECRET form) are not validated by the server itself: it forwards them to the control plane, which enforces the same permissions as the REST API on every call. They carry no scopes. Revoking a key in the console cuts the agent off on its next call; today that call answers a tool failure with error: "unauthorized" rather than an HTTP 401.

Delegated agent tokens are ES256 tokens issued for the audience https://mcp.customdomain.ai/mcp. The server verifies them locally against the control plane's JWKS, refuses one issued for another audience with 401 invalid_token, and checks the tool's scope on every call:

Scope

Tools

domains:read

search-domain-availability, generate-domain-suggestions, check-connection-status, check-order-status, discover-provider, list-connections, check-certificate-status, diagnose-connection, check-billing-status, list-domain-orders, check-nameserver-status

domains:connect

connect-domain, reapply-connection, forward-domain, add-email, recheck-connection, migrate-nameservers, rollback-nameservers

domains:disconnect

disconnect-domain

domains:purchase

create-domain-order

A token without the tool's scope gets JSON-RPC error -32003, insufficient_scope, which names the scope it needed. See Agent access and Authentication.

Prefer an agent token or an application-scoped JWT for anything an agent touches: a leaked cd_live_ key carries the whole workspace.

Rate limits

Tool calls are limited to 5 a second with bursts of 20 per caller. The caller is the workspace for a delegated agent token (all of a workspace's agents share one bucket), the credential itself for an API key or a /token JWT (a workspace with three keys has three buckets), and the application for the CLIENT_ID:CLIENT_SECRET form. Over the limit the call answers a tool failure with error: "rate_limited" and retryAfterSeconds, and never reaches the control plane. The control plane also limits each client address.

Lifecycle, polling and errors

Connections move pending → propagating → live. failed is a diagnosis, not a tombstone.

Status

Meaning

pending

Created. Records not yet written, or not yet observed in public DNS. A manual connection never fails on its own: after 72 hours it stays pending with errorCode: "setup_incomplete" and is re-checked every six hours.

propagating

A rail wrote the records. The poller is checking them against public DNS by value.

live

Every desired record resolves to its intended value and the edge serves TLS for the host.

failed

Records a rail wrote never resolved within 24 hours (errorCode: "propagation_timeout"), or a rail rejected the write. It clears if the connection later re-verifies.

The background poller re-checks about once a minute, so polling faster than every 30 to 60 seconds gains nothing. failed is recoverable and is not a reason to tell a user to start over: diagnose-connection says what is wrong, and recheck-connection re-runs the check once it is fixed. The full lifecycle is at Connections.

Errors come in two shapes. A malformed call fails at the JSON-RPC layer: -32602 for invalid or missing arguments, -32601 for an unknown tool, -32700 for unparseable JSON, and -32003 for a missing scope. Those are caller bugs. A call the control plane refused comes back as a tool result flagged isError: true, in one shape whichever tool made it:

{ "success": false, "error": "billing_required", "message": "A card is needed to connect more domains.", "details": "..." }

error is the control plane's code when it sent one, else the tool's fallback code (connect_failed, status_failed, list_failed and so on). Branch on error; show message. The full list is in TOOLS.md.

How it works

This repository is the public face of a hosted service: the registry manifest that lists the server, the tool contract, and this page. The server itself is a small Go process. It terminates JSON-RPC, verifies delegated agent tokens and their scopes, limits each caller's rate, and forwards each tool call to the CustomDomain™ control plane, which owns provider detection, the record templates, the registrar rails, the propagation poller and certificate issuance. That split is why the tool surface can stay this narrow: all the state and all the judgment live one hop away.

Repository layout

.
├── README.md      # this file: what the server is, how to point a client at it, how it is built
├── TOOLS.md       # the tool contract: every argument, scope, response field, status value and error code
├── CHANGELOG.md   # one entry per server version, matching the tags and GitHub releases
├── server.json    # MCP registry manifest: name ai.customdomain/mcp, version 0.6.0, remote endpoint, tools and scopes
├── glama.json     # Glama directory ownership claim
└── LICENSE        # MIT

Service modules

The server source lives in the private CustomDomain™ monorepo under services/mcp, alongside the control plane it calls. Named here so the boundaries are legible, not linked, because the repository is closed.

Module

Purpose

Talks to

server.go

Routes initialize, tools/list, tools/call; registers the twenty tools in a fixed order; checks scope and rate before a tool runs

tools.go, usage.go

tools.go

Tool definitions, JSON schemas, annotations, handlers and the one failure shape

client.go

nameservers.go

The three nameserver tools' handlers: confirm: true, and no target or nameservers from an agent token

client.go

scopes.go

The tool to scope map, and the construction check that every tool declares exactly one scope

server.go

ratelimit.go

The token bucket per caller (5 a second, bursts of 20 by default)

server.go

jsonrpc.go

JSON-RPC 2.0 framing and error codes

transport.go

transport.go

The stdio and streamable-HTTP transports

server.go

client.go

The ControlPlane port: the interface every tool handler calls

implemented by httpclient.go

httpclient.go

HTTP adapter onto the control-plane REST API

api.customdomain.ai

clientinfo.go

Passes the caller's address on, so the control plane's per-address limit sees each user

control plane

origin.go

The Origin check on /mcp (a browser origin must be the server's own or one the operator lists, else 403) and the CORS preflight answer

none

auth.go

Bearer extraction, POST /token, and the RFC 9728 WWW-Authenticate challenge

control plane

agenttoken.go

Local ES256 verification of delegated agent tokens (signature, expiry, audience)

control-plane JWKS

purchaseauth.go

The fail-closed purchase-authorization callback, checked before any paid order

integrator callback

registrar.go

Registrar resolution and preferred-registrar ordering

control plane

untrusted.go

Bounds third-party text (DNS values, provider errors) before it reaches an agent

none

wellknown.go

Serves the protected-resource and authorization-server metadata and /.well-known/agent/mcp.json

none

usage.go

Batched, drop-on-full usage recorder; never blocks a tool call

control-plane ingest

cmd/mcp/main.go

Process entry point: flags, environment, Sentry, OpenTelemetry

all of the above

Entry point: services/mcp/cmd/mcp/main.go.

The flow that matters

Connecting a domain someone already owns is the flow everything else is shaped around.

sequenceDiagram
  participant A as Agent
  participant M as mcp.customdomain.ai
  participant C as Control plane
  participant D as Customer's DNS
  participant E as Edge
  A->>M: tools/call connect-domain {domain}
  M->>C: POST /v1/connections (credential forwarded)
  C->>C: detect provider, choose rail, compute records
  C-->>M: connectionId, records[], link, managed
  M-->>A: same payload as structuredContent
  Note over A,D: automatic rail: user authorizes once<br/>manual rail: user adds the returned records
  C->>D: poll public DNS by value (~60s)
  C->>E: request certificate once records resolve
  A->>M: tools/call check-connection-status {connectionId}
  M-->>A: status live, connected true

Invariants

  • No tool accepts a DNS record as input. Record values are computed server-side from vetted templates. This is the security model, not a gap.

  • The server has no database. Its go.mod pulls Sentry and OpenTelemetry and nothing else; every fact a tool returns was fetched from the control plane during that request. The rate limiter's buckets live in memory.

  • Every tool declares exactly one scope. The server refuses to start if one does not.

  • There is no ownership-challenge step. No tool asks for or checks a TXT verification record.

  • Nothing in this repository is deployed. No build, no CI, no server source: the files here are read by the MCP registry and by people, and the endpoint ships from elsewhere.

  • POST /v1/connections is idempotent per application and domain, so a repeated connect-domain resumes the existing connection and returns already_connected: true rather than creating a second one.

Configuration

What you set, as a client. There are no environment variables to set in this repository.

Setting

Required

Default

Purpose

Endpoint URL

yes

none

https://mcp.customdomain.ai/mcp

Authorization header

unless the client signs in

none

Bearer cd_live_..., a JWT from /token, or CLIENT_ID:CLIENT_SECRET. A client that signs in gets an agent token and sends it itself.

Token URL

only for /token JWTs

none

https://mcp.customdomain.ai/token

Keep the key out of the config file where your client supports it. If it lives in a secrets manager, inject it at launch; never paste a literal key into a repository, a .cursor/mcp.json you commit, or an issue.

How the hosted service is configured. Listed because several of these change what the tools do, and integrators ask why a purchase was denied. Secret values are injected at deploy time and appear in no repository.

Variable

Required

Default

Purpose

MCP_ADDR

no

:8080

HTTP listen address

MCP_API_BASE

no

https://api.customdomain.ai

Control-plane base URL

MCP_PUBLIC_BASE

no

https://mcp.customdomain.ai

Public origin stamped into the discovery documents, and the base of the agent token audience

MCP_AUTH_SERVER

no

https://api.customdomain.ai

Public origin of the control plane that issues agent tokens; named first in the protected-resource metadata

MCP_AGENT_JWKS_URL

no

unset

JWKS for delegated agent tokens. Unset leaves the agent-token path dark. The hosted service sets it.

MCP_ALLOWED_ORIGINS

no

unset

Browser origins (scheme://host[:port], comma separated) allowed to call /mcp besides the server's own. Any other Origin gets 403; a request with no Origin (every non-browser client) is unaffected. No wildcard. Unset on the hosted service.

MCP_TENANT_RPS

no

5

Tool calls per second per caller. 0 turns the limit off.

MCP_TENANT_BURST

no

20

Tool calls per caller in one burst

MCP_LINK_BASE

no

console URL

Prefix for the guided-DNS fallback links returned to agents

MCP_PURCHASE_AUTHZ_URL

for purchases

unset

Authorization callback. Unset denies every paid order, which is how the hosted service runs today.

MCP_PURCHASE_AUTHZ_USER

with the URL

unset

Basic-auth user for that callback

MCP_PURCHASE_AUTHZ_PASSWORD

with the URL

unset

Basic-auth password for that callback (injected, never stored)

MCP_PREFERRED_REGISTRAR

no

unset

Registrar tried first during order resolution

JUSTEASY_MCP_USAGE_KEY

no

unset

Enables usage recording; unset makes the recorder a no-op. CUSTOMDOMAIN_MCP_USAGE_KEY is read first, and MCP_USAGE_INGEST_KEY is a fallback.

MCP_USAGE_INGEST_URL

no

derived from MCP_API_BASE

Where usage batches are posted

CUSTOMDOMAIN_API_TOKEN

stdio only

unset

Static control-plane bearer for the local stdio transport (JUSTEASY_API_TOKEN is the legacy name)

MCP_INSECURE

no

unset

Serves /mcp with no bearer check, for one developer's machine: the server refuses to start unless it listens on a loopback address, and refuses on production hosts.

SENTRY_ENVIRONMENT

no

unset

Environment tag; also what the production guard reads

OTEL_EXPORTER_OTLP_ENDPOINT

no

unset

Opt-in tracing; no-op when unset

Development and testing

There is no build here. The repository holds two JSON manifests and three Markdown documents, so the checks are proportional:

git clone https://github.com/CUSTOM-DOMAIN-APP/customdomain-mcp.git
cd customdomain-mcp
jq -e . server.json glama.json > /dev/null && echo "manifests parse"

server.json must validate against the MCP registry schema it declares (https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json; note its 100 character limit on description), and its version must match the version published to the registry. TOOLS.md is the contract: if a tool argument or a response field changes upstream, the change is not done until it is reflected there.

The service itself is Go 1.26 and carries 196 tests across 26 test files (counted 2026-10-21 at the 0.6.0 commit) covering the protocol, the transport, the control-plane adapter, the purchase gate and its confirmation, scope enforcement, agent token verification, the rate limiter, the Origin check, the nameserver tools and the consistency of the discovery documents. It is exercised with go build ./... && go test ./... in the monorepo, plus a -race pass, before anything reaches the endpoint.

A live check needs nothing at all, because the agent manifest is public:

curl -s https://mcp.customdomain.ai/.well-known/agent/mcp.json | jq '{version, tools: (.tools | length)}'
# -> { "version": "0.6.0", "tools": 20 }

Versioning and releases

One version number describes the server: the version in serverInfo, in /.well-known/agent/mcp.json, in the registry entry, and in server.json here. Every version has a vX.Y.Z tag, a GitHub release and a CHANGELOG.md entry. The current version is 0.6.0.

Two things ship on different tracks, and confusing them is how a version string goes stale.

This repository ships to directories. server.json is the manifest the official MCP registry reads for ai.customdomain/mcp; glama.json is the ownership claim for the Glama directory. Publishing a change means bumping version in server.json, re-publishing the manifest to the registry (a published registry version is immutable), adding the CHANGELOG entry, and tagging. To roll back: revert the commit and re-publish the previous version. Nothing here is on the request path, so a bad commit here cannot take the endpoint down.

The registry knows the namespace ai.customdomain from a DNS TXT record on customdomain.ai (v=MCPv1; k=ed25519; p=...), so publishing takes the Ed25519 private key that matches it, held by the maintainers and never in this repository, and the registry's mcp-publisher CLI:

mcp-publisher login dns --domain customdomain.ai --private-key "$REGISTRY_PRIVATE_KEY_HEX"
mcp-publisher publish server.json

The endpoint ships from the private monorepo as a distroless container, with a /healthz probe built into the binary (-probe, because the image has no shell). Clients always reach the deployed build, a tool added upstream appears with no change on your side, and rollback is a redeploy of the previous image with no client action at all. Pin your integration to tool names and to the connected boolean, not to a version string.

Operational levers, in the order you would reach for them: revoke the key or the agent's grant in the console to cut off a misbehaving agent; lower MCP_TENANT_RPS; unset MCP_PURCHASE_AUTHZ_URL to deny all purchases; watch error reporting for the service and the control plane. Tool-affecting changes are recorded in the docs changelog.

Known limitations

  • 116 of the 178 catalogued DNS providers have no automatic rail. The tools still work (they return the records plus a link), but a human adds them. Call discover-provider first to find out which case you are in. The breakdown is published at Providers.

  • An agent cannot finish a manual connection alone. The design is link-mediated on purpose: the agent returns a link and polls; a human does the money step and, where there is no automatic rail, the DNS step.

  • Purchases are off on the hosted service. create-domain-order is authorization-gated and no purchase authorization callback is configured there, so it answers rejected. Buy in the console.

  • confirm is compared with the domain only for agent tokens. disconnect-domain requires confirm from every caller, but today the control plane checks that it names the connection's domain only for a delegated agent token. With an API key or a client credentials token, check it yourself before you call.

  • Cursor cannot sign in yet. It uses an API key, which carries the whole workspace.

  • Browser-based clients are refused. A request from a web page carries an Origin, and the hosted service accepts only its own (MCP_ALLOWED_ORIGINS is unset there), so such a client gets 403. Clients that run as a process or on a server send no Origin and are not affected.

  • An agent token cannot choose where a domain's nameservers go. migrate-nameservers moves them to the application's DNS host; naming a target or nameservers takes the platform's own credential.

  • No arbitrary DNS writes, ever. If you need to write records you supply, this is the wrong server; use the REST API or your provider's own.

  • The rate limit is per credential for API keys. A workspace that spreads calls over several keys gets a bucket for each; agent tokens share one bucket per workspace. Connections are also metered per UTC calendar month against the plan allowance.

  • The server source is not in this repository. This is the manifest and the contract; the server is hosted.

Security

No credential belongs in this repository, and none is here. The MCP server holds no key of its own: it forwards yours to the control plane, and verifies an agent token without storing it. Prefer a delegated agent token (scoped, revocable, 15 minutes) or an application-scoped JWT over a workspace-wide cd_live_ key for anything an agent touches. Report a vulnerability as described under Support, never in a public issue.

Contributing

Issues and pull requests on the documentation and the manifests are welcome here. Changes to tool behavior happen upstream in the private monorepo: open an issue describing the behavior you need and it will be routed. Keep TOOLS.md and the live tool schemas in agreement; a pull request that changes one without the other will be asked for the other.

Support

License

MIT. CustomDomain™ is a product of EverJust Company.

Related MCP Connectors

Related MCP Servers