MCP server
Ten tools over Streamable HTTP, with a bearer key. The connection block for every client, then a reference for every tool.
Plain markdown: /docs/mcp.md
Domduck's MCP server is one of the three surfaces the product ships: a dashboard for people, an MCP server for agents, and an embeddable badge for other people's pages.
- Endpoint
POST https://domduck.com/api/mcp - Transport Streamable HTTP. Every answer is the response to a POST; this server never opens a stream.
- Authentication
Authorization: Bearer ddk_.... Create a key at https://domduck.com/settings/api-keys. - Rate limit 240 requests a minute, per key.
- Scope Every tool answers for the account that owns the key, and only for domains that account already tracks.
There is also a public server with three read-only tools and no key at all.
Connect
Claude Code
claude mcp add --transport http domduck https://domduck.com/api/mcp \
--header "Authorization: Bearer ddk_YOUR_KEY"
Cursor, VS Code, and any client with an mcp.json
{
"mcpServers": {
"domduck": {
"type": "http",
"url": "https://domduck.com/api/mcp",
"headers": { "Authorization": "Bearer ddk_YOUR_KEY" }
}
}
}
Claude Messages API
{
"mcp_servers": [
{
"type": "url",
"url": "https://domduck.com/api/mcp",
"name": "domduck",
"authorization_token": "ddk_YOUR_KEY"
}
],
"tools": [{ "type": "mcp_toolset", "mcp_server_name": "domduck" }]
}
Send the beta header anthropic-beta: mcp-client-2025-11-20 with that request.
Raw JSON-RPC
curl -s https://domduck.com/api/mcp \
-H "Authorization: Bearer ddk_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Protocol
This server answers both protocol eras on one endpoint.
| Era | Revisions | Shape |
|---|---|---|
| Modern | 2026-07-28 | Stateless. Every request declares its version in params._meta["io.modelcontextprotocol/protocolVersion"] and carries the matching MCP-Protocol-Version, Mcp-Method and Mcp-Name headers. |
| Legacy | 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 | The initialize handshake. |
Which era a request is in is decided by the body, not the header. A request with no _meta version is legacy even when it sends MCP-Protocol-Version, because that header existed in 2025-06-18 too.
server/discover is implemented and answers on both paths, with or without _meta, so a probe does not have to know the era first.
Modern requests get the status codes that revision requires: 400 with -32022 for a version this server does not implement, 400 with -32020 when a header disagrees with the body, and 404 with -32601 for an unknown method. Legacy requests answer 200 throughout.
Errors
A tool that fails returns a normal result with isError: true and a plain sentence, not a JSON-RPC error. That is deliberate: a model can fix its own arguments and retry, and it cannot fix a protocol error. Read the message.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "stripe.com is not tracked by this account. Use add_domain first, or list_domains to see what is."
}
],
"isError": true
}
}
A domain this account does not track is always reported as untracked, never as non-existent. The distinction would turn this endpoint into a way to ask what other people are watching.
Tools
| Tool | Writes | What it does |
|---|---|---|
[list_domains](#list-domains) | no | Every domain this account tracks, with its latest Domain Rating, Tranco rank, tags and observation count. |
[get_domain](#get-domain) | no | The latest reading from every source for one tracked domain: Domain Rating, Tranco rank, registration date, TLS certificate, DNS, AI crawler policy. |
[get_history](#get-history) | no | One reading per day for a domain and source, oldest first. |
[get_ai_crawler_policy](#get-ai-crawler-policy) | no | Which of the eight tracked AI crawlers a domain's robots.txt admits, and whether it publishes an llms.txt. |
[add_domain](#add-domain) | yes | Add a domain to this account and immediately read every cheap source once, so it has values rather than dashes. |
[remove_domain](#remove-domain) | yes | Take a domain off this account. |
[check_domain](#check-domain) | yes | Re-read every cheap source for one tracked domain right now, instead of waiting for the daily sweep. |
[set_domain_flags](#set-domain-flags) | yes | Change how a tracked domain is filed. |
[list_tags](#list-tags) | no | This account's tags with the number of domains carrying each. |
[list_sources](#list-sources) | no | What Domduck reads and what each source means, including the eight AI crawlers whose robots.txt policy is tracked. |
A worked call
curl -s https://domduck.com/api/mcp \
-H "Authorization: Bearer ddk_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"get_history",
"arguments":{"domain":"stripe.com","source":"ahrefs_dr","days":30}}}'
The result's content[0].text is JSON as a string, which is how MCP carries a structured answer:
{
"domain": "stripe.com",
"source": "ahrefs_dr",
"days": 30,
"higherIsBetter": true,
"points": [
{ "day": "2026-07-05", "value": 95 },
{ "day": "2026-07-06", "value": null },
{ "day": "2026-07-07", "value": 95 }
]
}
**null is a day with no reading, and it is not a zero.** Nobody looked that day. A zero is a value a domain can hold, and on a rank series where lower is better, reading a gap as zero turns every gap into the best result ever recorded. Draw a gap, not a point.
list_domains
Every domain this account tracks, with its latest Domain Rating, Tranco rank, tags and observation count. The place to start: most other tools take a domain from this list.
| Argument | Type | Required | Notes |
|---|---|---|---|
tag | string | no | Only domains carrying this tag. |
starred | boolean | no | Only starred domains. |
sort | name, dr, rank, observations, added | no | Sort key. Defaults to name. |
dir | asc, desc | no |
get_domain
The latest reading from every source for one tracked domain: Domain Rating, Tranco rank, registration date, TLS certificate, DNS, AI crawler policy. Includes how many days of history exist per source and any recent source failures.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
get_history
One reading per day for a domain and source, oldest first. This is what the product is for: the series cannot be obtained anywhere else and cannot be backfilled. A null value is a day with no reading, which is a real state and not a zero.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
source | ahrefs_dr, tranco, openpagerank, cloudflare_radar, robots_txt | yes | Which series. ahrefs_dr is Domain Rating 0-100, tranco is popularity rank where lower is better, robots_txt is the count of blocked AI crawlers. |
days | integer | no | Window length. Defaults to 90. |
get_ai_crawler_policy
Which of the eight tracked AI crawlers a domain's robots.txt admits, and whether it publishes an llms.txt. Three states per agent: allowed, disallowed, unspecified. Unspecified is NOT allowed, because a catch-all Disallow blocks an agent the file never names, so read effective for what is actually in force. Reads the last stored reading; it does not fetch the file.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
add_domain
Add a domain to this account and immediately read every cheap source once, so it has values rather than dashes. Unlimited domains, no tier cap. Re-adding one already tracked is a no-op that still refreshes it.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
tags | array of string | no | Optional labels. Tags, not folders, so a domain can carry several. |
remove_domain
Take a domain off this account. Its observation history is NOT deleted: it keeps accumulating and comes back intact if the domain is added again. Only the list changes.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
check_domain
Re-read every cheap source for one tracked domain right now, instead of waiting for the daily sweep. Use it after a change that should move a number, not as a polling loop: the metrics move over weeks.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
set_domain_flags
Change how a tracked domain is filed. Pausing keeps the domain and its history but drops it out of the daily sweep. Omitted fields are left alone.
| Argument | Type | Required | Notes |
|---|---|---|---|
domain | string | yes | A domain name. Scheme, www and trailing dots are stripped. |
starred | boolean | no | |
paused | boolean | no | Paused domains keep their history but stop being re-read. |
addTags | array of string | no | |
removeTags | array of string | no |
list_tags
This account's tags with the number of domains carrying each. Tags are per-user and a domain can hold several. Takes no arguments.
list_sources
What Domduck reads and what each source means, including the eight AI crawlers whose robots.txt policy is tracked. Useful for interpreting get_domain output without guessing at field names. Takes no arguments.