如果你最近在做AI应用集成,或者只是随手配置过几款带智能功能的开发工具,大概率已经碰到过“MCP”这个词。它不是什么产品代号,而是模型上下文协议(Model Context Protocol)的缩写,一个针对大模型与外部数据、工具之间连接方式而定制的开放协议。通俗点说,MCP试图让AI应用像插USB设备一样去对接外部能力——插上就能用,换一个也基本是同样的连接方式。这篇文章是我基于自学文档整理的MCP协议详解,会从“这个协议到底在解决什么问题”讲起,逐步拆解架构、讲解实操步骤,最后把我实际踩过的坑和排查方法整理出来。适合正在做AI应用开发、工具链集成,或者对大模型落地技术方案感兴趣的开发者阅读。
1. MCP协议到底解决的是什么问题
1.1 大模型应用集成的混乱现状
在大模型应用里,最常面对的问题不是“模型不够聪明”,而是“模型够不着数据”。比如我想让模型帮我查一下今天某城市的实时天气、读一个本地文档的最新版本、或者在内部系统里创建一条工单。这些能力模型本身并不直接拥有,通常的做法是为每一个外部能力单独写一段Function Calling描述,再单独写一层调用逻辑。工具数量少时,这套流程勉强能跑通;工具超过三四个,维护成本就会开始失控。不同的外部系统有各自的参数规则、鉴权方式、返回格式,每接入一个新系统,都要重新写一遍适配层,而且这套适配只能服务当前这一个AI应用。换个应用,全部重来。
其实这有点像家里客厅堆满了遥控器的场景:电视一个遥控器、空调一个遥控器、投影仪还有一个遥控器。每个遥控器都长得不一样,操作逻辑也各不相同。MCP这个协议,做的就是“统一遥控器”的事情:双方约定一套通用的消息格式和操作方法,任何外部能力只要实现了这套标准,AI应用就能用同样的方式连接它。这也是为什么我在这篇自学文档里把MCP放在工具箱优先级最高的位置——它不是锦上添花的框架,而是解决AI应用连接层混乱的基础设施。
1.2 协议想解决的三个核心问题
第一是标准化。在MCP之前,不同AI应用调用外部工具的方式各不相同,哪怕调用的工具功能一模一样,接口都可能完全不同。MCP把协议内容固定下来,客户端和服务端都遵循统一定义,描述工具、声明参数、返回结果都有了标准表达。标准化的价值在你需要同时接入多个AI应用时体现得尤其明显:服务端只需要实现一次MCP,就能被所有支持MCP的客户端复用,不用每个场景单独写一套API。
第二是动态发现。传统方法里,AI应用想调用哪些工具,通常在开发阶段就写死在代码里。MCP引入了动态发现机制,客户端连上服务端后,可以直接问“你到底有哪些能力”,服务端把自己的工具列表、数据资源列表一次性返回。这样新增一个工具后,只要服务端发布新版本,客户端无需改动代码就能感知到。这一点对于长期维护的项目帮助极大,我见过太多因为工具数量膨胀导致配置文件失控的案例,MCP天然规避了这个问题。
第三是双向的、可扩展的连接。MCP不只是让AI应用去调用外部工具,它同时支持服务端主动推送资源更新、通知业务事件。这意味着它不只是“远程函数调用”,更像一条持久化的、双方都能说话的通信链路。这个特性在文件同步、消息推送、状态监控这类场景里非常关键。换句话说,MCP不是单向的“请求-响应”,而是一套完整的会话协议。
1.3 MCP和传统Function Calling的对比
我经常被问到“MCP和Function Calling有什么区别”。这个问题如果展开讲,其实是两个不同层面的东西。Function Calling是大模型平台提供的一种参数约束能力:你在请求里声明一组函数,模型根据用户输入决定调用哪个,并返回结构化的调用参数。整个过程发生在单次请求内部,是一次性的、封闭的。MCP则是一个系统级的协议,它负责AI应用和外部工具之间的长连接、能力发现、消息路由。你可以在一个MCP服务器上挂多个工具,也可以在一个客户端里同时连接多个服务器。
| 对比维度 | Function Calling | MCP协议 |
|---|---|---|
| 集成方式 | 在代码里硬编码函数定义 | 通过协议动态发现能力 |
| 作用范围 | 局限于当前一次模型请求 | 面向整个应用与外部系统的连接层 |
| 状态保持 | 基本无状态,请求结束即结束 | 支持会话级状态和事件推送 |
| 生态适配 | 通常与模型平台API绑定 | 开放协议,任何客户端服务端都可接入 |
这两者并不互斥。实际上MCP的实现里也可以调用模型,而在调用模型时,底层可能依然是Function Calling。MCP更像把Function Calling这样的能力做了一层系统和工具层面的封装,让上层AI应用不用每家都单独实现一套。所以我的理解是:如果你只是想让模型能调用少数几个函数,Function Calling足够;如果你想构建一个稳定、可扩展的智能体系统,MCP是更省心的选择。
2. MCP核心架构与核心概念拆解
2.1 Host、Client、Server三类角色
MCP协议把参与者分成三层:Host(宿主)、Client(协议客户端)、Server(协议服务端)。理解这三层是理解整个协议的第一步。Host是最终运行AI应用的程序,比如一个聊天界面、一个智能IDE插件、一条自动化工作流。Client是Host内部负责与MCP服务器通信的组件,它作为协议客户端,向Server发起连接请求、发送JSON-RPC消息。Server则是实际提供能力的进程或服务,可能是一个本地Python脚本,也可能是一个远程的HTTP服务。
用一个实际场景来比喻:用户打开某个本地AI助手,在其中输入“帮我查一下上海今天的天气”。这个本地AI助手就是Host。助手内部自带的MCP Client负责和天气服务进程建立连接,问它“你有哪些工具”。天气服务进程就是Server,它返回“我有一个get_weather工具,需要city参数”。于是Client把工具的JSON Schema送给模型,模型决定要调用,最终Client向Server发送tools/call请求,Server执行查询并返回结果。整个过程用户感受到的是AI助手的自然对话,但实际上背后经历了Host、Client、Server三层协同。这种分层的好处是职责清晰:Host专注产品逻辑,Client专注协议通信,Server专注业务能力。
2.2 Tools、Resources、Prompts三大原语
MCP定义了三类核心能力,官方叫法叫三种原语:Tools、Resources、Prompts。简单记忆法:Tools是“干事”,Resources是“看数据”,Prompts是“走流程”。这三类原语覆盖了一个智能体系统最常用的交互模式。
Tools(工具)是最常用的一类,描述一份可以被模型调用的函数,包含名称、描述、入参Schema。模型判断用户意图后,按Schema填参,并发起调用。例如我后面实操里的get_weather就是一个典型的Tool。Resources(资源)面向数据场景。Server可以暴露某个文件、某张数据库表、某段配置内容,客户端先拿到资源URI列表,再通过resources/read来读取。Resources适合做“上下文供给”:模型回答前先知道项目里的规范文档、用户手上的资料,而不是靠用户手动粘贴。Prompts(提示词模板)则是一套预置交互模板。Server定义类似“生成需求文档”的模板,客户端可以列出模板并获取填充后的提示词,适合把常用的工作流固化下来。
三者之间的关系可以这样理解:工具偏动作,资源偏数据,提示词偏模板。实际开发中,一个复杂的MCP Server往往同时暴露Tools和Resources。比如一个项目管理Server,可能既有“创建任务”这个Tool,也有“读取本周任务列表”这个Resource,还有“生成周报”这个Prompt模板。
| 原语 | 回答的问题 | 典型示例 | 对应方法 |
|---|---|---|---|
| Tools | 模型能干什么 | get_weather、create_file | tools/list、tools/call |
| Resources | 模型能看到什么 | 项目文档、数据库表 | resources/list、resources/read |
| Prompts | 用户能走什么流程 | 生成周报、评审清单 | prompts/list、prompts/get |
2.3 传输层与JSON-RPC 2.0消息格式
MCP的消息格式不是自创的,而是基于JSON-RPC 2.0规范。这个选择很务实,因为JSON-RPC是一种非常成熟的远程调用协议,定义了请求、响应、错误码、通知这几种消息类型,可读性和调试性都很好。每条消息都包含jsonrpc字段固定为“2.0”,id字段用来匹配请求和响应,method字段是方法名,params字段是参数对象。错误响应用error字段携带标准错误码和原因描述。
在传输方式上,MCP目前有两种主流模式。第一种是stdio模式,Server由客户端作为子进程启动,通过标准输入和标准输出进行通信。这种模式部署最简单,安全边界也清晰,适合本地工具集成。第二种是HTTP模式,早期拆分成HTTP+SSE,后来协议演进为Streamable HTTP,统一了双向数据流。客户端可以通过HTTP POST发送JSON-RPC消息,服务端通过SSE或同步HTTP响应返回结果,适合远程部署和跨网络调用。
无论哪种传输模式,应用层消息格式保持一致。所以理论上,同一个MCP Server既可以用stdio模式嵌入本地应用,也可以包装成HTTP服务供远程客户端调用,消息层面无需改动。理解这一点,后面做代码移植时会省很多事。我实际开发时就是先写了stdio的Server,调试通过后改了启动入口直接变成HTTP服务,几乎没有改动业务代码。
3. 从零搭建一个MCP服务器
3.1 环境准备与项目结构
实操部分我以Python为例,因为官方SDK对Python支持最成熟。建议使用Python 3.10以上版本,创建一个干净的虚拟环境再安装依赖。不要图省事直接全局安装,MCP的SDK更新很快,版本隔离能避免很多莫名其妙的依赖冲突。
python -m venv .venv source .venv/bin/activate # Windows下执行 .venv\Scripts\activate pip install "mcp[cli]"安装完可以用mcp --version验证。项目结构不需要太复杂,先建一个目录,里面放一个Python文件就能跑通:
mcp_demo/ weather_server.py当然,如果是正式项目,我更推荐按模块拆分:server主逻辑、工具定义、配置读取,但这是后话。初学时保持一个文件,注意力集中在协议本身的流程上。等跑通了,再逐步重构也是一样的:MCP的Server本质就是一个实现了特定方法集的进程,拆不拆模块完全取决于业务复杂度。
3.2 用FastMCP实现一个天气查询工具
官方SDK里有两个层面的API:底层是Server类,接近原始JSON-RPC;上层是FastMCP封装,可以用装饰器快速定义工具。我建议初学者直接用FastMCP,代码少,心智负担小。下面是一个完整的天气查询服务:
from mcp.server.fastmcp import FastMCP server = FastMCP("weather-server") @server.tool() def get_weather(city: str) -> str: """查询指定城市的当前天气情况 Args: city: 城市名称,例如 上海、北京 """ weather_map = { "北京": "晴,25℃", "上海": "多云,28℃", "广州": "阵雨,26℃", } return weather_map.get(city, "暂不支持查询该城市天气") if __name__ == "__main__": server.run()这里@server.tool()装饰器把get_weather注册成一个MCP工具。函数名就是工具名,函数文档字符串会作为工具描述,参数上的类型注解会被转成JSON Schema。这一点非常重要:MCP客户端会把工具描述展示给模型,模型靠它判断什么时候该调用,所以多写几个字描述清楚,比少写导致模型错误调用的成本低得多。我见过太多工具名起得模棱两可、描述写得太短,结果模型频繁误调用的案例。
我特意把返回值写成了普通字符串。FastMCP会自动把它包装成标准的结果内容块,客户端拿到时是一个content数组。如果想返回更复杂的内容,也可以返回一个带结构的对象,FastMCP同样会处理。这一段代码已经是一个可以运行的MCP Server了,如果工具逻辑是异步的,比如要请求外部API,把函数定义成async def get_weather(city: str) -> str即可,FastMCP同样支持。
3.3 启动、调试与配置注册
在项目目录下执行:
python weather_server.py如果看到类似“server started”的日志,说明stdio模式的Server已启动并阻塞在标准输入上。这时候直接Ctrl+C退出即可。严格来说,stdio模式的服务必须由客户端进程拉起,自己手动执行主要是为了验证代码是否报错。如果你想确认Server能不能正常响应,可以用官方CLI,安装时我加上了[cli]选项,所以可以直接运行:
mcp dev weather_server.py这个命令会启动一个本地调试界面,允许你查看工具列表、手动调用工具、模拟客户端行为。我强烈推荐新手用它跑一遍,能非常直观地看到协议消息的往返过程——你会看到客户端发送的tools/list请求、服务端返回的工具定义列表,以及调用工具时的完整JSON消息。
如果是远程部署,FastMCP也提供了HTTP支持。把入口改为:
if __name__ == "__main__": server.run(transport="http", host="0.0.0.0", port=8080)服务就会跑在8080端口,远程客户端可以通过HTTP连接。这里需要留意鉴权问题:默认HTTP模式没有鉴权,生产环境必须配合网关或OAuth做访问控制,否则你的工具会被网络上任何能扫到端口的人使用。在实际的AI应用里,集成一个MCP Server通常通过配置文件完成。以某类支持MCP的桌面助手为例,配置JSON大致如下:
{ "mcpServers": { "weather-server": { "command": "python", "args": ["/path/to/weather_server.py"], "env": {} } } }key是自定义的服务名,command是启动命令,args是脚本路径。客户端启动时会按这个配置拉起子进程,并自动完成初始化握手。
4. 用MCP客户端调用工具
4.1 搭建一个最小可用的Python客户端
有了Server,还得有Client才能真正跑起来。客户端部分我同样用官方SDK,先看一个最短的调用示例:
import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params = StdioServerParameters( command="python", args=["weather_server.py"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() print("发现工具:", [t.name for t in tools]) result = await session.call_tool("get_weather", {"city": "上海"}) print("返回结果:", result.content) if __name__ == "__main__": asyncio.run(main())整个流程只有三行关键代码:initialize完成握手、list_tools列出能力、call_tool发起调用。StdioServerParameters负责定义子进程的启动参数,stdio_client负责建立进程管道,ClientSession负责协议消息的收发。这里的read和write是两个异步流对象,分别对应子进程的标准输出和标准输入。注意官方客户端本身是异步API,如果你的项目是基于同步框架的,需要自己做异步桥接,这是集成时最常遇到的工程问题之一。
4.2 初始化握手与能力协商
MCP的通信不是直接发送业务请求,而是先走一次初始化握手。客户端发送initialize消息,里面带上协议版本、客户端名称、客户端能力声明。服务端返回自己的协议版本、服务端能力声明。随后客户端发送notifications/initialized通知,表示可以开始正常业务通信。
这个过程类似TCP握手,但协商的内容不是序号,而是“双方各自支持什么”。比如服务端在capabilities里声明了tools: {"listChanged": true},客户端就知道服务端支持工具列表变更推送,监听事件即可。官方SDK的ClientSession已经把这层逻辑封装好了,调用session.initialize()会等握手全部完成。但是有一点需要注意:如果你用底层API自己组装消息,必须严格按顺序发送initialize、等待响应、再发notifications/initialized,顺序错了服务端会直接拒绝后续请求。
这里我把initialize阶段的关键字段列一下,方便排查问题:
| 字段 | 发送方 | 说明 |
|---|---|---|
| protocolVersion | 客户端 | 客户端支持的协议版本 |
| capabilities | 客户端 | 客户端能力声明,如roots、sampling |
| clientInfo | 客户端 | 客户端名称与版本 |
| serverCapabilities | 服务端 | 服务端能力声明,如tools、resources |
| instructions | 服务端 | 服务端返回的使用指引 |
4.3 调用工具与结果解析
调用之前,建议先用list_tools确认工具确实存在。返回的每个工具对象里,name是名称、description是描述、inputSchema是JSON Schema结构。你可以把inputSchema打印出来看,它就是模型用来生成参数的结构化说明。工具描述质量差,模型就会在参数填充上犯低级错误,比如把城市名写成“shanghai”而不是“上海”。
调用端代码中session.call_tool(name, arguments)两个参数分别指定工具名和参数对象。比如输出:
发现工具: ['get_weather'] 返回结果: [TextContent(type='text', text='多云,28℃')]result.content是一个列表,列表元素是内容块。大多是TextContent,直接用content[0].text就能取出文本。如果结果里有图片、结构化JSON,则会对应不同类型的内容块。解析前先检查result.isError字段,它表明这次调用是否执行失败。如果isError为True,content里通常放的是错误描述,而不是业务结果。
异步流方面,MCP还支持工具调用的进度通知,适合执行时间长的任务:Server可以推送进度消息,客户端可以实时展示进度。如果需要,在call_tool时传入相关参数,或者在会话中监听progress事件。初学阶段可以先忽略,但项目落地时建议考虑,否则用户等待长任务时体验会比较差。我做过一个文档生成工具,一次调用要执行几十秒,没有进度通知时用户完全不知道是不是卡死了,加了之后体验立刻不一样。
5. 常见问题与排查技巧实录
5.1 连接建立失败与超时
我在实际调试时最常见的错误,就是stdio模式下子进程起不来。用官方的stdio_client,如果传入command是python,而运行环境里python不在PATH中,会出现类似“spawn python ENOENT”的异常。解决办法是command使用绝对路径,或者用sys.executable动态取当前Python解释器路径。
server_params = StdioServerParameters( command=sys.executable, # 用当前解释器的绝对路径 args=["weather_server.py"], )HTTP模式下常见问题则是服务地址写错或服务未监听。建议先用curl或浏览器访问服务的健康检查端点,确认端口可通,再连MCP客户端。另外跨域限制也可能导致浏览器环境里的客户端连不上,需要服务端显式配置允许的跨域来源。
超时问题也值得单独说。官方客户端默认超时时间往往较短,长任务或慢网络下会报读取超时。可以显式设置read_timeout_seconds等参数,把超时放宽到30秒以上。不要一上来就怀疑协议有问题,先确认消息有没有真正到达对端。我曾经排查一个“服务端无响应”的问题,抓包发现客户端压根没把消息发出去,因为代理配置把localhost流量也代理走了。
5.2 工具列表为空或工具未发现
如果连上了Server,但list_tools返回空列表,大概率是工具没有被注册。用FastMCP时最常见的原因是漏写@server.tool()装饰器,或者工具函数定义在另一个模块但没有被导入。还有一种情况是装饰器作用在使用不同实例上:代码里创建了A、B两个FastMCP实例,但只把B启动了,工具却注册在A上。检查办法很简单:在Server启动处打印server.list_tools(),看看当前实例到底带了多少工具。
另一个容易忽视的点是客户端缓存。有些桌面应用会对工具列表做缓存,服务端更新工具后,客户端仍显示旧列表。遇到这种情况,重启客户端会话,或删除本地缓存目录。如果还是不行,就把服务端日志打开,看收到tools/list请求时返回了多少项。这类问题通常不是协议层面的,而是集成层面,别在消息格式上较劲太久。
5.3 协议版本和字段校验问题
协议版本不匹配是个典型问题。MCP协议本身在快速演进,SDK版本差异较大时,initialize阶段就可能返回版本不支持的错误。一个有效策略是让客户端和服务端使用同一版本范围的SDK,并在代码里打印协议版本号,方便对齐。我在自己的项目里固定了客户端和服务端的依赖版本,升级时一起升,避免出现一端支持新特性另一端还是旧协议的尴尬。
JSON-RPC字段校验问题也很常见。比如客户端发送tools/call时,arguments必须是对象;服务端返回result时,必须包含content数组。新手最容易犯的错误是:服务端处理完业务后直接return一个普通字符串,而底层SDK其实已经帮忙包装好了。但如果用原始的Server API,就需要自己构造完整结果结构,一旦少了字段,客户端解析就会失败:比如content不是数组,或者结果里没有content。排查这类问题时,建议打开调试日志,把双方收发的原始JSON消息打出来,逐字段核对,比盲猜快得多。
| 故障现象 | 可能原因 | 快速排查方法 |
|---|---|---|
| 子进程启动失败 | command不在PATH | 改用绝对路径或sys.executable |
| 初始化握手超时 | 服务端未启动或端口不通 | curl健康检查,检查防火墙 |
| list_tools返回空 | 工具装饰器漏写或实例不一致 | 打印当前实例的工具列表 |
| 字段解析失败 | 结果缺少content数组或类型不符 | 打印原始消息逐字段核对 |
| 版本报错 | 两端SDK版本差异过大 | 统一依赖版本,升级时同步 |
最后分享一个小技巧:无论用哪种SDK,调试MCP时第一件事永远是抓原始消息。stdio模式可以把子进程的标准输出重定向到日志文件,HTTP模式可以抓服务端访问日志。看到消息报文,整个链路从哪里断就一目了然。
这个内容后续扩展的方向其实很多:MCP协议还能用于文件同步、消息通知等场景,Remote MCP Server的引入也让跨团队共享工具变得更方便。我开始动手做MCP集成时的体会是,光看文档容易高估协议本身的复杂度,真正上手跑通一个端到端Demo之后就会发现,核心机制并不复杂,难的是外围的工程问题——工具描述质量、超时策略、错误处理、缓存机制,这些才是决定集成是否好用的关键。
如果你现在正要开始做MCP相关的集成,我的建议是先不要急着写复杂的Server,拿一个最简单的工具把“Server启动、握手、工具发现、工具调用”这条链路完整跑通,然后再逐步加参数校验、加日志、加异常处理。等这条链路在你脑子里变成肌肉记忆后,再去研究Resources和Prompts,甚至考虑多个MCP Server同时连接的情况。MCP的边界其实很清晰,花一个晚上把基础链路打通,后面就都是一点一点填细节了。