实战篇 04:MCP 工具接入
一句话总结:MCP 只规定了“工具在哪、叫什么、怎么调”;接进来之后,远程工具和本地工具走同一个执行入口、同一套授权。
本实战新增: 一个手写的 MCP Server(约 120 行)和一个把远程工具注册进工具池的扩展(约 140 行),都含注释。不依赖任何 MCP SDK。
对应理论篇:第 14 章 MCP Plugin、第 03 章 Permission。
先看结构
启动 Agent
↓
拉起子进程 notes_server.py,用 stdin / stdout 通信
↓
initialize 握手:协议版本、双方信息
notifications/initialized 通知 Server:可以开始了
tools/list 问它有哪些工具
↓
每个远程工具包成一个 McpTool:notes__list_notes、notes__read_note、notes__add_note
↓
和 read / write / edit / bash 放进同一个工具池
↓
模型调用 notes__add_note → 同一个 _execute → 同一个授权弹窗 → tools/call和第 14 章有什么不同
第 14 章的两个 Server 是同一个 Python 进程里的字典,用来讲清“先发现、再调用”的概念。
这里是真协议:
- Server 是一个独立进程;
- 双方用 JSON-RPC 2.0 通信,一行一条消息;
- 工具的参数结构来自 Server 给的 JSON Schema,客户端不再自己拼。
任何按 MCP stdio 协议实现的 Server,理论上都能用 SERVERS 里的一行配置接进来。本实战只和自带的 notes_server.py 一起测过;接别人的 Server 时,可能要处理这里省略的部分,比如分页(nextCursor)、图片等非文字结果、请求超时。
三处边界
1. 命名空间。 远程工具名加上服务名前缀,比如 notes__read_note。两个 Server 都有 search 时,也不会互相覆盖。用双下划线是因为函数名里不能用点号。
2. 授权。 Server 可以在 annotations.readOnlyHint 里声明“我是只读的”。但这是它自己说的,所以要不要相信,由客户端决定:
SERVERS = {
"notes": {"command": [...], "trust_read_only_hint": True}, # 自己写的,信
}来路不明的 Server 应该设成 False,这样它的每个工具都会弹窗确认。
3. Server 自己也要守边界。 notes_server.py 会检查笔记名,只允许字母、数字、中文、下划线和横线。模型传进 ../../etc/passwd,Server 会直接报错,不会去读那个文件。客户端的授权和 Server 的校验是两道独立的检查,谁都不能指望对方。
用生活例子理解
公司请了一家外包的档案管理公司。对方给了你一份服务清单(tools/list),上面写着“查档案”“存档案”。
清单是对方写的,但哪些事要你签字才能做,由你们公司自己的规定决定,不看对方怎么写。
跑起来
uv run python labs/01-mini-coding-agent/server.py --lab 04会自动挂上 mcp 扩展,并启动笔记 Server。页面上会显示下面这几个示例,点一下就能填进输入框。
试试:
列出我的笔记,读一下 agent-loop,再新建一篇叫 test 的笔记,内容写 hello实测结果:notes__list_notes 和 notes__read_note 直接执行;notes__add_note 停下来等你授权,弹窗里会显示完整参数。
Server 的 stderr 没有被接管,它打的日志会直接出现在终端里。stdout 只能放协议消息,这是 stdio 传输的规矩。
新建的笔记会写进
labs/04-mcp/notes/。
今天只记住
MCP 解决“怎么接”,Harness 决定“让不让做”。
想一想
如果一个 Server 在 tools/list 里把 delete_all_notes 标成了 readOnlyHint: True,而你的配置是 trust_read_only_hint: True,会发生什么?
参考思路(先自己想一想,再展开)
它会被当成只读工具,不弹窗就直接执行,笔记全没了。
所以 trust_read_only_hint 只能给自己能审查代码的 Server 打开。更稳妥的做法是:即使信任 Server,也在客户端按工具名再加一道规则,比如名字里带 delete 的工具一律需要确认。声明只是参考,决定权要留在自己手里。