把 Cloudflare API 接进 Agent 工具链
通过 MCP 把云平台 API 暴露给 Agent 之后,真正需要设计的不是连接本身,而是权限边界与回滚路径:token 按最小范围发放、写操作前先导出快照、下发脚本保持幂等,并警惕 PUT 整表语义。
问题:Agent 需要直接读写远端资源
在把云平台 API 接给 Agent 之前,操作路径是人肉的:打开控制台点几下,或者手敲 curl 拼 JSON。改一条 DNS 记录、调一条隧道 ingress、看一眼对象存储桶——每一步都要人读返回值判断成败。
麻烦的不只是慢。面板操作不可复现:点完之后没有能重放的命令,改错了只能凭记忆再点回去。
MCP(Model Context Protocol)把这类 API 封装成结构化工具:Agent 按 schema 调用,拿到的是结构化数据而不是一段 HTML。代价是凭据交给了进程——tools 里任何一个能改状态的函数,都让「误改」的成本降到接近于零。
tools 与 resources:只有一类会产生副作用
MCP 服务器对外暴露两类能力:
| 类型 | 语义 | 是否改变远端状态 | 权限给法 |
|---|---|---|---|
tools |
可调用的函数 | 会 | 按最小集,写权限尤其收紧 |
resources |
可读取的数据 | 不会 | 可以放宽 |
接上之后先列一遍:
/mcp list
<api 服务器> http https://mcp.example.com/mcp tools: docs, search, execute
<docs 服务器> http https://docs.example.com/mcp tools: search
这里有个容易误判的点:工具数量少不等于能力小。上面那个 API 服务器采用 Code Mode,只暴露三个工具——文档、检索、执行,而「执行」背后是两千多个 API 端点。也就是说单个 execute 的能力面,等于 token 的全部权限范围。
结论很直接:工具清单不是权限边界,token 的 scope 才是。
凭据:最小权限,以及本机的多处副本
配置本身很朴素,token 直接写在 header 里:
{
"mcpServers": {
"<api 服务器>": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <API 令牌>" }
}
}
}
两件事要同时做。
一是文件权限。 token 是明文,配置文件必须收到 root-only:
chmod 600 ~/.config/<agent>/mcp.json
stat -c '%a %n' ~/.config/<agent>/mcp.json
# 600 /home/user/.config/<agent>/mcp.json
二是 token 自身的范围。 给到能完成任务的最小集合,能限定到单个区域就别给账户级。最容易犯的错是图省事给「全域读写」:只想改几个域名的解析记录时,限定到 DNS 编辑权限就够,多出来的权限只是把 execute 的爆炸半径放大。
然后是被忽略的一面:token 在本机不止一份。除了配置文件,Agent 的会话记录和历史数据库通常会把工具调用原文存下来(包括 Authorization 头),备份目录又会把配置文件再复制一遍。按前缀搜一遍:
sudo grep -rlE '<前缀>_[A-Za-z0-9]{20,}' ~/.config ~/.local/share /srv /var/backups 2>/dev/null
实测常见结果是四条:配置文件、sessions/ 下的会话文件、history.db、以及一个时间戳命名的备份目录。这说明「轮换 token」不是改一个文件就完事;反过来也说明,先摸清副本清单,再谈要不要轮换。
「配置正确」与「实际接通」是两回事
配置里 URL 写对,不代表服务器认。两个坑:
- 端点 URL 会记错。 曾经把端点写成
https://api.mcp.<平台域>/mcp,而实际存在的是https://mcp.<平台域>/mcp。多一个api.前缀,HTTP 层直接连不上——这种错只有真发一次请求才暴露,读配置看不出来。不同平台的 MCP 端点形态还不一样,抄文档时尤其容易混。 - 连得上、有权限,是两层检查。 Bearer 头格式正确(TCP/TLS 与协议层通过)、token 有效(不返回 401)、token 有权做这件事(不返回 403),三者互相独立。
验证顺序应当是先只读、后写:
curl -fsS "https://api.example.com/v4/zones/<区域 ID>/dns_records?per_page=5" \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
| jq -r '.result[] | [.type, .name, .content] | @tsv'
A www.example.com 203.0.113.10
CNAME wiki.example.com <隧道 ID>.<平台隧道域>
只读调用通了,才谈写操作。而且写操作的首次验证应落在一个无副作用或可回退的对象上,不要拿生产记录试。
写操作:先 dump 再改
改云端配置的风险不在「改」,而在改之前有没有一份能回去的状态。有些配置只存在于云端:隧道采用 token 模式时,ingress 规则由平台侧下发,本地没有对应文件。这类资源必须先导出:
#!/bin/bash
set -euo pipefail
zone='<区域 ID>'
stamp=$(date -u +%Y%m%dT%H%M%SZ)
out="/srv/backups/dns-${stamp}.json"
curl -fsS "https://api.example.com/v4/zones/${zone}/dns_records?per_page=200" \
-H "Authorization: Bearer $PLATFORM_TOKEN" > "$out"
jq -r '.result[] | [.type, .name, .content, .proxied] | @tsv' "$out" \
| sort > "${out%.json}.tsv"
wc -l < "${out%.json}.tsv"
三个要点:dump 必须在改之前执行;要落成可读文本(用 jq 打平的 TSV 比原始 JSON 好 diff);脚本同时保留 --dump(只读)与下发两个模式,让还原不需要重写代码。改完之后用同一脚本再 dump 一次,比对两次快照:
diff before.tsv after.tsv
非空的行就是这次改动的全部影响——这是把「Agent 说它改了什么」变成「磁盘上只剩这一处差异」的最低成本做法。
双刃:恢复变容易,误改也变容易
脚本化之后,恢复配置从「回忆着点面板」变成「跑一条命令」,这是收益;但同一条命令也让误改变成一次调用,而 Agent 执行时不会犹豫。两个必须内建的约束:
幂等。 同一份期望状态下发两次,结果必须一致。判据很具体:连续执行两次下发,再 diff 两次 dump 的快照,应当为空。做不到幂等就要先想清重复执行的后果——「追加」语义的接口会重复创建记录。
整表替换的警觉。 有些接口是 PUT 整表语义:提交的数组就是改完之后的完整集合,没列进去的条目会被静默删除。在这类接口上「只改一条记录」是危险的——你以为在做局部修改,实际在做全量覆盖。判断方法有两个:看返回体里缺不缺你没提交的条目;或看改后快照里有没有你没打算删的行。
在脚本里加硬约束比事后发现便宜得多,例如:清单条目数少于预期时直接中止;兜底项(如 http_status:404 这类结尾规则)不存在时拒绝下发。注意顺序语义——具体条目必须排在兜底项之前,否则全部请求落到兜底。
可操作要点
- 权限在 token 的 scope 上收紧,不要指望在 MCP 配置层限制;
execute一类工具的能力面等于 token 的全部权限 - 配置文件
chmod 600,并用 token 前缀全盘grep摸清副本:配置、会话记录、历史库、备份目录都算 - 连通性验证先只读(列记录),再写;写操作的首次尝试落在可回退对象上
- 任何写操作前先 dump 成可 diff 的文本,脚本里保留只读模式与还原路径
- 幂等性用「连做两次下发,快照 diff 为空」来判定
- 遇到 PUT 整表语义的接口,禁止下发只含局部条目的清单;条目数变少要当作异常中止