你有没有遇到过这种情形:Agent 反复调用一个此刻根本不可能成功的工具,靠报错才知道此路不通,然后一次次重试?MCP 服务器会告诉模型「有哪些工具、参数是什么」,却不会告诉它「这个工具在资源处于什么状态时才有效」。MCPgraphy 补的就是这一层:它在 Agent 和 MCP 服务器之间放一个本地代理,从 Agent 自己发出的调用里被动学习,画出一张「状态 → 工具 → 结果」的行为图,然后按你设的策略给提示或直接拦下无效调用。本教程六步跑通:两分钟无密钥演示、写 adapter、三档策略、接进宿主、读懂行为图、渐进切到拦截并验收,每一步都有可复制的命令与配置。
一、先搞懂:MCP 少了哪一层信息
一份标准的 MCP 工具清单只有名字、描述、参数 schema,它描述的是「能力」,不是「时机」。于是模型会去编辑一个还不存在的文件、发布一个没跑过测试的版本、移动一张已经关闭的工单,然后从服务端错误里学乖——有时重试的那一下恰好造成了真实改动。MCPgraphy 的做法是把资源在每次调用前后各读一次,把观测到的状态记成节点、把发生的事记成边:
| 角色 | 谁提供 | 说明 |
|---|---|---|
| 工具清单 | MCP 服务器 | 有哪些工具、参数怎么填 |
| 资源状态 | 只读观测工具 | 调用前后各读一次,由 adapter 指定 |
| 行为图 | MCPgraphy | 哪个状态→哪个工具→成功或失败,记几次 |
| 策略动作 | MCPgraphy | 只记录 / 附提示 / 直接拒绝 |
它是观测型工具,不是权限系统。官方明确写了:授权(Authorization)是独立字段,绝不会从行为里推断——某个工具每次都成功,不等于它被批准了。另外它是 v0.1.0 的早期项目,官方自测数据来自两个模拟服务器上的 200 个有状态任务与脚本化 Agent(无效调用从每任务 2.19 次降到 0.71 次、完成率 0.85 到 1.00),这是作者自建基准而非第三方生产验证,别把它当成你环境里的承诺值。
Step 1:装包并跑一遍两分钟无凭证演示
# 建个干净目录,避免污染现有项目
mkdir mcpgraphy-lab && cd mcpgraphy-lab
# 安装(MIT,纯本地,无密钥)
pip install mcpgraphy
# 下载官方演示脚本与 filesystem adapter
curl -O https://raw.githubusercontent.com/hemanth1999k/mcpgraphy/main/examples/filesystem_demo.py
curl -O --create-dirs --output-dir adapters https://raw.githubusercontent.com/hemanth1999k/mcpgraphy/main/examples/adapters/filesystem.json
# 跑两幕演示(需要 Node,脚本内部会用 npx 拉官方 filesystem 服务器)
python filesystem_demo.py /tmp/mcpgraphy-demo
演示分两幕:Act 1 以 warn 模式跑一个「不聪明」的 Agent,故意编辑不存在的文件、移动不存在的文件,再各做一次正确操作,代理从这七次调用里学会规律;Act 2 用学到的图重启为 block 模式,同样的错误被带证据拒绝、合法的编辑放行、未批准的 move 被扣住,并列出当前状态下能做什么。
mcpgraphy.sqlite,删掉即可从头再来。Step 2:写 adapter.json,告诉代理怎么读回资源
代理不猜你的工具语义,需要你指明「用哪个只读工具把资源读回来」以及「哪个参数代表资源标识」。其余字段都可省。以官方 filesystem adapter 为例:
{
"server_id": "filesystem",
"resource_class": "file",
"observation_tool": "get_file_info",
"id_param": "path",
"id_param_aliases": ["source"],
"observation_format": "kv_text",
"features": ["isFile", "isDirectory"],
"effect_classes": {"edit_file": "MUTATING", "create_directory": "MUTATING"},
"approval_required": ["write_file", "move_file"]
}
| 字段 | 作用 |
|---|---|
| observation_tool / id_param | 用哪个只读工具读回资源、哪个参数是标识 |
| id_param_aliases | 同一标识在别的工具里的别名(move 用 source) |
| observation_format | fields(结构化,默认)或 kv_text(键值文本) |
| features | 哪些字段算状态;省略时自动识别布尔与枚举式字符串 |
| effect_classes | READ_ONLY / MUTATING / DESTRUCTIVE;DESTRUCTIVE 在 block 下需批准 |
| approval_required | 无论证据如何,block 下都要 --approve 才放行 |
别把 observation_tool 填成写工具。代理会在每次调用前后各读一次资源,填错等于让观测动作本身产生副作用。另外官方目前只带 filesystem 与两个内置模拟器三种现成 adapter,GitHub / Linear / Slack / Postgres 以及从 schema 自动起草 adapter 的能力都还在 Roadmap 里,接别的服务器要自己写这份 JSON。
Step 3:选策略,从 observe 到 warn 再到 block
# stdio 下游(把命令跟在 -- 后面)
mcpgraphy wrap --adapter adapter.json --policy warn -- npx -y @modelcontextprotocol/server-filesystem ~/work
# Streamable HTTP 下游
mcpgraphy wrap --adapter adapter.json --policy block --approve publish_release --url http://localhost:8000/mcp
# 指定数据库与重学频率
mcpgraphy wrap --adapter adapter.json --policy warn --db ./lab.sqlite --refresh-every 20 -- npx -y @modelcontextprotocol/server-filesystem ~/work
| --policy | 行为 |
|---|---|
| observe | 只记录与学习,不附提示也不拦 |
| warn(默认) | 全部放行;命中「此状态曾失败」或结果矛盾时附一条说明 |
| block | 在服务器看到之前拒绝曾失败于此状态的调用,以及 DESTRUCTIVE / approval_required 工具(除非列入 --approve) |
--db 选 SQLite 文件(默认 mcpgraphy.sqlite),--refresh-every N 表示每 N 次调用重学一次,--no-hints 关掉附在结果里的状态提示行。官方建议先在 warn 跑一段时间,等提示看着靠谱了,再切到 block 并把自己允许的写操作逐项列进 --approve。
Step 4:把宿主指向代理,而不是原来的服务器
在 Claude Desktop、Cursor 或任何以 stdio 启动 MCP 服务器的宿主里,把原来那条命令整体挪到 -- 后面即可。下游工具对模型完全透明:
{
"mcpServers": {
"filesystem": {
"command": "mcpgraphy",
"args": ["wrap", "--adapter", "/abs/path/adapters/filesystem.json", "--policy", "warn",
"--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/you/work"]
}
}
}
adapter 路径请用绝对路径,宿主启动时的工作目录和你在终端里的目录常常不是同一个;相对路径会让代理起不来且报错不够直观。若宿主报找不到 mcpgraphy,把 command 换成 which mcpgraphy(Windows 用 where mcpgraphy)返回的完整路径。密钥安全方面:本教程全程不需要 API Key;若你接的是需要凭据的下游服务器,凭据仍放在宿主的环境变量或配置文件里,别写进 adapter.json 并顺手提交到 Git。
Step 5:读懂行为图——四个工具和四种边状态
除了透传下游工具,代理自己会多出四个工具:
| 工具 | 回答什么 |
|---|---|
| mcpgraphy_affordances(resource_ref, goal?) | 这个资源此刻:哪些可选、哪些要批准、哪些此状态不支持、哪些没试过 |
| mcpgraphy_explain(offer_id) | 这条结论背后的守卫、正反观测、反例、还有哪些没验证 |
| mcpgraphy_graph(format) | 整张图,JSON(权威)或 Mermaid |
| mcpgraphy_status() | 记录了多少调用、告警与拦截数、当前模型版本 |
每次读资源或做动作,结果里都会附一行图状态提示,形如:
mcpgraphy [status=DRAFT, tests_passed=False] ok: run_tests -> tests_passed becomes true | no: archive_release; publish_release | to tests_passed=True: run_tests
被拒绝的调用也会带上理由,例如「观察到此状态 [exists=False] 失败过 2 次」。每条边有四种状态:
| 状态 | 含义 |
|---|---|
| OBSERVED_SUPPORTED | 就在这个观测状态下复现成功过,且无矛盾观测 |
| CONDITIONAL | 由归纳出的守卫推出,或是矛盾双方中的一边 |
| CONTRADICTED | 同状态下后续调用与早前结论不一致 |
| UNKNOWN | 没试过;既不算有效也不算无效 |
服务器改了工具 schema 会触发重测。官方说明:模型指纹不再匹配时,继承来的边会被标记为待重测,改动后的头几次调用要先验过才会重新被信任。所以升级下游 MCP 服务器后,别急着把策略切到 block。
Step 6:验收、渐进切换与边界
建议在 warn 阶段先攒够观测,再按下面的顺序验收:
# 1. 看累计记录与告警数(也可直接查 SQLite)
mcpgraphy graph export --server release_sim --format mermaid --db demo.sqlite
mcpgraphy offers --server release_sim --resource release:demo-0-5 --db demo.sqlite
mcpgraphy offers --server release_sim --resource release:demo-0-1 --db demo.sqlite
# 2. 切到 block,只放行你确认过的写操作
mcpgraphy wrap --adapter adapter.json --policy block --approve publish_release -- npx -y @modelcontextprotocol/server-filesystem ~/work
# 3. 复放 Act 1 的错误动作,核对每条拒绝都带证据行
预期效果:同一批错误动作在 block 模式下于服务端看到之前被拒绝,拒绝消息里能指出具体状态与观测次数;合法动作照常放行;未批准的写操作被扣住;mcpgraphy_affordances 能列出当前状态可做与不可做的清单;重启代理后图仍在(存在 SQLite 里)。
三类情况它帮不上:官方基准文档点名了隐藏状态(hidden state)、过期快照(stale snapshots)、非确定性开关(nondeterministic flag)——可见状态图看不见的因果,不会凭空变准。另外它是 v0.1.0 单机代理:--approve 是静态列表而非运行时弹窗审批,stdio 之外以 Streamable HTTP 对外提供代理、本地图浏览页、向宿主发审批请求都还在 Roadmap。生产里请把它当作「减少无效调用的一层」,而不是安全边界。
uv sync + uv run pytest(约 100 个测试)、mcpgraphy learn 主动实验、mcpgraphy benchmark run --suite stateful-v1 与 benchmark report,可拿两个内置模拟器复跑官方基准。作者还列出 mcp-scan 这类「这条调用是否被允许」的护栏工具,与 MCPgraphy「这条调用有没有在此状态成功过」是互补关系,可以串在同一条链上。常见问题速查
| 现象 | 大概率原因与处理 |
|---|---|
| 宿主连不上代理 | command 找不到可执行文件,或 adapter 用了相对路径;换绝对路径并重启宿主 |
| 装上后一条告警都没有 | 冷启动图还很稀疏;先在 warn 下多跑几轮真实调用,或调小 --refresh-every |
| block 把正常写操作也拦了 | 该工具落在 DESTRUCTIVE 或 approval_required;把它加进 --approve 并复核 effect_classes |
| 升级下游服务器后判断变乱 | schema 指纹变更触发边重测,等头几次调用验完再恢复 block |
| UNKNOWN 很多 | 设计如此——没观测过的路径报未知,不报安全;别据此推断可用 |