Via MCP
Give an agent the solver as a tool it can reach for mid-task, rather than writing the challenge handling in ahead of time.
Every other page here is written the other way round: you pick a target, and the handling for that one site gets written before anything runs. That's the right shape for a scraper and the wrong shape for an agent, which doesn't know which page will block it until the 403 comes back.
claude mcp add --transport http xhrdev https://mcp.xhr.dev/mcpAnything that reads the standard mcpServers block — Claude Desktop, Cursor, Windsurf, Zed:
{
"mcpServers": {
"xhrdev": {
"type": "http",
"url": "https://mcp.xhr.dev/mcp"
}
}
}There's nothing to clone, install, or keep running. That URL is a hosted server we operate.
This is not the same as Via Claude
That page is about getting an agent to write your integration — a prompt and a skill, so the code it produces is right. This page is about an agent using the solver at runtime, on pages it hits while working. They compose: use the skill to build your scraper, use MCP for the agent's own browsing.
Whether it can reach your solver
One question decides whether this is any use to you: is your solver reachable from the public internet?
The server runs in our AWS account. When it calls a solver, the call leaves from there. A container on a private subnet — which is the posture we recommend — is not reachable from AWS, and no configuration changes that.
| Your deployment | What to use |
|---|---|
| Evaluating, no container yet | This, with no headers. It uses our trial box. |
| Container published on the internet, behind TLS and a key | This, with x-solver-host and x-api-key. |
| Container on a private network | Not this. The HTTP integration — your agent's code calls your own solver directly, and nothing has to cross a boundary. |
That last row is the common one for customers, and it isn't a downgrade: an agent that can run code can call POST /dd/solve on your own host in the same four requests every other page here describes. What MCP buys is not capability but reach — tools an agent can discover and call without you having written an integration first, which is exactly what's useful while evaluating and exactly what a locked-down deployment doesn't need.
Pointing the hosted server at your own solver
With no headers you get our trial box on a shared key, rate limited — enough to try the tools, not to run on. Two optional headers change that, and each is independent:
claude mcp add --transport http xhrdev https://mcp.xhr.dev/mcp \
--header "x-solver-host: https://solver.example.com" \
--header "x-api-key: <your key>"| Header | What it does |
|---|---|
x-solver-host | Your deployment instead of the trial box. Same two forms the examples take: 10.0.0.5 means http://10.0.0.5:3000, a full URL is used as given. |
x-api-key | Sent on to the solver as x-api-key. Supplying it also lifts the rate limit here. authorization: Bearer <key> works too, for clients that only offer a token field. |
x-solver-host has to name something routable from the internet — so give it https, not http://…:3000, since your key and the clearance material cross the public network on that hop. A container exposed that way needs the reverse proxy and x-api-key gate described in Deployment; the container itself has no authentication of its own.
If that sounds like more exposure than you want, that's the right instinct — take the HTTP integration instead.
There is no proxy header, on purpose
A DataDome solve makes no outbound request — that is why iframeData is required, and why a proxy in the solve body is accepted and ignored. The one request this server makes on your behalf is fetching the challenge document, and that is not what DataDome binds a cookie to; your submission is, and you send that yourself.
So the proxy that matters is yours, for your own three requests: the blocked one, the submission, and the retry. Pin that session and send all three through it.
Call solver_info if you want the server to tell you which mode it decided you were in. It's the quickest way to find a mistyped header name.
The tools
| Tool | What it does |
|---|---|
health_check | GET /hc — is the solver reachable |
solver_stats | your own solve counts and success rate |
akamai_queue_metrics | queue depth, for backpressure |
datadome_solve | solve a DataDome captcha or interstitial |
solver_info | which solver, which mode, which tools |
datadome_solve takes the HTML body of the 403 you just received, parses the challenge out of it, fetches the challenge document, and returns a prepared submission — {url, body?, origin, referer}. Your agent sends that itself, from the address it will browse from, and gets the clearance cookie back. That's rule 1 doing its job: a submission made by anything else earns a cookie that's void where you need it.
The browser identity is filled in for you from the profile the examples repo shares, so an agent calls these with a URL rather than having to invent a coherent profile and js_profile — which it can't, since those fields are cross-checked against each other and against the headers actually sent. That's rule 2, handled.
There is no akamai_solve on the hosted server
The local one has it. This is not a port that hasn't happened yet.
An Akamai _abck solve has to be submitted by a real browser — the solver computes the sensor payloads and Chrome relays each one, so the requests carry a genuine TLS fingerprint and cookie jar. Solved server-side against the targets that matter, the payload is built correctly and then simply never accepted, ending as a timeout with nothing naming the cause. And what comes back is bound to the address that earned it: cookies solved in our AWS account are void the moment your agent retries from its own.
A hosted server fails both halves at once, so it doesn't pretend to offer it. Akamai has to be solved from the machine that's doing the browsing: drive the browser bridge directly, with comcast.ts as the runnable example.
Using it
Give the agent a target and tell it what to do when the target says no:
Fetch
https://www.grainger.com/through the proxy in.env. If the response is a403whose body containsvar dd =, pass that whole body to the xhrdev MCP server'sdatadome_solve, send the prepared submission it returns — same proxy — and retry with thedatadomecookie you get back.
That's the whole shape. The agent makes its request, and only if it's actually blocked does it reach for a tool:
1. GET https://www.grainger.com/ (through the proxy)
-> HTTP 403, body carries `var dd = {...}`
2. datadome_solve { url, blockedHtml }
-> { url, body, origin, referer } a request for you to make
3. POST that submission, same proxy
-> Set-Cookie: datadome=...
4. GET the same URL again, same proxy, sending that cookie
-> HTTP 200That exact flow is a runnable script — npm run mcp:grainger in the examples repo, which is grainger-undici.ts with its middle two requests replaced by the one tool call. Diffing the two is the shortest description of what the server does for you.
Every step has to leave from the same address. Whatever your agent makes requests with has to honour the proxy — worth stating in the prompt, as above, rather than hoping. An agent that solves through the proxy and then retries with a built-in web-fetch tool from somewhere else gets a fresh 403 that looks exactly like a failed solve.
Raise your client's request timeout
A tool call here is cut off at 55 seconds, which is comfortable for DataDome — a solve is answered in a second or two. Raising your client's default matters for the browser bridge instead, where an Akamai solve launches a real Chrome and takes 20–60s.
Talking to it with curl
Useful when a client is misbehaving and you want to see the wire. The Accept header is not optional — the MCP spec requires both types, and the server answers 406 without them:
curl -s https://mcp.xhr.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'GET /hc answers {"status":"ok"} with no headers and no key, if you want to point an uptime monitor at it.
Next
- Via Claude — a prompt and a skill for agent-written integrations
- How to integrate — the non-agent paths, and the two rules
- DataDome API reference — what
datadome_solvecalls underneath - Deployment — what an internet-facing container needs in front of it