DAY 18 / PHASE 2 · 应用与系统

MCP

三层原语 · stdio vs HTTP · 自建 Server · Context 税

2026-06-01 · BigCat

MCP 不是又一个 function calling 包装——它把 N×M 集成问题压成 N+M。

// WHY THIS MATTERS

你已经会写 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 是什么,直接讲怎么工程化用它、怎么避坑。

// 01

三层原语:不是命名,是控制权

论断:把一个能力放进 Tool / Resource / Prompt,决定的是「谁来发起调用」——这是 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,是权限边界。

控制轴:谁发起这次调用? MODEL 决定 ──────────▶ TOOLS (动作 / 有副作用) 例:create_issue, send_email APP 决定 ────────────▶ RESOURCES (只读 / 进 context) 例:file://config.yaml, db://schema USER 决定 ───────────▶ PROMPTS (模板 / slash command) 例:/code-review, /summarize-pr

实战示例

# 设计 server 前先归类——贴在 server 文件顶部
# 这个能力会改变外部状态吗?      → 是 → Tool
# 它只是「让模型看到」某些数据吗?  → 是 → Resource
# 它需要用户主动点一下才发生吗?    → 是 → Prompt
# 拿不准:默认 Resource(最便宜,不污染 tool 列表)
失败模式:把只读数据暴露成 Tool——模型要先「想到」去调它,常常忘了调,或在不需要时乱调,污染 selection。反过来,把有副作用的动作做成 Resource,host 可能在用户没授权时就自动注入并触发。控制轴选错,权限模型也跟着错。
进阶资源:Anthropic · Introducing MCP(2024-11 发布,含三层原语动机) · MCP Spec · Prompts
// 02

30 行自建 Server:docstring 就是 prompt

论断:MCP server 的 tool 描述会原样进模型 context——写 server 的那一刻你在写 prompt,不是写 API 文档。

背景与原理

官方 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 或用 pydanticField 补描述。换句话说,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")
失败模式:docstring 写成「Get forecast」这种给人看的简短描述——模型不知道 city 要英文、days 范围,就会传 'Tokyo, Japan' 或 days=30 报错。tool 数量也别贪多:一个 server 塞 30 个 tool,selection 准确率会塌(同 Day 4 的退化曲线)。
进阶资源:modelcontextprotocol/python-sdk(FastMCP quickstart) · modelcontextprotocol.io(官方文档 + SDK 列表)
// 03

stdio vs Streamable HTTP:transport 决定进程模型

论断:transport 不是部署细节——它决定 server 是本地子进程还是远程服务、能否多租户、认证边界在哪。

背景与原理

MCP 用 JSON-RPC 2.0,跑在两种标准 transport 上:

两种 transport 之上跑的是同一套协议生命周期:连接后先 initialize 握手——client 和 server 交换协议版本,并协商各自支持的能力(server 声明有没有 tools / resources / prompts、支不支持 list 动态变更通知)。这意味着接同一个 server,不同 client 看到的能力面可能不同;也意味着你的 server 要老实声明 capability,否则 client 不会去拉对应的 list。能力协商是 MCP「N+M 复用」得以成立的根:client 不需要预知 server 长什么样,握手时问一次即可。

stdio Streamable HTTP ┌────────┐ ┌────────┐ client ─┤subprocess├ stdin/stdout client ─┤ POST/GET├─▶ https://host/mcp └────────┘ └────────┘ (可选 SSE 流) 本地 · 1:1 · 无 auth 远程 · 多租户 · 需 auth 延迟最低 · 进程随 client 可独立扩缩 · 跨网络

实战示例

// 本地 stdio:Claude Desktop / Claude Code 配置
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["weather_server.py"]
    }
  }
}
失败模式(经典 bug):stdio 模式下,任何写到 stdout 的东西都会被当成 JSON-RPC 消息。一行 print("debug") 或库的进度条就能静默损坏整条协议流,client 报「invalid JSON」却查不到源头。铁律:stdio server 的日志一律走 stderrlogging 默认即 stderr,但别手滑 print)。
进阶资源:MCP Spec · Transports(stdio / Streamable HTTP 规范,含 SSE 弃用说明)
// 04

Context 税:MCP 最大的 production 失败模式

论断:多数 client 把所有 server 的全部 tool 定义在开场就灌进 context——接 10 个 server,用户还没说话就烧掉几千 token。

背景与原理

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
失败模式(反直觉):「多接 server = agent 更强」是错的。每多接一个 server,开场 token、选择难度、延迟都涨,而模型在 30+ tool 里反而更容易选错。更多工具到某个点后是净负——MCP 的可组合性诱导你过度连接。
进阶资源:Anthropic · Code execution with MCP(context 税量化 + progressive disclosure) · Anthropic · Advanced tool use

// ENGLISH GLOSSARY

MCP (Model Context Protocol)
连接 AI 应用与外部数据/工具的开放协议,Anthropic 2024-11 开源
Host / Client / Server
Host = AI 应用(如 Claude Desktop);Client = host 内连接单个 server 的连接器;Server = 暴露能力的进程
Tool (model-controlled)
模型自主决定调用的动作,通常有副作用
Resource (application-controlled)
由 client 决定注入 context 的只读数据
Prompt (user-controlled)
用户显式触发的可复用模板,常渲染为 slash command
Transport
承载 JSON-RPC 消息的通道:stdio 或 Streamable HTTP
stdio transport
把 server 作为子进程,经 stdin/stdout 通信,本地 1:1
Streamable HTTP
单端点远程 transport,2025-03 起取代弃用的 HTTP+SSE
JSON-RPC 2.0
MCP 底层的远程过程调用消息格式
Progressive disclosure
按需暴露 tool 定义而非全部前置加载,省 context

// 深入思考

MCP 和直接写 function calling 比,什么时候 MCP 是过度工程?
当集成是一次性、单 client、不复用时,MCP 的协议开销(起进程、JSON-RPC 往返、schema 协商)纯属负担——直接在代码里 def 一个函数当 tool 更快。MCP 的 ROI 来自 N+M 复用:你有多个 host(Claude Desktop + Cursor + 自建 agent)要共享同一批能力,或要把能力分发给别人用。单 agent、单脚本、工具固定的场景,function calling 足够。判据:这个能力会被第二个 client 复用吗?不会 → 别上 MCP。
三层原语里 Resource 最被忽视,为什么几乎所有 server 只暴露 Tool?
两个原因:一是 Tool 是 model-controlled,「模型自己会调」符合直觉,开发者不用想注入时机;Resource 是 application-controlled,要 host 主动决定何时塞进 context,但很多 host 的 Resource 支持比 Tool 弱、UI 也不完善。二是把数据做成 Tool「能跑」,开发者就不深究——代价是每次读数据都占一次模型决策、污染 selection。这是协议设计与 host 实现成熟度的错位:规范鼓励三层,生态实际偏向 Tool。
code execution 让 tool 不进 context——这是否意味着 MCP 的「tool 列表」抽象本身要被取代?
不是取代,是分层。MCP 依然是能力的发现与描述层(server 声明有什么),但「全部前置加载进 context」只是 client 的一种朴素实现。code execution 把 MCP server 投射成文件系统/代码模块,agent 按需 import——协议没变,变的是 client 怎么把协议暴露给模型。趋势是 context 里只放入口(如何发现工具),真正的 schema 按需拉取。可以预期未来 client 默认走 progressive disclosure,而非一次灌满。
MCP server 是 untrusted code 与 untrusted 数据的双重入口,怎么 defense in depth?
两类风险:server 本身恶意(tool poisoning:描述里藏注入指令骗模型),和 server 返回的数据携带 indirect prompt injection。防御要分层:1) 只接信任来源的 server,本地优先 stdio(无网络面);2) host 层对敏感动作做 permission gate,不靠模型自觉;3) 把 server 返回值当 untrusted content 隔离,别让它直接改写系统指令;4) 远程 HTTP server 加 auth + 最小权限 scope。这正接 Day 24 Prompt Injection——MCP 扩大了攻击面,隔离与权限不能省。
stdio 的 1:1 进程模型 vs HTTP 的多租户,本地个人 agent 未来走哪条?
短期本地 agent 仍偏 stdio:零网络面、零认证、延迟最低,符合「个人工具链」隐私诉求。但当你想在手机、笔电、云端共享同一套能力与状态时,stdio 的进程绑定就成了枷锁——这时 Streamable HTTP + 个人 gateway(见 Day 23 Personal AI Infra)更合适。可能的折中:本地 stdio 跑高频私密工具,远程 HTTP 跑需要跨设备/共享状态的工具,host 同时连两类。transport 选择本质是隐私-可达性的权衡。

// 延伸阅读