Dev.to · 11 min read

Ahrefs MCP Server: Setup for Claude, Codex and the Rest

Ahrefs MCP Server: Setup for Claude, Codex and the Rest

The first thing that will happen when you connect Ahrefs to an AI client is that nothing happens. No error, no tools, just a server that sits there looking connected. In my case the cause was mundane: Ahrefs ships two different MCP servers and two different kinds of API key, and only one of the four possible pairings is the one you want today. Nobody tells you which one you picked. That is the short version of why this guide exists. The longer version is that I spent a subscription cycle running about 1,100 logged calls through this thing, and most of what cost me time was not the SEO analysis. It was the plumbing. What You Are Actually Connecting To Ahrefs runs a hosted MCP server at https://api.ahrefs.com/mcp/mcp. It speaks Streamable HTTP, which is the current transport in the Model Context Protocol spec and the one every serious client supports. SSE is deprecated and you should not build anything new on it. Behind that endpoint sits most of what you would otherwise click through in the Ahrefs web app: Site Explorer for backlinks and organic keywords, Keywords Explorer for volume and difficulty, Rank Tracker, Site Audit, and the Google Search Console integration if you have connected an account. In my instance that came out to 130 callable tools, which is more than the marketing pages claim, because the server has been growing. Two things about it are worth knowing before you wire anything up. Access starts at the Lite plan, so a free trial account will not get you in. And every billable call draws from the same monthly API unit budget as regular API v3 usage, which means your chat assistant and your cron jobs are eating from one plate. Billing splits three ways: a good number of endpoints cost nothing, some charge a flat rate per request, and the rest charge per row. The last section is about telling them apart. Two Servers, Two Key Types, One Silent Failure This is the part that wasted my first evening. There is an older local server, published as @ahrefs/mcp on npm and hosted at ahrefs/ahrefs-mcp-server on GitHub. That repository is now archived, and its README carries a sentence worth reading twice: it works with API v3 keys only, and it does not work with MCP keys. The hosted remote server is the opposite. It wants a key with MCP scope, which you generate separately in your Ahrefs account. Ahrefs states plainly that API keys and MCP keys are not interchangeable. So the matrix looks like this. Two cells work, but only one of them is a sensible choice today: API v3 key MCP-scoped key Local @ahrefs/mcp worked, repo now archived fails Remote /mcp/mcp fails correct The archived combination is not broken so much as abandoned. It still runs if you already have it, but it receives no maintenance and Ahrefs points at the remote server instead. The failure modes are quiet. An MCP-scoped key pointed at the REST API returns Unauthorized, which at least tells you something. A client that cannot complete the handshake often just shows the server with zero tools, and you go looking for a config typo that is not there. If you are setting up today, use the remote server and generate an MCP-scoped key. Ignore every tutorial that has you npm-installing anything. Authentication: OAuth Is the Official Path, Bearer Is the Useful One Ahrefs documents OAuth as the way in. Your client opens a browser window, you sign in, it caches the credentials. For interactive work that is fine and it is genuinely the least fiddly option. It gets awkward the moment you want a scheduled job to pull data at seven on a Sunday morning. OAuth needs a human at a browser for the initial authorisation, and after that you are maintaining a token refresh that has to keep working unattended. So for anything headless I authenticate with a bearer token instead, and pass the MCP key directly in the Authorization header. Same endpoint, same tools, no browser, nothing to refresh. The practical rule I settled on: bearer for anything that has to survive without me, OAuth for a laptop I am sitting in front of. If you only ever use this in chat, take the OAuth prompt and skip the next few sections. Claude Code One command, and the scope flag decides whether the server lives in this project or in your user config. claude mcp add --transport http ahrefs https://api.ahrefs.com/mcp/mcp \ --header "Authorization: Bearer $AHREFS_API_KEY" -s project Project scope writes into a .mcp.json next to your code, which is the right choice when the key belongs to one client or one site. Put that file in .gitignore before you paste a key into it, because the header sits there in plain text. Two things that cost me time here. The tools appear as mcp__ahrefs__, not as ahrefs., which matters when you are writing prompts that name a tool explicitly. And a running session loads its MCP servers at startup only, so the connection you just added shows up in the next session, not this one. I restarted three times convinced the config was wrong. Claude Desktop No config file needed. Settings, then Connectors, then Add custom connector, then paste the endpoint URL. OAuth client ID and secret go under Advanced settings if your server needs them. There is one architectural detail here that surprises people and that changes what you can connect. Claude Desktop does not reach your MCP server from your machine. It reaches it from Anthropic's cloud infrastructure. For a hosted service like Ahrefs that makes no difference at all. For a server running on your own laptop or behind a company VPN it makes all the difference, because that server has to be reachable from the public internet before it will ever work. Free accounts are capped at one custom connector. Paid tiers are not. Codex Codex reads ~/.codex/config.toml globally, or a .codex/config.toml in a project directory you have marked as trusted. One TOML table per server, and the transport is inferred from which keys you set: a command key means stdio, a url key means Streamable HTTP. [mcp_servers.ahrefs] url = "https://api.ahrefs.com/mcp/mcp" bearer_token_env_var = "AHREFS_API_KEY" Note what bearer_token_env_var takes. It is the name of an environment variable, not the token itself. Writing your key there directly gives you a config file full of secret and a server full of nothing. The codex mcp add subcommand exists, but it is shaped around stdio servers, so for a remote endpoint editing the TOML is both faster and easier to put in version control. Verify with codex mcp list. Cursor, VS Code and Windsurf All three speak the same JSON dialect with one annoying difference: the key that holds the URL. Cursor calls it url. VS Code wants url alongside an explicit "type": "http", the same shape Claude Code uses. Windsurf calls it serverUrl. Everything else about the block copies across unchanged, which means a config that works in one editor is thirty seconds of renaming away from working in the next. VS Code has had native MCP support since 1.99, surfaced through Copilot Chat. Windsurf added it early this year. If your team is split across editors, write the block once and keep the three variants in a snippet somewhere, because you will need them again. Headless, Without Any Client at All MCP is a session protocol, not a plain REST call. You initialize, you send an initialized notification, and only then can you call a tool. Every request after the handshake carries the session id you got back. curl -sD hdr -X POST "$AHREFS_MCP_URL" \ -H "Authorization: Bearer $AHREFS_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-06-18","capabilities":{}, "clientInfo":{"name":"sm","version":"1"}}}' SID=$(grep -i '^mcp-session-id:' hdr | awk '{print $2}' | tr -d '\r') curl -s -X POST "$AHREFS_MCP_URL" \ -H "Authorization: Bearer $AHREFS_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "mcp-session-id: $SID" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' Three things in there are easy to get wrong. Do not skip the second call: the session id alone does not finish the handshake, and a server that never received the initialized notification will refuse tool calls. The Accept header needs both content types even if you never intend to read a stream. And from the 2025-06-18 revision onward, every request after initialization must carry the MCP-Protocol-Version header, so it belongs on the notification and on every tool call that follows, alongside mcp-session-id. Wrapping this in a small shell helper is worth the twenty minutes, because it makes the same data available to cron jobs and agents that have no chat interface at all. One warning from experience: if you build that helper with a bash default like ${ARG:-{}} for the JSON argument, brace matching inside the default value will silently append a stray closing brace and produce malformed JSON. The call then fails with no output and no error. Default the variable on a separate line. Your Plan Decides Your Row Cap, and That Is the Expensive Part This is the section I wish I had read first, because it explains a mistake that cost me a full month of budget in a single morning. Ahrefs gates two things by subscription tier: how many units you get per month, and how many rows a single request may return. Plan Units per month Max rows per request Lite 100,000 100 Standard 400,000 250 Advanced 1,000,000 500 Enterprise 2,000,000 unlimited Those numbers changed on 28 April 2026, and they changed a lot. Lite went from 25,000 units to 100,000 and from 10 rows to 100. Standard went from 150,000 to 400,000 units and from 25 rows to 250. Advanced doubled its units and went from 100 rows to 500. Now hold that next to how the pricing works. A call costs a minimum of 50 units, and beyond that you pay per row, multiplied by how many columns you asked for. Premium columns like volume, keyword_difficulty and traffic_domain add roughly ten units per row each. Put those two facts together and you get the trap. The most expensive call in my entire log ran with a limit of 250 rows. That is not a number I chose after thinking about it. It is exactly the row cap of the plan I was on, and I reached for it because it was the maximum available. Each of those calls cost 5,250 units. The same query at 50 rows would have cost 1,050 and told me the same thing, because rows 51 through 250 were long-tail noise I never used. The row cap is not a recommendation. It is a ceiling, and since April it is a ceiling that sits up to ten times higher than it used to. That change is harmless if your code passes an explicit limit, because 50 still means 50. It bites in three specific cases: when you pass no limit at all, when your code asks for whatever the maximum currently is, and when a query used to be clipped by the old cap and now returns the full ten times more rows. All three are common in scripts written before spring, and none of them look different in the code. Set the limit from what you will actually read. For keyword expansion I now start at 50 and go higher only when a result visibly clipped at the boundary and the extra rows matter. Ten Traps That Each Cost Me a Run None of these are in the documentation in a way you would find before you hit them. All of them produced either an error I had to decode or, worse, an empty result that looked like a finding. where is JSON, never a string expression. Writing "position>3 and position

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News