MCP 实战

专栏:Agent 工程 · 第 11 / 18 篇
AgentMCP工具

你的 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 可以水平扩展,这对部署是重大简化。

图表(mcp-in-practice.md)

写一个最小 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 规范方向)。

常见踩坑

  1. 在 server 里 print() 调试——stdio 传输下 stdout 就是协议通道,一个 print 就把 JSON-RPC 消息流搅乱,客户端直接解析失败。调试信息一律 print(..., file=sys.stderr)
  2. 工具抛未捕获异常——客户端收到的是 server 崩溃而非可读错误;返回 Error: ... 字符串让模型自纠;
  3. 路径写死绝对路径——server 可能在任何工作目录被拉起,路径参数化或从环境变量读;
  4. 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"})

Codexcodex-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 的复利才开始。

随堂练习(带验收标准)

  1. 跑通 blog_stats_server.py,用 mcp dev 调用一次。验收:返回统计文本而非异常;
  2. 把它接进你的 mini-agent(工具清单合并 + 调用转发),让模型回答”那篇文章有多少代码块”。验收:模型自主调用工具并给出正确数字;
  3. 踩坑复现:在 server 里加一行 print("called!"),观察客户端如何解析失败——再改回 stderr。这个坑值得亲手踩一次;
  4. 进阶:读 codex-rs/codex-mcp/src/ 的连接管理,或在 dsh 里写一个 MCP 桥接插件。

← 返回文章列表