MCP 真正解决的不是再发明一个协议,而是把工具接入做成通用插座

上一篇结尾留了个问题:每做一个 Agent 项目,都在重复造工具。这个项目写一遍查数据库的工具,下个项目又写一遍;你给自己的 Agent 写的搜索工具,没法直接拿到别人的应用里用。工具和应用绑死了。

MCP 要解决的就是这件事。这是进阶系列的最后一篇,我们先把 MCP 和你已经熟悉的 Function Calling 摆在一起讲清楚关系,然后动手写一个真正能跑的 Server。

MCP 和 Function Calling 不是二选一

很多人第一次听到 MCP,会以为它要取代 Function Calling。不是的,它俩在不同的层。

回忆一下 Function Calling 解决的是什么——它解决的是"模型怎么表达一个调用意图"。但它没解决工具的复用问题:工具的 schema、实现,仍然是你在每个应用里各写各的,和这个应用焊死。

MCP(Model Context Protocol)是 Anthropic 提出的一个开放协议,它往上加了一层标准化。打个比方:Function Calling 让模型学会了"说出我要调某个函数",而 MCP 是定义了一种统一的插座标准——你把工具实现成一个独立的 Server,任何支持 MCP 的客户端,不管是 Claude Desktop、某个 IDE,还是你自己写的 Agent,都能即插即用地连上来用它的工具。工具一次实现,到处可用。

所以它俩是协作关系:MCP Server 对外暴露的那些工具,最终被模型调用时,走的还是 Function Calling 那套机制。MCP 标准化的是工具怎么被发现、被描述、被连接这一层,底下的调用意图表达,依然是 Function Calling。

具体说,一个 MCP Server 可以暴露三类东西(协议里叫 primitives):Tools 是可执行的函数,会产生动作或副作用,对应你熟悉的工具调用;Resources 是可读取的数据,类似 HTTP 的 GET,比如一个文件、一段数据库记录,由应用决定何时读取;Prompts 是预设的提示词模板,供用户主动调用。架构上是 Host(如 Claude Desktop)通过内置的 Client 连接若干 Server,一个 Host 可以同时连多个 Server。传输方式有两种:本地 Server 用 stdio(把 Server 作为子进程,通过标准输入输出通信),远程 Server 用 HTTP。

从零写一个 MCP Server

讲再多不如写一个。用官方的 Python SDK,里面的 FastMCP 把样板代码都包好了,写起来和定义普通函数差不多。

from mcp.server.fastmcp import FastMCP
import httpx

mcp = FastMCP("notes-and-weather")


@mcp.tool()
def search_notes(keyword: str, limit: int = 5) -> list[dict]:
    """在本地笔记库中按关键词搜索笔记。

    用于用户想查找自己记录过的内容时。
    keyword 是要搜索的关键词,limit 是返回的最大条数。
    """
    rows = db.query("SELECT title, body FROM notes WHERE body LIKE ?",
                     (f"%{keyword}%",))
    return [{"title": r.title, "excerpt": r.body[:200]} for r in rows[:limit]]


@mcp.tool()
async def get_weather(city: str) -> dict:
    """查询指定城市的实时天气。city 需为城市的拼音或英文名。"""
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://api.example.com/weather",
                                 params={"city": city}, timeout=10)
        return resp.json()


@mcp.resource("notes://recent")
def recent_notes() -> str:
    """最近修改的 10 条笔记,作为可读资源供客户端按需加载。"""
    rows = db.query("SELECT title FROM notes ORDER BY updated_at DESC LIMIT 10")
    return "\n".join(r.title for r in rows)


if __name__ == "__main__":
    mcp.run(transport="stdio")

这里有个细节值得停一下:你完全没有手写任何 JSON Schema。FastMCP 直接从你的类型注解和 docstring 自动生成工具的 schema——参数类型来自注解,工具描述来自 docstring。这正好呼应了第五篇讲工具设计时强调的:描述是模型选对工具的唯一依据。在 MCP 里,你函数的 docstring 就是那段决定成败的描述,所以别偷懒,把"什么时候用、参数什么含义"写清楚。

写完怎么接入。以 Claude Desktop 为例,在它的配置文件里加上这个 Server:

{
  "mcpServers": {
    "notes-and-weather": {
      "command": "python",
      "args": ["/绝对路径/server.py"]
    }
  }
}

重启客户端,它就会把这个 Server 作为子进程拉起来,工具自动出现在可用列表里。注意路径要写绝对路径,相对路径在客户端的工作目录下经常找不到。

什么该做成 MCP,什么不该

能写 Server 之后,更重要的判断是:什么该做成 MCP,什么不该。把所有工具无脑都包成 MCP Server,是另一种过度工程。

适合做成 MCP 的,是那些需要被复用的能力:会被多个项目、多个客户端共用的工具;通用而稳定的能力,比如查公司内部知识库、访问某个标准数据源;需要跨团队共享的东西。这些场景下,MCP 那层标准化的接入开销,换来的是"一次实现处处可用",很值。

不适合的也很明确。一个逻辑如果只在你单个应用内部用一次,那直接用 Function Calling 写个工具就好,再套一层 MCP 协议、多一个进程、多一道通信,纯属负担。高频、对延迟极敏感、和主程序紧耦合的内部调用,也不该跨进程走 MCP。带复杂事务、复杂状态的逻辑,做成无状态的工具接口本就别扭,硬塞进 MCP 只会更别扭。一句话:MCP 的价值在复用边界上,没有跨边界复用的需求,就别引入它

还有个常被忽略的安全问题必须摆出来。MCP Server 跑的是真实代码——当你安装并连接一个第三方的 MCP Server,等于在你自己的机器上执行别人写的代码,这是实打实的供应链风险。所以来路不明的 Server 要审代码,密钥和权限要按最小化原则给。另外,工具的返回值会进入模型上下文,恶意的返回内容可能携带 prompt injection,这一点在第二篇讲过,在 MCP 场景下同样要防。

调试:先记住别往 stdout 打日志

最后说调试,先讲一个几乎人人都会踩的坑:stdio 传输的 Server,绝对不能用 print 往标准输出打日志。原因很简单——stdout 这条通道已经被 MCP 协议本身占用了,是 Server 和 Client 通信的管道。你往里 print 一行调试信息,就等于往协议流里塞了一段垃圾,Client 解析直接乱套。日志要打到 stderr,或者写文件。

调试 MCP Server 的官方利器是 MCP Inspector。它是一个可视化工具,能直接连上你的 Server,把暴露的 tools、resources、prompts 全列出来,你可以手动填参数调用、看真实返回,不需要先接进某个客户端再绕一大圈。开发阶段先用 Inspector 把每个工具单独验通,再去接客户端,能省掉大量"到底是 Server 错了还是接入配置错了"的纠结。

测试上分两个层次。单元测试很轻松——@mcp.tool() 装饰的函数本质还是普通函数,直接像测普通函数那样导入、调用、断言就行,工具逻辑和 MCP 协议是解耦的。集成测试则用 MCP 的 client SDK 真正连上 Server,走一遍完整的握手、列举、调用流程,确认 schema 生成正确、传输正常。常见的集成问题就那么几类:类型注解不规范导致 schema 生成不对、路径写成了相对路径、Server 依赖的环境变量没传进子进程——排查时优先看这几处。

到这里,进阶系列七篇就走完了。回头看这条线索其实很清晰:第一篇把 API 调用调到能上线,第二篇把 Prompt 做成可评估的工程,第三、四篇把检索从能跑通做到能答准,第五篇让模型学会用工具,第六篇把工具、记忆、控制流组织成 Agent,到这一篇,又把工具的接入本身标准化成了可复用的 MCP Server。从"调通一个 API",到"搭起一个能复用、能协作、能维护的系统"——这中间的每一步,都不是玄学,而是一件件可以拆开、可以度量、可以做扎实的工程。

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:MCP:自己动手写一个 Server

本文链接:https://www.sshipanoo.com/blog/ai/llm-advanced/07-MCP自己动手写一个Server/

本文最后一次更新为 天前,文章中的某些内容可能已过时!