Skip to content

第 19 章 · 编写第一个 MCP Server

本章目标:理解 MCP Server 的最小结构,用官方 Python SDK 的装饰器风格写出包含工具、资源与提示模板的完整服务器,并学会用 MCP Inspector 调试它。

13.1 环境准备:安装官方 Python SDK

Anthropic 主导的 Model Context Protocol(MCP)提供了官方 Python SDK,包名为 mcp。推荐安装带 CLI 扩展的版本:

bash
pip install "mcp[cli]"
# 或者使用 uv:
uv add "mcp[cli]"

[cli] 这个 extra 会在 SDK 之上额外安装 mcp 命令行工具,提供三个高频子命令:

text
mcp dev server.py      # 启动 MCP Inspector 调试服务器(开发模式)
mcp run server.py      # 直接运行服务器
mcp install server.py  # 把服务器一键安装到 Claude Desktop

如果只想临时跑一次而不污染环境,也可以用 uv 的一次性运行:uv run --with "mcp[cli]" mcp dev server.py

版本注意

早期教程普遍使用 from mcp.server.fastmcp import FastMCP 这种写法。当前官方主线的 API 已经统一为 MCPServer,本教程严格按最新官方 README 编写。如果你在网上看到 FastMCP 字样,那是旧版集成方式,概念完全相通。

13.2 十五行就是一个完整的 Server

创建 server.py

python
from mcp.server import MCPServer

# 创建服务器实例,参数是给客户端展示的名称
mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

这就是一个完整可运行的 MCP 服务器:一个工具(tool)、一个模板化资源(resource)。启动调试界面看看效果:

bash
mcp dev server.py
# 会自动拉起 MCP Inspector 并在浏览器打开 http://localhost:5177

在 Inspector 的 Tools 标签页里调用 add(a=1, b=2),会得到 3——一条标准的 JSON-RPC 响应走完了完整协议流程。

13.3 类型注解即 Schema,docstring 即描述

回看上面的代码,你没有写的东西比写了的更重要:

  • ❌ 没有 JSON Schema —— a: int, b: int 这两个类型注解就是 schema;
  • ❌ 没有请求解析 —— 协议层由 SDK 处理;
  • ❌ 没有参数校验代码 —— Pydantic 在底层自动完成;
  • ❌ 没有协议握手 —— initialize / capabilities 协商全自动。

函数的 docstring 会成为工具描述(description),直接进入模型的上下文——模型就是靠它判断"什么时候该调用这个工具"。因此写工具时要遵守两条纪律:

python
@mcp.tool()
def word_stats(path: str) -> dict:
    """统计一个文本文件的行数、单词数和字符数。

    当用户想了解某个文本文件的规模或长度信息时使用此工具。
    path 必须是本机可读的文件绝对路径。
    """
    from pathlib import Path

    text = Path(path).read_text(encoding="utf-8")
    lines = text.count("\n") + 1
    words = len(text.split())
    return {"lines": lines, "words": words, "chars": len(text)}

对比一下两种写法给模型的信息量:

docstring 写法模型的判断依据
"stats"几乎为零,可能永远不会被正确调用
一句话说明功能能判断"做什么",但不知道边界
功能 + 使用时机 + 参数约束(如上)三要素齐全,触发可靠

参数类型尽量简单

优先使用 strintfloatbool 和这些类型的列表/字典。复杂嵌套对象虽然支持,但 schema 越复杂,模型传参出错率越高。

13.4 再加两个实用工具:带默认值与错误处理

一个更贴近真实场景的工具集示例:

python
import os

from mcp.server import MCPServer

mcp = MCPServer("FileTools")


@mcp.tool()
def find_markdown(directory: str, keyword: str = "") -> list[str]:
    """在目录中查找 Markdown 文件,可按关键词过滤文件名。

    当用户想定位项目中的 .md 文档时使用。
    directory 为要搜索的目录路径;keyword 为文件名包含的关键字(可选)。
    """
    if not os.path.isdir(directory):
        # 把失败原因返回给模型,而不是抛出裸异常
        return [f"错误:目录不存在 {directory}"]

    results = []
    for root, _dirs, files in os.walk(directory):
        for f in files:
            if f.endswith(".md") and keyword.lower() in f.lower():
                results.append(os.path.join(root, f))
                if len(results) >= 20:  # 限制结果量,保护上下文
                    results.append("...(结果已截断)")
                    return results
    return results or ["未找到匹配文件"]


@mcp.tool()
async def disk_usage(path: str, ctx) -> str:
    """查看目录占用的磁盘空间摘要。

    需要读取系统状态时使用;path 为目标目录。
    """
    total = sum(
        f.stat().st_size
        for f in __import__("pathlib").Path(path).rglob("*")
        if f.is_file()
    )
    # ctx 是 SDK 注入的上下文对象,可以写日志反馈执行进度
    await ctx.info(f"已扫描 {path}")
    return f"{path} 共占用 {total / 1024:.1f} KB"

两个值得学习的细节:

  1. 把错误作为正常结果返回:目录不存在时返回一句说明而不是让服务器崩溃,模型拿到文字后能自行向用户解释或修正参数;
  2. 异步工具原生支持async def 直接可用,适合 IO 密集操作;ctx 参数由 SDK 自动注入(不进入模型可见的参数列表),用于日志与进度上报。

13.5 注册资源与提示模板

除了工具,MCP 还有两类服务端能力。继续在 server.py 里扩展:

python
# 资源(Resource):把数据以 URI 形式暴露给客户端读取
@mcp.resource("file://changelog")
def get_changelog() -> str:
    """返回项目的 CHANGELOG 内容。"""
    with open("CHANGELOG.md", encoding="utf-8") as f:
        return f.read()


# 提示模板(Prompt):预置的可复用对话起点,客户端用户主动选择
@mcp.prompt()
def review_commit(hash: str) -> str:
    """生成一次代码评审的起始提示。"""
    return f"""请帮我评审提交 {hash}

1. 先用 git show {hash} 查看改动内容;
2. 从正确性、可读性、测试覆盖三个角度逐条给出意见;
3. 最后给出「建议合入 / 需要修改」的结论。
"""

三者的分工要用一张表记清楚:

能力注册方式由谁触发典型用途
Tool 工具@mcp.tool()模型自主决策调用执行动作、查询数据
Resource 资源@mcp.resource(uri)应用/用户侧读取暴露文件、数据库行等上下文
Prompt 提示模板@mcp.prompt()用户主动选择固化高质量工作流入口

资源 URI 支持模板参数(如 greeting://{name}),客户端会先请求模板列表,再按实参取值。

13.6 用 MCP Inspector 系统化调试

mcp dev 打开的 Inspector 是调试 MCP 的标准工具,核心工作流:

bash
# 方式一:开发模式(自动重启 + Inspector)
mcp dev server.py

# 方式二:以 Streamable HTTP 传输运行(模拟线上形态)
mcp run server.py --transport streamable-http

在 Inspector 中依次验证:

  1. Connect —— 观察 initialize 握手与能力协商是否成功;
  2. Tools 标签页 —— List Tools 查看 schema 是否符合预期(重点看参数名与必填标记),再用表单调用来验证返回;
  3. Resources / Prompts 标签页 —— 分别确认资源可读、模板可展开;
  4. 故意传入错误参数(如不存在的路径),确认返回的是友好文本而非堆栈。

调试通过后,可以直接 mcp install server.py 把它装进 Claude Desktop,或者按下一章的手工配置方式接入任意客户端。

本章小结

  • 官方 Python SDK 安装命令是 pip install "mcp[cli]"[cli] 提供 mcp dev / run / install
  • 当前 API 为 MCPServer@mcp.tool() 定义工具、@mcp.resource(uri) 定义资源、@mcp.prompt() 定义提示模板;
  • 类型注解自动生成 JSON Schema,docstring 成为工具描述——两者共同决定模型的调用质量;
  • 错误应作为文本结果返回给模型,ctx 上下文对象可用于日志与进度;
  • Tool 由模型自主调用,Resource 由应用读取,Prompt 由用户选择,三者职责不同;
  • mcp dev + MCP Inspector 是标准调试闭环:握手 → 列工具 → 调用 → 异常路径。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 在官方 Python SDK 中,工具参数的 JSON Schema 来自哪里?

2. 关于工具的 docstring,下列说法正确的是?

3. Tool、Resource、Prompt 三者的触发者分别是?

4. 工具执行中出现"目录不存在"这类业务错误,最佳做法是?

🛠️ 动手实践

  1. 给本章的服务器再加一个 read_note(note_name) 工具:从固定目录读取指定的 .md 笔记并返回内容,注意处理"文件不存在"的情况。
  2. 注册一个模板资源 note://{name},与上一个工具形成"工具查、资源读"的组合,并在 Inspector 里分别验证两者。
  3. 写一个 @mcp.prompt() 模板 weekly_report(days: int),生成周报撰写提示,要求模板正文里对输出格式提出明确要求。

完成练习后,进入下一章:MCP 客户端集成实战,把这个服务器接入真实的 AI 客户端。