MCP(Model Context Protocol)解决的问题很直白:以前每个 AI 应用都要为每个工具、每个数据源单独写一遍集成,N 个应用乘 M 个工具,写 N×M 次。MCP 把这层集成抽成一个标准,让一个服务端写好之后,任何支持 MCP 的客户端都能直接用,把 N×M 变成 N+M。它底层是 JSON-RPC 2.0,客户端—服务端架构,是 Anthropic 主导、多家厂商一起推的开放协议。

这篇文章不重复协议规范,只讲一件事:从零写一个 MCP 服务端,让它从"能跑"进化到"能上生产",中间会踩哪些坑。

一个服务端的最小形态

写 MCP 服务端不需要手撸 JSON-RPC。官方 Python SDK 里有个 FastMCP,用装饰器把普通函数变成工具,函数签名就是工具的输入 schema,docstring 就是工具的说明。最小形态不到二十行:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("inventory")

@mcp.tool()
def query_stock(sku: str) -> str:
    """按 SKU 查库存余量,返回 JSON 字符串。"""
    return '{"sku": "%s", "stock": 42}' % sku

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

这里有两个细节比代码本身重要。第一,sku: str 这个类型标注会被转成 JSON Schema,客户端据此生成参数校验,不用单独维护一份 schema。第二,docstring 是写给模型看的——模型靠它判断"什么时候该调这个工具",写"查询库存"和写"按 SKU 查库存余量,返回 JSON 字符串"是两种完全不同的路由效果。

三个原语

一个 MCP 服务端能暴露三类东西,别只盯着工具:

Tools 是模型能调用的动作,比如查库存、发邮件、跑一段 SQL。Resources 是只读的数据,模型可以拉进来当上下文,比如一份配置文件、一段接口文档。Prompts 是可复用的提示词模板,由用户显式触发,模型不会自己决定去调用它。

很多人把服务端写成一堆工具,但真正省 token 的场景往往是 Resources——把一份几百行的内部规范作为 resource 暴露出去,模型按需读取,比每次把它拼进 system prompt 便宜得多。三者的边界:会改变状态的是 Tool,只读上下文是 Resource,固定话术是 Prompt。

两种传输

MCP 有两种标准传输。stdio 是本地子进程模式,客户端把服务端当成一个子进程拉起来,通过标准输入输出通信,适合本地开发工具。Streamable HTTP 是远程模式,服务端跑成一个 HTTP 端点,适合团队共享或云上的服务。

本地工具(Claude Desktop、Cursor 这类)大多用 stdio,配置里写的是"启动命令",不是 URL:

{
  "mcpServers": {
    "inventory": {
      "command": "python",
      "args": ["/absolute/path/to/inventory_server.py"]
    }
  }
}

注意 args 里的路径必须是绝对路径。客户端启动服务端时的工作目录是它自己的,写相对路径十有八九找不到文件。这个坑很小,但几乎每个第一次写的人都会掉进去一次。

生产级的几个坎

最小形态跑通之后,往生产推,有四个坎是绕不过去的。

第一个是 stdout 污染协议。 stdio 传输下,标准输出就是协议通道,任何一行 print 都会破坏 JSON-RPC 报文。日志必须走 stderr:

import sys
print("debug: fetching stock", file=sys.stderr)

第二个是超时。 模型等一个工具的时候,服务端一直不返回,整个会话就卡住。长任务要么设上限,要么走进度通知。简单场景下,给阻塞调用加超时是最低限度的保护:

import asyncio

@mcp.tool()
async def slow_report(region: str) -> str:
    try:
        return await asyncio.wait_for(build_report(region), timeout=15)
    except asyncio.TimeoutError:
        return '{"error": "report timeout"}'

第三个是输入不可信。 工具的入参是模型生成的,等于不可信输入。一个暴露了 SQL 或 shell 执行能力的工具,参数没校验就等于把执行权交给了模型。最少要做两层:按 schema 校验类型和范围,再对高危操作做权限收敛。有破坏性的操作不要做成静默执行,要么要求显式确认,要么干脆不暴露。

第四个是工具列表会变。 服务端支持热加载插件时,工具集可能中途变化,它会发一个 notifications/tools/list_changed 通知。客户端如果缓存了工具列表又不监听这个通知,就会拿到一份过期的列表,调一个已经不存在的方法。

值不值得用

如果你的 AI 能力只有一个应用、三五个工具,直接用函数调用(function calling)就够了,上 MCP 是多余的一层。MCP 的价值在规模上来之后才显现:同一批工具要被多个应用、多个 agent 复用,或者你希望工具实现和应用解耦、可以独立替换。判断标准很简单——如果你已经第二次为了一个新应用重写同一个 API 的集成,就该考虑把那个集成抽成一个 MCP 服务端了。

服务端的边界也要收敛。一个服务端只干一个领域的事(数据库一个、外部 API 一个、文件一个),每个里面放几个命名清晰、描述准确的工具,比一个什么都塞的"超级服务端"好维护、也好做权限控制。