MCP 实战
专栏:Agent 工程 · 第 11 / 18 篇你的 mini-agent 已经有了自定义工具,但每个 agent 都重写一遍”读数据库、调内部 API”是重复劳动。MCP(Model Context Protocol)把它标准化:工具能力写一次,任何 agent 都能挂载。
:::info 学习目标 完成本篇后你能够:说出 MCP 三角色的职责与两种传输方式;写一个可被任意 MCP 客户端挂载的 stdio server;解释 MCP 工具与本地工具的取舍。 前置:第 4 篇的 mini-agent 可用。预计时长:60 分钟。 :::
:::note 本章术语速查(新手建议先读)
- MCP(Model Context Protocol):连接模型与工具/数据源的开放标准——工具写一次,所有支持 MCP 的 agent 都能用。
- Host / Client / Server:MCP 三角色。Host 是 agent 本体;Client 是 host 里的连接端;Server 是提供工具的服务(可以是一个本地子进程)。
- stdio 传输:server 作为子进程启动,通过标准输入输出通信——最简单的部署方式。
- JSON-RPC:MCP 底层用的通信格式(用 JSON 表示的远程调用)。
- 工具清单(tools/list):server 告诉 client”我有哪些工具、参数是什么”的接口。 :::
MCP 的架构
三个角色:
- Host:agent 本体(Codex、dsh、你的 mini-agent),发起连接;
- Client:host 内维持与单个 server 会话的连接端;
- Server:暴露工具/资源/提示词模板的服务,stdio(本地子进程)或 HTTP(远程)传输。
2026 年的两个重要现状(开篇调研笔记提过):规范已是事实标准(企业大规模采用、NSA/CISA 发布安全指南);2026-07-28 版规范移除会话状态、转向无状态核心——server 可以水平扩展,这对部署是重大简化。
写一个最小 server
先装 SDK:pip install "mcp[cli]"。Python 官方 SDK 十几行就能起一个 stdio server——一个查询博客文章统计的”私有能力”:
# blog_stats_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("blog-stats")
@mcp.tool()
def post_stats(slug: str) -> str:
"""查询指定博客文章的统计信息。
用于:获取文章字数、代码块数量、标签。
slug 是文章的 URL 标识(如 agent-learning-path)。
"""
path = f"/srv/blog/content/{slug}.md"
try:
text = open(path).read()
except FileNotFoundError:
return f"Error: 文章 {slug} 不存在" # 错误信息给模型可读
code_blocks = text.count("```") // 2
return f"字数 {len(text)},代码块 {code_blocks},路径 {path}"
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
检查点:SDK 自带调试器直接验证——mcp dev blog_stats_server.py 打开交互界面,能看到 post_stats 工具、调用一次返回统计文本。三个 MCP 工具设计要点都在这个例子里:docstring 是模型可见的描述(何时用 + 参数语义 + 错误格式);错误返回字符串而不是抛异常(让模型自我纠正);server 无状态(每次调用自包含,符合 2026 规范方向)。
常见踩坑:
- 在 server 里
print()调试——stdio 传输下 stdout 就是协议通道,一个 print 就把 JSON-RPC 消息流搅乱,客户端直接解析失败。调试信息一律print(..., file=sys.stderr); - 工具抛未捕获异常——客户端收到的是 server 崩溃而非可读错误;返回
Error: ...字符串让模型自纠; - 路径写死绝对路径——server 可能在任何工作目录被拉起,路径参数化或从环境变量读;
- server 有隐藏状态——2026 规范已转向无状态核心,水平扩展时旧写法直接失效。
协议本身:三个 JSON-RPC 方法撑起最小可用
MCP 基于 JSON-RPC,最小闭环只有三步:initialize(交换协议版本与能力)→ tools/list(枚举工具与 schema)→ tools/call(调用并返回内容)。工具之外还有两类可选能力:resources(把数据暴露为可读资源,如日志文件)与 prompts(暴露提示词模板)。网关与 agent 通常只消费 tools——这也是”写一个 MCP server”的学习成本远低于想象的原因。
安全上记住一条:server 输出是不可信输入。工具返回的文本会进入模型上下文,可能夹带注入指令(第 12 篇)——对 MCP server 的作者而言,永远不要把第三方 API 的原始响应不加处理地返回给模型。
接进三种 agent
你的 mini-agent:MCP server 本质是”工具的外置进程”。最轻的接法是用 MCP client 库枚举工具清单、转发调用:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(command="python", args=["blog_stats_server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools() # schema → 转成你的 tools 格式
result = await session.call_tool("post_stats", {"slug": "agent-learning-path"})
Codex:codex-mcp crate 是完整的 MCP 客户端运行时——连接管理、工具目录缓存、OAuth 登录、elicitation(server 反向询问用户),底层用官方 RMCP 实现。MCP 工具经 handlers/mcp.rs 进入统一 ToolRouter,与内置工具平权竞争,审批同样覆盖 McpToolCall 动作。
dsh:建议路径是”每个 server 一个插件”——插件挂载时用 MCP client 枚举工具,逐个注册进 ctx.tools;优势是工具进入 dsh 的统一把关流水线(审批、超时、重排全部适用)。
设计取舍:什么时候写 MCP server,什么时候写本地工具
| 本地工具(进程内函数) | MCP server | |
|---|---|---|
| 适合 | agent 专属、逻辑简单 | 跨 agent 复用、需要独立进程隔离、第三方分发 |
| 成本 | 零部署 | 多一个进程/服务 + 协议层 |
| 沙箱/审批 | 随宿主 | 独立进程边界,天然隔离 |
个人单 agent 阶段,本地工具够用;当你想让两三个 agent 共享能力、或能力需要独立升级时,MCP 的复利才开始。
随堂练习(带验收标准)
- 跑通
blog_stats_server.py,用mcp dev调用一次。验收:返回统计文本而非异常; - 把它接进你的 mini-agent(工具清单合并 + 调用转发),让模型回答”那篇文章有多少代码块”。验收:模型自主调用工具并给出正确数字;
- 踩坑复现:在 server 里加一行
print("called!"),观察客户端如何解析失败——再改回 stderr。这个坑值得亲手踩一次; - 进阶:读
codex-rs/codex-mcp/src/的连接管理,或在 dsh 里写一个 MCP 桥接插件。