AI面包君Learn · Build · Share
02Bread MCP · 第 4
总进度 13 / 33

真实工具集 MCP server

让工具即插即用。真实工具集 MCP server,边读边运行配套 Python 代码。

2026-09-0735 分钟180 行代码难度 ★★

1. 故事:把 echo 换成真实工具

ch01 的 echo server 只是教学用——没人真的需要"回声"。这一章我们把它升级成 3 个真有用的工具,它们和 Bread Agent ch02 的工具一一对应:

工具作用
read_file读取文本文件内容
list_dir列出目录下文件
get_time返回当前系统时间

工程上的关键升级有 4 点:

  1. dispatch 表注册工具(不再 if-elif 一堆)
  2. schema 和实现配对存放(看 TOOL_REGISTRY
  3. 工具失败用 isError=true 返回(不抛异常给协议)
  4. handler 也用 dict 分发(看 HANDLERS

TOOL_REGISTRY 就是 Bread Agent 里 TOOLS_IMPL + TOOLS_SCHEMA 的合体版——加新工具只用加一项 dict 项,主循环代码一行不用改。

文件结构:

ch03_real_tools/
├── server.py    # 真实工具集 server
├── client.py    # ch02 的 MCPClient(复用)
├── demo.py      # 演示脚本:连 server + 调 4 个用例
└── README.md

2. 跑起来

cd ch03_real_tools
python demo.py

预期输出:

已连接: bread-mcp-fs v0.1.0

--- list_tools ---
  read_file  读取一个文本文件的内容(超过 4000 字会截断)。
  list_dir   列出一个目录下的文件和子目录。
  get_time   返回服务器当前的本地时间(精确到秒)。

--- 调 get_time ---
  结果: 2026-05-17 23:01:23  (error=False)

--- 调 list_dir(.) ---
  结果:
FILE README.md
FILE client.py
FILE demo.py
FILE server.py

--- 调 read_file(README.md) ---
  前 200 字: # 第 3 章 ...

--- 调一个会出错的:read_file(不存在的文件) ---
  isError=True, 错误信息: FileNotFoundError: ...

最后一行重点看——工具执行报错时,server 没崩,而是返回了 isError=true + 错误说明。


3. 逐行精讲(server.py)

第 1 段:工具注册表

TOOL_REGISTRY: dict[str, dict] = {
    "read_file": {
        "schema": {
            "name": "read_file",
            "description": "...",
            "inputSchema": {...},
        },
        "impl": tool_read_file,
    },
    ...
}

每一项有两部分:

  • schema:给 MCP client 看的 JSON Schema(在 tools/list 时返回)
  • impl:真正的 Python 函数(在 tools/call 时调用)

为什么放在一起?因为它们必须配对修改——schema 里说 required: ["path"],impl 就得接收 path 参数;不一起改容易出 bug。这是个朴素但有效的工程约束。

第 2 段:tools/list 变简单了

def handle_tools_list() -> dict:
    return {"tools": [t["schema"] for t in TOOL_REGISTRY.values()]}

一行。注册表里有什么就列什么。

第 3 段:tools/call 的优雅 dispatch + 错误处理

def handle_tools_call(params):
    name = params.get("name")
    arguments = params.get("arguments", {}) or {}

    entry = TOOL_REGISTRY.get(name)
    if entry is None:
        raise ValueError(f"unknown tool: {name}")

    try:
        result = entry["impl"](**arguments)
        return {
            "content": [{"type": "text", "text": str(result)}],
            "isError": False,
        }
    except Exception as e:
        return {
            "content": [{"type": "text", "text": f"{type(e).__name__}: {e}"}],
            "isError": True,
        }

关键点

  • "工具不存在":抛 ValueError → 协议层错误(JSON-RPC error 字段,code -32603)
  • "工具执行失败":捕获后返回 isError=true应用层错误(JSON-RPC 响应是成功的,但 result 里有 isError 标记)

为什么要区分?因为 agent 拿到工具错误应该继续工作(读 isError → 把错误信息回喂给 LLM → 重试或换法);但 agent 拿到协议错误就是 client 写错了(method 拼错之类),不应该假装"工具失败"。

这跟 Bread Agent ch02 的"错误回喂"思想完全一致——MCP 协议把它标准化了。

第 4 段:HANDLERS dict 取代 if-elif

HANDLERS = {
    "initialize": lambda p: handle_initialize(p),
    "tools/list": lambda p: handle_tools_list(),
    "tools/call": lambda p: handle_tools_call(p),
}

# 主循环里:
handler = HANDLERS.get(method)
if not handler:
    send(...错误响应...)
    continue
result = handler(params)

加新协议方法只用加一项。resources/list / resources/read / prompts/list 等都可以这么扩。


4. 卡住了怎么办

❌ 调 read_file参数类型错误

工具 schema 里说 required: ["path"],但你没传 path——但我们没在 server 做 schema 校验。生产级 MCP server 应当用 jsonschema 库校验,教学版省了。可以作为思考题。

❌ 中文文件名读不到

Path(path) 在 Windows 上对中文路径正常,但你传的 path 字符串本身要是 UTF-8 编码。如果直接在 shell 里手敲中文,注意终端编码。

read_file 返回的内容看不全

我们有 4000 字截断(防止把 client 上下文撑爆)。要看完整文件,改 tool_read_fileif len(text) > 4000 那段注释掉。

❌ 想加自己的工具

3 步:

  1. 写一个普通 Python 函数
  2. TOOL_REGISTRY 加一项(schema + impl)
  3. 重启 server——主循环代码不用改

5. 思考题

题 1:加 write_file 工具

学着加一个 write_file(path, content)。schema 里 required 是 ["path", "content"],impl 把内容写到文件(用 pathlib.Path.write_text)。

加完后用 demo.py 多调一个 c.call_tool("write_file", path="test.txt", content="hello") 验证。

题 2:用 jsonschema 校验参数

pip install jsonschema,然后在 handle_tools_call 调 impl 之前先校验:

from jsonschema import validate, ValidationError

try:
    validate(arguments, entry["schema"]["inputSchema"])
except ValidationError as e:
    return {
        "content": [{"type": "text", "text": f"参数不合法: {e.message}"}],
        "isError": True,
    }

这样调用方传错参数会拿到清晰的错误,而不是 Python 的 TypeError。

题 3:暴露 resources

MCP 不只能暴露工具,还能暴露资源(文件、API 响应等可读对象)。给 server 加两个方法:

"resources/list": lambda p: {"resources": [
    {"uri": "file:///cwd/", "name": "Current Directory", "mimeType": "inode/directory"},
]},
"resources/read": lambda p: {"contents": [
    {"uri": p["uri"], "mimeType": "text/plain", "text": "..."},
]},

跑 demo.py 时改成调 c._request("resources/list") 直接看效果。这是 ch04 把 MCP 接到 Bread Agent 的下一步铺垫


这一章你学会了什么

  • TOOL_REGISTRY 把 schema 和 impl 配对——加工具只加一项,主循环不动
  • 协议错误 vs 应用错误:方法不存在抛 JSON-RPC error;工具执行失败用 isError=true
  • ✅ 用 dict + lambda 把 method dispatch 写得很优雅(HANDLERS
  • ✅ MCP 工具的错误处理思想 = Bread Agent 的"错误回喂"

下一章 ch04(重头戏):把这个 server 接入 Bread Agent——让 agent 透明地调用 MCP 工具,就像它们是本地工具一样。MCP 的价值在这一章才真正显形