AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
맞춤 도메인용 호스팅 MCP 서버. Claude·Cursor·ChatGPT 에이전트가 검색·구매·연결을 지원하고 인증·TLS를 처리합니다
A hosted MCP server for custom domains. Claude, Cursor and ChatGPT agents can search, buy and connect domains with verification and TLS handled for you.
개발 도구GitHub @CUSTOM-DOMAIN-APP온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기CustomDomain™ MCP server: AI agents search, buy, connect and verify custom domains end to end, with DNS records, ownership and TLS handled for them.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗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
Website · Docs · Console · Product page · MCP docs · Tool reference · Provider coverage · Pricing
Install: · 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"]
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.
live instead of stopping at the last mile with a paragraph of DNS instructions.Twenty tools, one credential, no DNS records in any argument. Each of these is a tool call, not a workflow you have to assemble:
search-domain-availabilitygenerate-domain-suggestionscreate-domain-orderconnect-domaindiscover-providercheck-connection-statusdiagnose-connectionrecheck-connectioncheck-certificate-statuscheck-order-statuslist-domain-ordersadd-emailforward-domainlive, using a stored grant: reapply-connectiondisconnect-domainlist-connectionsbilling_required
refusal: check-billing-statusmigrate-nameserversFull 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.
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.discover-provider first, sees the customer
runs a provider that cannot hold a root-domain alias, and warns before anyone authorizes anything.add-email writes MX, SPF, DKIM and DMARC from a template; the agent supplies the
per-domain DKIM selector and nothing else.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.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.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.
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.
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.
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 }
]
}
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:
cd_live_...), which acts for the whole workspace, orAPPLICATION_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 | 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 |
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.
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.
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.
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.
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.
.
├── 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
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) |
Entry point: services/mcp/cmd/mcp/main.go.
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
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.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.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) |
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 }
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.
discover-provider first to find out which case you are
in. The breakdown is published at Providers.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.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.migrate-nameservers moves them to the
application's DNS host; naming a target or nameservers takes the platform's own credential.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.
customdomain-js and @customdomain/reactIssues 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.
MIT. CustomDomain™ is a product of EverJust Company.
check-nameserver-statusrollback-nameservers| 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 |
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 |