MCP 不是又一个 function calling 包装——它把 N×M 集成问题压成 N+M。
你已经会写 tool schema、会接 API。MCP 的工程价值不在「又能调工具了」,而在它把集成的拓扑结构改了:以前每个 model × 每个数据源要写一份胶水代码(N×M),MCP 让 server 写一次、所有 client 复用(N+M)。但真正决定 MCP 用得好不好的,是三个被多数人忽略的工程点:三层原语的控制轴(什么该放 Tool、什么该放 Resource)、transport 的进程模型(stdio 的一个隐蔽 bug 能让整条 JSON-RPC 流静默损坏)、以及 MCP 最大的 production 失败模式——tool-definition 的 context 税(接 10 个 server 还没开口就烧掉几千 token)。这一期假设你懂 MCP 是什么,直接讲怎么工程化用它、怎么避坑。
MCP server 能暴露三种原语,区别不在功能而在控制轴:
多数人把一切都做成 Tool,于是把「读一份配置文件」也变成一次模型 tool call——既浪费一次决策,又给 tool selection 增加噪声。正确做法:只读数据 → Resource,有副作用的动作 → Tool,需要用户显式发起的工作流 → Prompt。
这条控制轴同时是信任轴:model-controlled 的 Tool 风险最高(模型可能在你没预期时触发副作用),所以 host 普遍对 Tool 加 permission gate;application-controlled 的 Resource 由 host 策展,注入什么、何时注入都可控;user-controlled 的 Prompt 由人主动发起,信任度最高。把动作错放成 Resource,等于绕过了本该有的审批;把数据错放成 Tool,等于把一个本来零风险的读操作丢进了需要审批的高风险通道。原语选择不只是 UX,是权限边界。
# 设计 server 前先归类——贴在 server 文件顶部
# 这个能力会改变外部状态吗? → 是 → Tool
# 它只是「让模型看到」某些数据吗? → 是 → Resource
# 它需要用户主动点一下才发生吗? → 是 → Prompt
# 拿不准:默认 Resource(最便宜,不污染 tool 列表)
官方 Python SDK 的 FastMCP 把 JSON-RPC、schema 生成、transport 全自动化,你只写三个装饰器。关键工程点和 Day 4 Tool Use 一脉相承:参数描述 > 参数名字,docstring 是给模型读的不是给人读的。type hint 会自动转成 JSON Schema,docstring 会自动变成 tool description——这两样直接决定模型选不选对、传不传对参数。
自动化的边界要心里有数:简单 type hint(str / int / list[str])转 schema 很干净,但复杂参数光靠类型名表达不了约束(取值范围、格式、互斥关系),这些必须写进 docstring 或用 pydantic 的 Field 补描述。换句话说,schema 自动生成省的是样板代码,省不掉「把约束讲清楚给模型」这件认知工作——后者恰恰是 server 质量的分水岭。一个把每个参数的边界、单位、示例都写进描述的 server,和一个只有类型名的 server,模型调用成功率能差出一截。
# pip install "mcp[cli]"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""查询某城市未来天气预报。
Args:
city: 城市名,用英文,如 'Tokyo'
days: 预报天数,1-7,默认 3
"""
return _call_weather_api(city, days)
@mcp.resource("config://units")
def units() -> str:
"""当前温度单位偏好(只读,自动进 context)"""
return "celsius"
@mcp.prompt()
def trip_brief(city: str) -> str:
"""生成出行天气简报(用户触发的模板)"""
return f"用一句话总结 {city} 适不适合户外活动。"
if __name__ == "__main__":
mcp.run(transport="stdio")
MCP 用 JSON-RPC 2.0,跑在两种标准 transport 上:
/mcp 端点,同时接 POST 和 GET,可选用 SSE 流式推送多条消息。可扩展、可多租户,但要自己处理 auth。注意:旧的 HTTP+SSE 双端点 transport 已在 2025-03 弃用,新项目直接用 Streamable HTTP。⚠️ 路线图注(2026-08):官方已明确要让 transport 继续向 HTTP 收敛——本地 server 也统一成「在 stdio 上跑 Streamable HTTP」,而不是长期维护两套。别把设计绑死在 stdio 专属路径上,详见 Day 60。
两种 transport 之上跑的是同一套协议生命周期:连接后先 initialize 握手——client 和 server 交换协议版本,并协商各自支持的能力(server 声明有没有 tools / resources / prompts、支不支持 list 动态变更通知)。这意味着接同一个 server,不同 client 看到的能力面可能不同;也意味着你的 server 要老实声明 capability,否则 client 不会去拉对应的 list。能力协商是 MCP「N+M 复用」得以成立的根:client 不需要预知 server 长什么样,握手时问一次即可。
⚠️ 已被改写(2026-07-28 规范):上面这套 initialize 握手与会话语义已在 2026-07-28 规范中删除——身份与能力改为每请求走 _meta,能力发现变成可选的 server/discover。现行无状态协议与迁移清单见 Day 60 · MCP 无状态化与迁移。
// 本地 stdio:Claude Desktop / Claude Code 配置
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["weather_server.py"]
}
}
}
print("debug") 或库的进度条就能静默损坏整条协议流,client 报「invalid JSON」却查不到源头。铁律:stdio server 的日志一律走 stderr(logging 默认即 stderr,但别手滑 print)。MCP 的便利有隐藏成本,且这个成本是双重的。其一是前置加载:client 通常在开场就把每个连接 server 的全部 tool schema 灌进 context,这是固定开销,与用户问什么无关。其二更隐蔽——中间结果累积:agentic loop 里每次 tool 调用的返回值都留在 context 供下一轮参考,调一次大返回的工具就把几千行 JSON 永久钉进后续每一轮的输入。两者叠加:tool 越多开场越贵,调用越多过程越胀,到长任务后期 context 塞满了早已用不上的 schema 和中间数据,既烧钱又稀释模型注意力(回到 Day 2 lost-in-the-middle)。Anthropic 2025-11 的工程博客 Code execution with MCP 给了一个反直觉的解法:把 MCP server 暴露成代码 API,让 agent 写代码去调,按需 import 用到的工具、在执行环境里处理中间数据再回传。他们报告一个原本约 150k token 的工作流降到约 2k token(约 98.7% 削减)。核心 insight:tool 目录不该常驻 context,应该 progressive disclosure(按需发现)。
# 不做 code execution 也能立刻省的三招:
# 1. 只接当前任务需要的 server,用完即关
# (Claude Code: 别把全部 MCP 写进全局 settings)
# 2. 一个 server 别堆几十个 tool——按职责拆,或合并同类
# 3. 大返回值在 server 端先裁剪/摘要,别让原始 JSON
# 几千行直接穿过 context