详细记录 Ahrefs MCP Server 的接入避坑经验,包括 Streamable HTTP 传输配置、90 个工具函数和 Schema 自省功能。
将 Ahrefs 连接到 AI 客户端后,你注意到的第一件事就是——什么也没发生。没有报错,没有工具列表,只有一个看起来已经连接上的服务器杵在那儿。就我而言原因很蠢:Ahrefs 提供了两个不同的 MCP Server 和两种不同的 API Key,而四种可能的组合中,只有一种是当前正确的选择。没人告诉你你选的是哪一个。
这就是本指南存在的简短版本。详细版本是:我花了一个订阅周期跑了大约 1100 次带日志的调用,其中大多数耗费我时间的并不是 SEO 分析本身,而是那些管道工程。
Ahrefs 在 https://api.ahrefs.com/mcp/mcp 运行一个托管型 MCP Server。它使用 Streamable HTTP 传输——这是 Model Context Protocol 规范中当前的传输方式,也是每个正经客户端都支持的方式。SSE 已弃用,不应再基于它构建任何新东西。
这个端点背后坐镇着你原本需要在 Ahrefs 网页应用中点击操作的大部分功能:Site Explorer(外链和自然搜索关键词)、Keywords Explorer(搜索量和难度)、Rank Tracker、Site Audit,以及你已关联账号情况下的 Google Search Console 集成。就我的实例而言,共有 130 个可调用工具,比营销页面声称的数量要多,因为这个服务器一直在增长。
在接线之前有两件事值得了解。访问权限从 Lite 套餐开始,所以免费试用账号是进不去的。每个可计费调用都从你常规 API v3 使用的同一月度 API 配额中扣费,这意味着你的聊天助手和定时任务共用同一个盘子。计费分三类:相当多的端点不收费,有些按请求收取固定费用,其余按行数计费。最后一节会讨论如何区分它们。
这就是浪费我第一个晚上的部分。
有一个较老的本地服务器,发布在 npm 上的 @ahrefs/mcp,托管在 GitHub 的 ahrefs/ahrefs-mcp-server 仓库。该仓库现已归档,其 README 有一句话值得读两遍:它仅与 API v3 Key 配合使用,不支持 MCP Key。
托管型远程服务器则相反。它需要一个具有 MCP 作用域的 Key,你在 Ahrefs 账号中单独生成。Ahrefs 明确声明 API Key 和 MCP Key 不可互换。
所以这个矩阵是这样的。四个单元格中有两个是有效的,但其中只有一个是今天合理的选择:
已归档的组合与其说是坏了,不如说是被抛弃了。如果你已经在用它,它仍然能运行,但没有得到任何维护,Ahrefs 将你指向远程服务器。
失败方式是安静的。具有 MCP 作用域的 Key 被发送到 REST API 会返回 Unauthorized,至少这还能告诉你出了什么错。而一个无法完成握手的客户端通常只是显示服务器有零个工具,然后你去查找一个根本不存在的配置拼写错误。
如果你现在要配置,请使用远程服务器并生成一个具有 MCP 作用域的 Key。忽略所有让你 npm install 任何东西的教程。
Ahrefs 文档将 OAuth 描述为入口。你的客户端打开一个浏览器窗口,你登录,它缓存凭证。对于交互式工作这样没问题,而且确实是最不麻烦的选择。
但当你希望一个定时任务在周日早上七点拉取数据时,这就变得尴尬了。OAuth 需要一个人在浏览器前完成初始授权,之后你还要维护一个必须在无人值守情况下持续工作的 Token 刷新机制。因此对于任何无头场景,我改用 bearer token 认证,直接在 Authorization header 中传递 MCP Key。同一个端点,同样的工具,不需要浏览器,也没有什么要刷新的。
我最终确定的实用规则:任何必须在我不介入情况下存活的使用场景用 bearer,在我坐在面前的笔记本上使用时用 OAuth。如果你只在聊天中使用,就用 OAuth 提示,后面的几节可以跳过。
一条命令,scope 标志决定服务器是放在这个项目里还是你的用户配置中。
claude mcp add --transport http ahrefs https://api.ahrefs.com/mcp/mcp \
--header "Authorization: Bearer $AHREFS_API_KEY" -s project
Project scope 会写入代码旁边的 .mcp.json,当 Key 属于一个客户端或一个站点时这是正确的选择。把那个文件放进 .gitignore 再往里粘贴 Key,因为 header 以明文形式躺在那里。
这里有两件事耗费了我时间。工具显示为 mcp__ahrefs__<toolname>,而不是 ahrefs.<toolname>,这在你编写明确命名工具的提示词时很重要。还有,运行中的会话只在启动时加载其 MCP 服务器,所以你刚添加的连接会在下一个会话中出现,而不是当前这个。我重启了三次,以为配置有问题。
不需要配置文件。进入 Settings,然后 Connectors,然后 Add custom connector,然后粘贴端点 URL。如果你的服务器需要,OAuth 客户端 ID 和密钥放在 Advanced settings 下。
这里有一个让很多人意外的架构细节,会改变你能连接什么。Claude Desktop 不是从你的机器上访问你的 MCP Server。它是从 Anthropic 的云基础设施访问它。对于像 Ahrefs 这样的托管服务这没有任何区别。但对于运行在你自己的笔记本上或公司 VPN 后的服务器,区别就大了,因为那个服务器必须能被公网访问才能正常工作。
免费账号限制为一个 custom connector。付费档次没有限制。
Codex 读取全局的 ~/.codex/config.toml,或者你标记为可信的项目目录中的 .codex/config.toml。每个服务器一个 TOML 表,传输方式从你设置的 Key 类型推断出来:command key 表示 stdio,url key 表示 Streamable HTTP。
[mcp_servers.ahrefs]
url = "https://api.ahrefs.com/mcp/mcp"
bearer_token_env_var = "AHREFS_API_KEY"
注意 bearer_token_env_var 接受的是什么。它是一个环境变量的名字,而不是 token 本身。把你的 Key 直接写在那里只会给你一个满是密钥的配置文件,和一个什么都调不了的服务器。
codex 的 mcp add 子命令存在,但它是为 stdio 服务器设计的,所以对于远程端点直接编辑 TOML 更快,也更容易放入版本控制。用 codex mcp list 验证。
这三个编辑器使用相同的 JSON 方言,只有一个烦人的区别:承载 URL 的 Key 名称。Cursor 叫它 url。VS Code 需要 url 加上显式的 "type": "http",Claude Code 使用的也是这个形状。Windsurf 叫它 serverUrl。除此之外这个块的所有内容完全一致,这意味着在一个编辑器中可用的配置只需三十秒重命名就能在下一个编辑器中工作。
VS Code 从 1.99 版本开始支持原生 MCP,通过 Copilot Chat 暴露。Windsurf 今年初添加了它。如果你的团队横跨多个编辑器,就写一次这个块,然后在某个地方存好三个变体,因为你会再次需要它们。
MCP 是一个会话协议,不是普通的 REST 调用。你初始化,发送一个 initialized 通知,然后才能调用工具。握手之后的每个请求都携带你返回的会话 id。
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"}'
这里面有三件事容易出错。不要跳过第二个调用:单独的会话 id 并不完成握手,一个从未收到 initialized 通知的服务器会拒绝工具调用。Accept header 需要两种内容类型,即使你从不打算读取流。还有,从 2025-06-18 版本开始,初始化后的每个请求必须携带 MCP-Protocol-Version header,所以它应该出现在通知上,也应该出现在后续的每个工具调用上,与 mcp-session-id 并列。
把它包在一个小 shell 辅助脚本里值得花这二十分钟,因为它让同样的数据对 cron 作业和完全没有聊天界面的智能体也可用。一个来自经验教训的警告:如果你用 bash 默认值(比如 ${ARG:-{}})来构建 JSON 参数的默认值,花括号匹配会在默认值内部静默追加一个多余的右花括号,产生格式错误的 JSON。调用会失败,既没有输出也没有错误。把默认值放在单独一行上。
你的计划决定行数上限,而那才是贵的部分
这是我很希望自己首先读到的一节,因为它解释了一个错误——让我在一个上午就花掉整整一个月的预算。
Ahrefs 按订阅等级限制两件事:每月获得多少单位,以及单次请求可以返回多少行。
这些数字在 2026 年 4 月 28 日发生了变化,而且变化很大。Lite 从 25,000 单位增加到 100,000,从 10 行增加到 100 行。Standard 从 150,000 增加到 400,000 单位,从 25 行增加到 250 行。Advanced 的单位翻倍,从 100 行增加到 500 行。
现在把这个和定价方式放在一起看。一次调用最低花费 50 单位,此外按行计费,再乘以你请求的列数。Premium 列(如 volume、keyword_difficulty 和 traffic_domain)每行大约额外增加 10 单位。
把这两个事实放在一起,你就掉进了陷阱。我整个日志中最昂贵的一次调用是以 250 行的限制运行的。这不是我经过思考后选择的数字。它正是我所在计划的行数上限,我伸手去拿它是因为它是可用的最大值。那些调用每次花费 5,250 单位。同样的查询在 50 行时只需要 1,050 单位,而且告诉我的东西是一样的,因为第 51 到 250 行是我从未使用过的长尾噪音。
行数上限不是一个建议。它是一个天花板,而且自 4 月以来这个天花板比以往高出了十倍。如果你的代码传递了一个显式限制,这个变化是无害的,因为 50 仍然意味着 50。它会在三种特定情况下咬你:当你根本没有传递限制时,当你的代码请求当前可用的最大值时,以及当一个查询过去被旧上限裁剪而现在返回整整十倍行数时。这三种情况在春季之前写的脚本中都很常见,而且在代码中看起来没有任何不同。
根据你实际要读取的内容设置限制。对于关键词扩展,我通常从 50 开始,只有当结果明显在边界处被裁剪且额外行确实重要时才提高。
十个让我白跑一趟的陷阱
这些都没有以你会提前找到的方式写在文档里。全部都产生了要么需要解码的错误,要么更糟糕的是一个看起来像发现的空结果。
where 后必须是 JSON,字符串表达式不行。写 "position>3 and position<15" 返回 bad where: invalid JSON syntax。同样的过滤器写成 JSON 需要每个条件一个子句:
{"and":[{"field":"position","is":["gt",3]},
{"field":"position","is":["lt",15]}]}
运算符有 gt、gte、lt、lte、eq。
select 在不同工具之间类型会变。Site Explorer 和 Keywords Explorer 想要逗号分隔的字符串。batch-analysis 想要数组。把它们混用会得到 column '["domain"' not found(一个方向)和 expected array but got string(另一个方向)。
不要音译变音符号。关键词匹配是字面的。用 ue 代替 ü 写的德语术语返回零行,看起来像一个死关键词。西班牙语的重音符号同样适用。
国家过滤器可能静默抹去真相。按国家过滤自然排名关键词时,对于一个确实有排名的域名返回了零行,因为它的排名位于其他国家。我花了 50 单位买了一个空答案,差点得出该域名没有任何排名的结论。先不加过滤器取数,把 keyword_country 作为一列。
默认使用 mode: "subdomains"。对于任何顶级域名重定向到 www 的网站,mode: "domain" 测量的是确切的主机并返回虚假的零。我见过一个域名在 domain 模式下报告几百个反向链接和零流量,而在 subdomains 模式下是两万两千个反向链接。
volume-history 接受 keyword,单数。传入 keywords 会报错 required arguments [keyword] are missing。一批 26 次调用全部被拒绝,就因为一个字母。
今天不是有效的 date_to。甚至聚合端点也会拒绝当前日期,报 bad date_to。用昨天。
永远不要按名称查找 Site Audit 问题。问题名称不是唯一的。我发现同一个名称附属于两个不同的 issue id,其中一个永久为空。从你自己的 issues 响应中获取 id,而不是用文本匹配。
空的 Search Console 详情表并不意味着连接断了。详情端点大约有六周的延迟才会出现数据,所以查询最近 30 天会返回空结果,而聚合数据是截至昨天的。我见过两个人断定他们的 GSC 集成坏了,而它运行得完全正常。
把 org_traffic 当作模型而不是测量值。它是从 Ahrefs 索引中的关键词乘以一条点击曲线估算出来的,所以索引中没有的任何东西simply does not exist 在那个数字里。在我将其与真实 Search Console 数据对比的 niche 网站上,它的结果低得太多,有一个案例的差距如果不把两个数字并排放着我都不会相信。在你自己的域名上,Search Console 是真相。在竞争对手域名上,把它标为下限而不是一个具体数字。
在花任何钱之前先跑免费的面
我养成最好的习惯:所有与你自验证项目相连的东西要么免费要么几乎免费,而大多数人从未碰过它。
在我的日志中,Search Console 端点、管理端点和免费的域名评级查询加起来花了零单位,返回了将近 6,000 行。只有一个例外而且是个陷阱:gsc-anonymous-queries 虽然带有相同的 gsc- 前缀,但每次调用计费 50 单位,在小网站上返回的几乎什么都没有。site-audit-page-explorer 无论返回多少行,每次请求固定收取 50 单位,在将近 10,000 行中折算下来大约每行三分之一单位,每个 URL 有二十多个技术字段。Rank tracker 和订阅信息也是免费的。
然后是账本的另一面。三个工具,都是按行计费的关键词和反向链接拉取,占了我所有支出的 78%。便宜和免费的面返回了大约 18,000 行花了 3,400 单位。三个昂贵的返回了 30,000 行花了 592,000 单位。同样的预算,价值相差约一百倍。
所以顺序是:先检查你剩余的单位,耗尽免费的项目面,用按次计费的审计工具深入研究,然后只有在有你慎重选择的限制时才使用按行计费的工具。
如果有人从明天开始我会告诉他们什么
生成一个 MCP 范围的密钥,把你的客户端指向远程端点,不要安装任何东西。如果你在聊天中工作用 OAuth,如果你的任何东西按计划运行用 bearer。在你的第一个真正的查询之前,打开订阅信息工具,从你的计划中读取你的行数上限,而不是假设它。
然后写下你自己的数字。这篇文章中的成本是我在一个计划上通过 1,102 次调用测量出来的,定价模式在 4 月有了明显变化。每个响应都内联携带其实际成本,这意味着如果你注意看的话工具会告诉你它收了多少钱。两天花时间记录那个字段教会了我关于钱花在哪里的东西,比读多少文档都多。
本文最初发表于 studiomeyer.io。