Domain MCP server
Registrar Domains is a domain registrar your AI agent can use through the Model Context Protocol. One endpoint, https://registrar-domains.com/mcp, gives 32 tools: search names, read prices, start a registration, then manage DNS records, nameservers, the transfer lock and code, and renewals. Every order answers a payment link for you. The one exception is automatic renewal: an agent can ask to turn it on for a domain (set_auto_renew), and once you confirm it by email, we charge your saved card before expiry. An unlock, a transfer code and a nameserver change also wait for your confirmation by email.
- Connect a client
- Tools and arguments
- Example prompts
- What needs you
- Scopes
- Confirmation by email
- Endpoint details
- Changes
Connect a client
Sign-in (OAuth 2.1) is the simplest: the app opens a page where you sign in with your email and press Allow. With a key from your dashboard, send it as a bearer token instead. Search and prices work with no account at all.
Claude
Claude.ai
Add a custom connector in the app's settings with this URL, then sign in when the app asks:
https://registrar-domains.com/mcp/oauthClaude Code
claude mcp add --transport http registrar-domains https://registrar-domains.com/mcp/oauthOr with a key:
claude mcp add --transport http registrar-domains https://registrar-domains.com/mcp --header "Authorization: Bearer apex_…"Claude Desktop
In claude_desktop_config.json, through the mcp-remote bridge, which opens the sign-in page:
{
"mcpServers": {
"registrar-domains": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://registrar-domains.com/mcp/oauth"
]
}
}
}ChatGPT
Add a connector in the app's settings with this URL, then sign in when the app asks:
https://registrar-domains.com/mcp/oauthCursor
In .cursor/mcp.json:
{
"mcpServers": {
"registrar-domains": {
"url": "https://registrar-domains.com/mcp/oauth"
}
}
}VS Code and others
In .vscode/mcp.json:
{
"servers": {
"registrar-domains": {
"type": "http",
"url": "https://registrar-domains.com/mcp/oauth"
}
}
}Other clients
Any client with Streamable HTTP works: give it the URL https://registrar-domains.com/mcp and the header Authorization: Bearer <key>, or the URL https://registrar-domains.com/mcp/oauth if it can sign in. Many read this shape:
{
"mcpServers": {
"registrar-domains": {
"type": "http",
"url": "https://registrar-domains.com/mcp",
"headers": {
"Authorization": "Bearer apex_…"
}
}
}
}Tools and arguments
The first three are open to anyone. The others act on your account and need a key or a signed-in connection with the right scope. Every answer is JSON.
Every failed tool call has isError: true. A refusal, for a business rule or for access, also carries structuredContent { "error", "requires"?, "scope"?, … }: error says what went wrong in plain words, and hints such as save_card_url say what to do. An argument that does not fit the schema answers a text that starts with MCP error -32602 and names the field. On /mcp, a missing or bad key is a refusal in the tool answer (HTTP 200), because the open tools work without a key. On /mcp/oauth, the sign-in provider answers HTTP 401 to a bad token, before any tool runs. The other errors outside tool answers are in Endpoint details.
check_domain open
Is this exact domain available? Answers available, taken, or unknown. Taken also covers a name we already sold.
domainstring — Full domain, for example tiny.fr
suggest_domains open
Try one idea across the launch TLDs and rank by availability.
ideastring — A word, a short phrase, or a full domain such as quietharbor.frtldsstring[], optional — TLDs to try. Defaults to the launch list.
get_price open
First-year and renewal price in USD before VAT for one TLD, next to our registrar's costs (cost, renewCost). Our price is our registrar's cost plus 30 %, and never less than what covers the payment fees, the VAT and US$1.00 of margin.
tldstring — TLD without the dot, for example fr
register_domain account
Start a registration. Needs an API key or a signed-in connection. Answers an order with a payment link for the human; the domain is registered once payment clears. A retried call answers the same open order. .fr needs a registrant living or established in the EU, Iceland, Liechtenstein, Norway or Switzerland; .eu needs one in the EU, Iceland, Liechtenstein or Norway. Other registrants are refused before payment (see /terms#eligibility).
domainstringyearsinteger, optionalregistrantobject — Legal registrant of the domainaddonsobject, optional — Options for the domain, priced per year: privacy, premiumDns, spamExperts, easyDmarc. Only the ones this deployment offers are kept.
get_order account
Follow an order. The state is one of: awaiting_payment, expired, paid, registering, registered, partly_failed, failed, refunded. The final states: registered, partly_failed, failed, refunded, expired. partly_failed: some names were registered and the others were not; each line's status, and whether its refund is done or pending, is in lines[]. A line with heldUntil set waits for a registrar top-up on our side; after that time, it fails and a refund is started.
orderIdstring — Order id returned by register_domain
list_domains account
The domains of your account, soonest expiry first, with state, expiry, days left and auto-renew.
No arguments.
list_orders account
The orders of your account, newest first, from every key and connection, with the amount paid and the invoice link.
limitinteger, optional — How many orders, newest first. 1 to 200, 50 by default.
get_domain account
One domain: expiry, auto-renew, nameservers, transfer lock, DNSSEC, what the registry allows, and whether it is managed here.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
renew_domain account
Start a renewal for N years. Answers an order with a payment link for the human; the domain is renewed once payment clears. A registrant whose country cannot hold the .fr or .eu name is refused with { error, field }.
domainstring — A domain of your account, as list_domains names it, for example ada.fryearsinteger, optional — Years to add, 1 to 10. The domain must stay at most 10 years ahead of today, so a domain with years left takes fewer. 1 by default.addonsobject, optional — The options to keep with the renewal: privacy, premiumDns, spamExperts, easyDmarc. Absent: the domain keeps its current options. {}: drops them all.
get_basket account
The account's basket, priced live: domains, years, options, line totals and the subtotal.
No arguments.
set_basket account
Replace the basket with lines of { domain, years, addons }. Nothing is charged. It does not cancel an open order.
linesobject[]
checkout_basket account
One order and one payment link for the whole basket, every domain to the same registrant. A failed line is refunded alone. Payment clears the saved basket only when its domain names match the paid order; a basket changed after checkout stays. A retried call, or any key or connection of the account, gets the same open order for the same lines and registrant. A new order closes the older open ones for the same names. .fr needs a registrant living or established in the EU, Iceland, Liechtenstein, Norway or Switzerland; .eu needs one in the EU, Iceland, Liechtenstein or Norway. Other registrants are refused before payment (see /terms#eligibility).
linesobject[], optionalregistrantobject — Legal registrant of every domain in the basket
list_dns_records account
The DNS records of a domain, each with a stable id, and whether our nameservers serve them.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
add_dns_record account
Add a DNS record: A, AAAA, CNAME, MX, TXT, CAA or SRV. TTL 600 to 86400 seconds.
domainstring — A domain of your account, as list_domains names it, for example ada.frnamestring, optional — Host name inside the domain: "" or "@" for the domain itself, "www" for www.<domain>type"A" | "AAAA" | "CNAME" | "MX" | "TXT" | "CAA" | "SRV" | "NS"valuestring — Value: an IP for A/AAAA, a host for CNAME/MX, text for TXT (without quotes), for example v=spf1 include:_spf.example.com ~allttlinteger, optional — Seconds, 600 to 86400priointeger, optional — Priority, for MX and SRV only
update_dns_record account
Replace one DNS record by its id.
domainstring — A domain of your account, as list_domains names it, for example ada.fridstring — The record's id from list_dns_records, for example rec_3f2a9c0d1e4b5a67namestring, optional — Host name inside the domain: "" or "@" for the domain itself, "www" for www.<domain>type"A" | "AAAA" | "CNAME" | "MX" | "TXT" | "CAA" | "SRV" | "NS"valuestring — Value: an IP for A/AAAA, a host for CNAME/MX, text for TXT (without quotes), for example v=spf1 include:_spf.example.com ~allttlinteger, optional — Seconds, 600 to 86400priointeger, optional — Priority, for MX and SRV only
delete_dns_record account
Remove one DNS record by its id.
domainstring — A domain of your account, as list_domains names it, for example ada.fridstring — The record's id from list_dns_records, for example rec_3f2a9c0d1e4b5a67
set_nameservers account
Ask to point the domain to other nameservers (two to thirteen), or back to ours with an empty list. Nothing changes until the owner confirms it from an email. Hosts public DNS does not find come back as warnings.
domainstring — A domain of your account, as list_domains names it, for example ada.frnameserversstring[] — Two to thirteen distinct host names, for example ["ns1.cloudflare.com", "ns2.cloudflare.com"]. An empty list puts the domain back on our nameservers.
use_cloudflare account
Ask to move the domain's DNS to Cloudflare: exactly the two nameservers Cloudflare assigned. Refused while DNSSEC is on. Nothing changes until the owner confirms it from an email.
domainstring — A domain of your account, as list_domains names it, for example ada.frnameserversstring[] — Exactly the two nameservers Cloudflare assigned to the domain, as its dashboard shows them, for example ["ada.ns.cloudflare.com", "bob.ns.cloudflare.com"].
get_cloudflare_status account
Where the move to Cloudflare stands: pending, waiting, live (visible in public DNS), failed with the reason, or replaced, and what public DNS answers now.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
export_zone_file account
The domain's DNS records as a BIND zone file, to import in Cloudflare (DNS → Records → Import and Export) before a move.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
turn_off_dnssec account
Ask to turn DNSSEC off before a move to Cloudflare. Nothing changes until the owner confirms it from an email; public DNS then drops the DS record within a day.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
connect_cloudflare account
Link the owner's Cloudflare account with an API token (Zone: Read, Zone: Edit, DNS: Edit), so the one-click move can create the zone and copy the records. The token is stored encrypted and never shown again.
tokenstring — A Cloudflare API token with Zone: Read, Zone: Edit and DNS: Edit, as Cloudflare shows it once after you create it. It is stored encrypted and never shown again.account_idstring, optional — The Cloudflare account id, when the token sees several accounts (the refusal lists them).rememberboolean, optional — true keeps Cloudflare linked for other domains. false (the default) deletes the stored token once the move is done (active, failed or cancelled).
get_cloudflare_connection account
Whether a Cloudflare account is linked: its id and name, when, and whether the link stays after a move. Never the token.
No arguments.
disconnect_cloudflare account
Delete the stored Cloudflare token. Delete the token in Cloudflare too.
No arguments.
prepare_cloudflare_move account
Create the domain's zone on the linked Cloudflare account (Free plan) and copy every DNS record exactly, then read it back. Shows the diff. Nameservers do not change.
domainstring — A domain of your account, as list_domains names it, for example ada.frtokenstring, optional — Optional: link Cloudflare in the same call. Without it, the linked account is used.account_idstring, optional — The Cloudflare account id, when the token sees several accounts (the refusal lists them).rememberboolean, optional — true keeps Cloudflare linked for other domains; false deletes the stored token once the move is done (active, failed or cancelled). Absent: the stored link's choice, else false.
switch_cloudflare_move account
Point a prepared move's nameservers to Cloudflare, after every check again. Nothing changes until the owner confirms it from an email.
domainstring — A domain of your account, as list_domains names it, for example ada.frmoveIdstring — The move's id, from prepare_cloudflare_move or get_cloudflare_move.
get_cloudflare_move account
Where the one-click move stands: preparing, prepared (with the diff), switching, switched, active, failed or cancelled, and what public DNS shows.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
cancel_cloudflare_move account
Cancel a prepared move. Deletes the zone only when apex created it and it is still pending.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
set_transfer_lock account
Lock the domain against transfers, at once, where the registry allows it. An unlock waits for the owner to confirm it from an email.
domainstring — A domain of your account, as list_domains names it, for example ada.frlockedboolean — true locks the domain against transfers; false unlocks it, which a transfer away needs
set_auto_renew account
Turn auto-renew off at once, or ask to turn it on. On: we charge the saved card 7 days before expiry, then renew. Turning it on waits for the owner to confirm it from an email, and needs a card the owner saved with Stripe through Manage payment method in the dashboard (save_card_url). A registrant whose country cannot hold the .fr or .eu name is refused with { error }.
domainstring — A domain of your account, as list_domains names it, for example ada.frenabledboolean — true: we charge the saved card 7 days before expiry, then renew for one year. false: renewal waits for you (renew_domain).
get_auth_code account
Ask for the transfer code, to move the domain to another registrar. The owner confirms it from an email and sees the code on that page. The agent never gets the code.
domainstring — A domain of your account, as list_domains names it, for example ada.fr
get_domain_activity account
Who changed what on a domain, newest first: DNS, nameservers, lock, auto-renew, transfer code, renewals.
domainstring — A domain of your account, as list_domains names it, for example ada.frlimitinteger, optional — How many events, newest first. 1 to 200, 50 by default.
The same tools have REST routes, described in /openapi.json. Prices per extension: domain extensions.
Example prompts
- “Find me a .com or .fr name for a bakery called Quiet Harbor, and tell me what each costs to keep for three years.”
- “Register quietharbor.fr for one year with my details, then send me the payment link.”
- “Point quietharbor.fr to my Vercel site: add the A record for the root and a CNAME for www.”
- “Set up Google Workspace mail on quietharbor.fr.”
- “Which of my domains expire in the next 60 days? Start the renewals and give me the links.”
- “Move quietharbor.io to Cloudflare: it gave me ada.ns.cloudflare.com and bob.ns.cloudflare.com.”
What needs you
- Payment.
register_domain,renew_domainandcheckout_basketanswer an order with acheckoutUrl. Nothing is registered, renewed or charged until you pay on that page. Follow the order withget_order. The payment page names the key or the app that started the order. - Basket and shared orders. Payment clears the saved basket only when its domain names match the paid order; a basket changed after checkout stays. A retried call, or any key or connection of the account, gets the same open order for the same lines and registrant. A new order closes the older open ones for the same names.
set_basketdoes not cancel an open order. - Eligibility. .fr needs a registrant living or established in the EU, Iceland, Liechtenstein, Norway or Switzerland; .eu needs one in the EU, Iceland, Liechtenstein or Norway. Other registrants are refused before payment. .re, .pm, .tf, .wf and .yt names must be at least 3 characters long before the dot (Afnic Naming Policy, article 2.2). .fr has no such limit. See who can register.
- Moves away. When an agent asks for a nameserver change, an unlock or a transfer code, nothing changes until you confirm it from an email (confirmation by email). In the dashboard you act at once, and a nameserver change or a transfer code shown there sends an email to the account owner. Every change and every request goes into the domain's activity log, with who did it: you on the web, a key by its label and id, or a connected app by its name and client id.
- Renewal. Renew from your dashboard, or from the link in the reminder emails we send 30 and 7 days before expiry. Automatic renewal is off by default. Turn it on per domain in the dashboard, or confirm an agent's request from the email we send you, and we charge your saved card 7 days before expiry, then renew. You can turn it off at any time. An agent with your key or connection can ask to turn it on with
set_auto_renew; revoke the key or app to stop the requests. - Revoking. Revoke a key or disconnect an app in the dashboard under Connect agent; it stops at once. Revoking a key does not cancel the orders it opened: they stay payable until they expire. When you approve an app again, its new connection replaces the old one. In rare cases (another redirect address, two approvals close together) Connect agent shows two rows for one app: disconnect the extra one.
- Limits. 30 tool calls a minute: per IP address on
/mcp, per account on/mcp/oauth. Each call in a batch counts, and a batch holds at most 10 messages.initialize,notifications/initialized,tools/listandpingare not tool calls: they have their own 30 a minute, per IP address on/mcpand per account on/mcp/oauth, so they never use up the tool calls. A batch of them costs one per message. On/mcp/oauth, an address that sends 30 bad tokens in a minute waits for the rest of that minute. A request body is at most 64 KiB. Past the limit, the answer is HTTP 429 withretry-after: 60and a JSON-RPC error. An availability the registry did not confirm is reported asunknown, never as available. A name we already sold, or one being registered after payment, istaken. When our registrar checks are busy, the reason says "The registrar check is busy. Try again in a few minutes."
Scopes
A connected app asks for scopes, and the consent page shows each one with the line below. Read is always granted. An app without the scope a tool needs gets { "error", "requires": "scope", "scope" }: disconnect it under Connect agent, then connect it again and allow that scope. An app connected before the split (scope domains) can only read until you connect it again. An API key has no scopes: it can do everything an app with all three scopes can do.
domains:read
Search and price domains, read your domains, orders, DNS records and activity, and start registrations and renewals that you pay through a link.
domains:write
Change DNS records (which can redirect your web and email traffic), nameservers and transfer locking (locking only), and turn auto-renew on or off. Turning auto-renew on lets us charge your saved card off-session at renewal. Nameserver changes and auto-renew on wait for your email confirmation.
domains:transfer
Unlock domains and ask for transfer codes. Each request waits for your email confirmation, and only you see the code.
get_auth_codeset_transfer_lock— to unlock
No scope
Open to anyone, with no key and no connection.
Confirmation by email
Four actions let someone take a domain away from you. When an agent asks for one, we email the account owner a link, and nothing changes until the owner confirms it.
set_nameservers: every change, including the empty list that puts the domain back on our nameservers.set_transfer_lockwithlocked: false. Locking acts at once.get_auth_code. The agent never gets the code: the owner sees it on the confirmation page, once.set_auto_renewwithenabled: true. Turning it off acts at once. A domain already on answers{ "autoRenew": true }at once.
The link works once, for 30 minutes. It opens /account/confirm, which asks the owner to sign in to the same account. The page shows the request and who asked for it, with two buttons: Confirm and Refuse. Confirm carries out the change, with the rules checked again. Refuse cancels it. The domain's activity log shows the request (confirm.requested), a refusal (confirm.refused), and the change itself once it is carried out, under the name of who asked.
The tool answers at once:
{
"status": "confirmation_sent",
"domain": "quietharbor.fr",
"action": "nameservers",
"expiresAt": "2026-10-21T10:30:00.000Z",
"message": "We emailed the account owner a link. Nothing changes until they confirm it. The link works once, for 30 minutes."
}actionisunlock,authcode,nameserversorautorenew_on.- The same request again, while its link is open, adds
alreadySent: trueand sends no new email. A newer nameserver request for the same domain replaces the open one. - The REST routes answer
202with the same body. An action that acts at once answers200. - An account can make 5 requests an hour. After that the answer is an error (
429on REST), and nothing changes. - If the email cannot be sent, the answer is an error (
503on REST), and nothing changes. - API keys go through the same confirmation. Revoking a key cancels its open requests.
- In the dashboard you are signed in already, so these actions act at once.
Endpoint details
https://registrar-domains.com/mcp: Streamable HTTP, stateless, JSON-RPC over POST. No session header. Open tools need nothing; account tools takeAuthorization: Bearer <key>.https://registrar-domains.com/mcp/oauth: the same tools behind OAuth 2.1 with dynamic client registration, and the scopes above. Discovery at/.well-known/oauth-protected-resource/mcp/oauth.- Server name for configs:
registrar-domains.
Errors outside tool answers, from the transport or the sign-in provider:
- Rate limited: HTTP 429,
retry-after: 60, and a JSON-RPC error with code-32000anddata.retryAfterSeconds. A batch gets an array with one error per request id. - A body that is not JSON: HTTP 400, JSON-RPC
-32700("Parse error"). A message that is JSON but not JSON-RPC also answers-32700, with the message "Parse error: Invalid JSON-RPC message". A batch of more than 10 messages: HTTP 400, JSON-RPC-32600. - A body over 64 KiB: HTTP 413.
- A missing, expired or revoked token on
/mcp/oauth: HTTP 401 from the sign-in provider, withWWW-Authenticate.
A raw call, to see the shape:
curl -s https://registrar-domains.com/mcp \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_price","arguments":{"tld":"fr"}}}'Changes
, prices in US dollars
- Prices from
get_price,suggest_domainsandlist_domains, baskets and new orders are in US dollars before VAT, and theircurrencyisUSD. An order made before the switch stays in EUR. Always readcurrencywith a price or a total.
, server version 0.6.0
- Every refusal now has
isError: true. ItsstructuredContentand its text do not change. An agent that read a refusal as a success now sees a failure. - A rate-limited call answers HTTP 429 with a JSON-RPC error body (code
-32000), not{ "error": "rate limited" }. The status andretry-after: 60do not change. initialize,tools/listandpinghave their own limit (per IP address on/mcp, per account on/mcp/oauth), so they never use up the tool calls. Each call in a batch counts as one tool call.- On
/mcp/oauth, the limit of tool calls is per account, not per IP address. renewPriceonlist_domainsandGET /api/domainsis now today's renewal price for one year with the domain's options, before VAT: what the auto-renew charge takes. It was the price quoted at the first purchase. It isnullwhen the price list does not answer.- The server sends
instructionsatinitialize, and every tool has annotations.renewinget_priceis the old name ofrenewCost: it stays for now, and it will go in a later version.
More: all docs, /llms-full.txt for assistants.