夜雨聆风学习资料网

ARTICLE · 1153927

MCP 凭什么统一 AI 工具生态?附手写 Server 教程

MCP 凭什么统一 AI 工具生态?附手写 Server 教程

点击上方「蓝字」关注我们,每周一篇 AI 工程实战

原创 · AI 工程笔记 · 系列第 3 篇 · 预计阅读 14 分钟

导读:前两篇我们讲了 Agent 的架构和评测。这一篇聊工具——Agent 的手脚。过去两年,这个领域最大的变化不是某个新工具,而是 MCP 这个协议:它把"N 个模型对接 M 个工具"的适配地狱,变成了一次性接入。这篇讲清它的设计逻辑,带你手写一个能跑的 Server,并解读 2026 年 7 月那次让协议彻底改变的大改版。

一、为什么需要 MCP:N×M 问题

假设你有 3 个 AI 应用(IDE 插件、内部 Agent、聊天客户端),要接 5 个工具(文件系统、数据库、GitHub、Jira、内部 API)。

没有统一协议时,你需要写 3 × 5 = 15 套适配代码。而且这 15 套里,鉴权方式、错误处理、参数格式各不相同,任何一方升级都要跟着改。

有 MCP 之后:每个应用实现一次 MCP 客户端(3 份),每个工具实现一次 MCP 服务端(5 份),总共 8 份。而且新加一个工具,3 个应用全都自动能用。

MCP 的价值就一句话:它是 AI 工具生态的 USB-C。工具方实现一次,所有支持 MCP 的宿主都能用;宿主实现一次,能接入整个生态。

这个类比不是修辞。USB-C 的意义不在于"接口更小",而在于它把接口的复杂度,从每对设备之间转移到了一个公共标准上。MCP 做的事一模一样。

二、三个角色:Host、Client、Server

讲 MCP 架构的图到处都是,但最常见的错误是把 Client 理解成"客户端软件"。实际不是。

· Host(宿主) 用户直接面对的那个 AI 应用。Claude Desktop、Cursor、你自己写的 Agent 运行时,都是 Host。它负责调模型、管上下文、渲染结果、做用户审批。

· Client(客户端) Host 内部的连接器。一个 Client 只连一个 Server,它是协议层的存在,不是一个独立软件。

· Server(服务端) 一个进程,把某类能力暴露出来。它可能是本地跑的子进程,也可能是远端 HTTP 服务。

关键点在这句:Host 里可以有多个 Client,一个 Client 对应一个 Server。所以你看到"Claude 连了 5 个 MCP Server",实际上是宿主里跑着 5 个 Client 实例。

还有一个容易被忽略的事实:MCP 是双向的。Server 不只是被动响应,Client 也能反过来给 Server 提供能力:

· Sampling Server 可以请求宿主帮它调一次大模型(Server 自己不持有模型凭证)

· Roots Client 告诉 Server"你的文件访问边界在这儿"

· Elicitation Server 需要额外信息时,可以请求宿主向用户提问

这三条是 MCP 和"普通 Function Calling"最本质的差别:它不是一组无状态的函数调用,而是一套有边界的双向协议。

三、三种原语:别把什么都做成 Tool

Server 对外暴露的能力分三类。这三个概念最容易混,但有一条极简的判断标准——看谁控制。

原语
谁控制
是什么
Tools
模型控制
可调用的操作,有 schema,可能有副作用
Resources
应用控制
URI 寻址的只读数据,由应用放进上下文
Prompts
用户控制
服务端预置的提示词模板,用户主动选

· Tools 是"做事情" 模型决定什么时候调,所以有副作用,所以需要审批和权限控制。

· Resources 是"给上下文" 注意控制权在应用手里,不在模型手里。这正是它的意义——数据进入上下文这件事应该由人决定,而不是模型自己伸手去拿。

· Prompts 是"工作流" 把"怎么用好这些工具"固化下来。生产环境里用得最少,但它是 Server 作者表达使用约定的地方。

两个常见的设计错误:

1. 把一切读取都做成 Tool。 因为 Function Calling 熟,习惯性什么都包成函数。但如果客户端只是需要一份文档、一个配置当上下文,用 Resource 更干净——而且不需要模型花一次调用去拿。

2. 把有副作用的操作藏在 Resource 后面。 Resource 语义是只读的,宿主不会对它做审批。你把"删除记录"包装成看起来无害的资源,等于绕过了所有安全设计。

判断准则一句话:Tools 是动作,Resources 是上下文,Prompts 是模板。按"谁来决定这件事发生"来选。

四、协议长什么样:JSON-RPC + 两种传输

MCP 的消息格式是 JSON-RPC 2.0——一个极简、成熟、所有语言都有现成库的 RPC 协议。这也是它推广快的原因之一:不用造新轮子。

传输方式
说明
stdio
Server 作为宿主子进程运行,走标准输入输出。最简单也最安全:不占端口、不出本机
Streamable HTTP
Server 独立部署,单个 HTTP 端点,POST 请求,可选 SSE 流式返回

选择逻辑很直接:Server 跑在用户机器上 → stdio;Server 要部署成服务 → Streamable HTTP。

早期版本(2024-11)用的是 HTTP + SSE 双端点方案,已经被 Streamable HTTP 取代,新项目不用再考虑。

注意倒数第二步——MCP 工具最终还是要被翻译成模型原生的 Function Calling 格式。MCP 标准化的不是"模型怎么调工具",而是"工具怎么被描述和调用"。这条边界想清楚,很多困惑就没有了。

五、2026 年 7 月:MCP 变成无状态了

这是协议发布以来最大的一次改版,也是这篇最需要更新的部分。如果你看的还是老教程,这一节请重点看。

改版前(截至 2025-11-25 版本):连接要先握手。客户端发 initialize 协商版本和能力,服务端回 initialized,然后产生一个 Mcp-Session-Id,后续所有请求都要带上它。

改版后(2026-07-28 版本):握手没了,Session ID 也没了。每个请求自带它需要的全部上下文。

变化
内容
握手取消
不再有 initialize / notifications/initialized
请求自包含
每个请求在 _meta 里带协议版本和客户端能力
能力发现
新增可选的 server/discover 方法,想先探查再调用可以用
列表可缓存
tools/list 等结果带 ttlMs 和 cacheScope,客户端可放心缓存
网关友好
新增 Mcp-Method / Mcp-Name 请求头,不用解析 body 就能按工具限流
通知统一
变更通知收拢到 subscriptions/listen 单条流
Tasks 转扩展
长任务机制从核心移到官方扩展,重新设计过

为什么这件事重要?一句话:无状态意味着它可以躺在任何普通负载均衡后面。

以前你要为 MCP 服务端做 sticky session——用户第一次请求打到 A 机器,后续必须还打 A 机器,否则会话状态就丢了。这在云原生环境里是个很别扭的要求。现在任何实例都能处理任何请求,扩容、滚动更新、灰度发布,全都变成标准操作。

对服务端作者的实际影响:别再在连接上存状态。如果你的工具需要在多次调用之间记住东西(比如"先开一个事务,后面几步都在里面"),正确做法是服务端生成一个 handle 返回给客户端,客户端下次把它当普通参数传回来,而不是挂在连接上。

兼容性不用担心:老版本客户端仍然能用,官方 SDK 也支持自动探测和回退。但如果你在写新 Server,按 2026-07-28 版本设计是不会错的。

六、动手:写一个能跑的最小 Server

用官方 Python SDK(pip install mcp),核心代码不到 30 行:

from mcp.server.fastmcp import FastMCP import json  mcp = FastMCP("demo-server")  @mcp.tool() def get_weather(city: str) -> str:     """查询指定城市的天气。      Args:         city: 城市名,例如 "杭州"     """     return fetch_weather_api(city)  @mcp.resource("config://app") def app_config() -> str:     """当前应用的配置,供模型作为上下文读取"""     return json.dumps(load_config(), ensure_ascii=False)  if __name__ == "__main__":     mcp.run()          # 默认 stdio 传输

就这么多。装饰器把普通函数注册成 Tool / Resource,函数签名自动变成参数 schema,docstring 自动变成工具描述。

两点提醒:

① docstring 不是注释,是 prompt。 模型只能靠这段文字判断什么时候该调这个工具。写清楚"做什么、什么场景用、参数什么含义",效果差别很大。

② 跑起来只是第一步,还得注册到宿主。

{   "mcpServers": {     "demo": {       "command": "python",       "args": ["/path/to/server.py"]     }   } }

重启宿主,工具就出现在列表里了。要部署成远端服务,mcp.run(transport="streamable-http") 换个传输方式即可。

七、五条设计经验

01 工具粒度:一个工具干一件完整的事。 太细(read_file + parse_json + filter)会让模型在编排上消耗大量步数;太粗(do_everything)会让参数变成一坨没法校验的 JSON。按"用户意图"切分,不是按"代码函数"切分。

02 描述即提示词。 模型看不见你的实现,只看得见 name 和 description。把边界条件写进描述里——"仅支持 30 天内的订单"这类约束不写,模型就会瞎调。

03 错误要能纠正。 返回"city 必须是 北京/上海/杭州 之一"远好过"Invalid input"。错误信息是给模型看的,不是给日志看的。

04 权限最小化。 Server 是带着你给的凭证在跑。一个只读报表工具不需要写权限,一个查天气的 Server 不需要你的文件系统。凭证能分开就分开,能只读就别给写。

05 装 Server 要像加依赖,不像装 App。 恶意工具描述、仿冒包、过宽的权限范围,都是真实存在的攻击面。接入前看一眼它的代码或来源,比事后排查便宜得多。

八、什么时候不该用 MCP

说了这么多好处,也得说反面。这几种情况,MCP 是过度设计:

· 你只有一两个自己家的工具,而且工具和 Agent 是同一个团队、同一个生命周期一起演进——直接写原生 Function Calling 更省事,少一层协议成本。

· 延迟敏感路径。多一层进程间通信和协议转换,就是多一份延迟。

· 运行时已深度绑定某套原生工具模型,迁移成本远大于收益。

MCP 解决的是多对多的集成复杂度问题。如果你的世界本来就是一对一,它带来的收益很有限。

写在最后

MCP 值得学,不是因为它是当下的热点,而是因为它的位置足够底层。

模型几个月换一茬,客户端来来去去,工具背后的服务也在不断变。而协议作为中间层,让这些东西可以各自演进、互不牵制。这也是为什么 2026 年 7 月那次改版、以及更早把它捐给 Linux 基金会,都是同一件事的两面:它在往基础设施的方向走,而不是某个厂商的功能。

工具描述怎么写、权限边界画在哪、什么时候该把能力拆开——这些判断,比记住协议版本号是哪天发布的要重要得多。


下一篇,我们聊聊 Agent 的上下文工程:为什么窗口越做越大,Agent 反而更容易犯迷糊。


如果这篇对你有帮助,点个「赞」和「在看」
转给正在折腾 Agent 的朋友 👇

关注我们,每周一篇 AI 工程实战
让每一次技术投入,都算数

相关学习资料