MCP协议详解:AI Agent与外部工具的标准通信协议
1. 从“AI Agent 的通用语言”说起为什么我们需要 MCP如果你最近在折腾 AI Agent尤其是那些需要调用外部工具、访问数据库或者操作复杂系统的 Agent你大概率会遇到一个核心痛点如何让大语言模型LLM稳定、可靠地与五花八门的外部资源对话我们当然可以写一堆零散的提示词Prompt告诉模型“调用 API A 时用这个格式查询数据库 B 时用那个 SQL”。但这种方式在 Agent 功能稍微复杂一点后就会迅速变成一场维护噩梦。API 变了怎么办新增一个数据源怎么办不同工具返回的数据格式千差万别模型能准确理解吗这背后是一个更本质的问题在 AI Agent 的架构中LLM大脑与外部工具手脚之间缺乏一种标准化的“通信协议”。就像人类社会的繁荣离不开语言和交通规则AI Agent 生态的规模化发展也需要一套被广泛认可的“基础协议”。这就是MCPModel Context Protocol出现的背景。它不是某个具体产品的 SDK而是一个由 Anthropic 牵头、社区推动的开放协议。你可以把它理解为AI Agent 世界的“USB 协议”或“HTTP 协议”。它的目标极其明确为 LLM 与任何外部数据源、工具或服务之间定义一套统一的、标准化的对话方式。简单来说MCP 试图回答“一个工具应该以何种格式告诉 LLM ‘我能做什么’能力声明LLM 又应该以何种格式告诉工具‘请执行这个动作’调用请求工具执行完毕后又该如何把结果‘打包’好送回给 LLM响应格式”理解了 MCP 要解决的“元问题”我们再来拆解它的具体实现。本系列文章我们将深入 MCP 协议的内部从最基础的消息格式到支撑其运行的传输层进行一次完整的“庖丁解牛”。今天这篇就是整个子系列的地基。2. MCP 协议的核心设计哲学声明式与标准化在深入技术细节前我们必须先把握 MCP 的设计哲学这决定了它所有技术选择的走向。MCP 的设计可以概括为两个关键词声明式Declarative和标准化Standardized。2.1 声明式让工具“自我介绍”而非“被命令”传统的集成方式是“命令式”的。开发者需要写死代码当用户说 X 时就调用函数 Y并手动处理参数 Z。这种方式将工具的逻辑硬编码进了 Agent 的核心流程里。MCP 则采用了声明式。工具在 MCP 中称为 Server不需要被“命令”如何工作它只需要向 LLM在 MCP 中称为 Client清晰地“声明”自己有哪些能力以及使用这些能力需要提供什么信息。例如一个天气查询工具会声明“我叫‘天气查询’我能根据‘城市名’这个参数返回该城市的天气信息。” 它不需要知道 LLM 具体怎么组织对话只需要定义好能力的“接口规范”。这样做的好处是巨大的动态发现与组合LLM或 Agent 框架可以在运行时动态发现可用的工具并根据当前对话上下文智能地选择并组合使用它们。工具可以随时上线、下线或更新而无需修改 Agent 的核心逻辑。降低耦合工具提供方和 Agent 开发者可以独立工作。工具提供方专注于实现功能并发布符合 MCP 协议的 ServerAgent 开发者则专注于设计提示词和业务流程通过标准协议连接所需工具。提升可靠性由于交互格式是标准的LLM 生成错误调用的概率会降低工具也能更准确地解析请求并返回结构化的结果。2.2 标准化定义通用的“词汇表”和“语法”声明式的前提是大家说同一种语言。MCP 通过定义一系列标准化的 JSON Schema为工具能力的描述、调用请求和响应建立了通用的“词汇表”和“语法”。资源Resources代表可被读取的静态或动态数据如一个文件、数据库查询结果、API 返回的 JSON。MCP 定义了如何描述一个资源URI、MIME 类型、元数据以及如何读取它resources/listresources/read。工具Tools代表可执行的操作通常带有输入参数并产生副作用或返回结果如发送邮件、创建日历事件、执行命令。MCP 定义了工具的描述格式名称、描述、输入参数 Schema和调用方式tools/listtools/call。提示词模板Prompts一种特殊的资源它本身是一个文本模板可以被 LLM 获取并用于生成更复杂的提示词。这允许工具提供领域特定的提示词片段。这三种核心抽象几乎涵盖了 AI Agent 需要与外部世界交互的所有主要模式。通过将它们标准化MCP 为 Agent 构建了一个丰富、可插拔的“外部能力市场”。3. 消息格式全解MCP 协议的数据交换单元协议的核心是消息。MCP 中所有的通信都基于 JSON-RPC 2.0 规范这是一种轻量级的远程过程调用协议。选择 JSON-RPC 是因为它简单、通用、语言无关并且有成熟的客户端/服务器实现库。一条 MCP 消息就是一个 JSON-RPC 消息。它主要包含以下几个部分3.1 请求Request消息格式当 Client通常是 LLM 端或 Agent 框架需要 Server工具端做某事时会发送一个请求。{ jsonrpc: 2.0, id: 1, method: tools/list, params: {} }jsonrpc: 固定为2.0表明遵循 JSON-RPC 2.0。id: 请求的唯一标识符数字或字符串。这个id至关重要因为 Server 的响应必须携带相同的id这样 Client 才能将响应与对应的请求匹配起来尤其是在异步通信中。method: 要调用的方法名。MCP 定义了一系列标准方法如tools/list列出所有工具、tools/call调用一个工具、resources/list列出所有资源等。params: 调用方法所需的参数是一个对象。例如调用tools/call时params里会包含工具名和具体的输入参数。一个完整的tools/call请求示例假设 Server 声明了一个名为get_weather的工具它需要一个city参数。{ jsonrpc: 2.0, id: req_weather_001, method: tools/call, params: { name: get_weather, arguments: { city: 北京 } } }这里id用了字符串req_weather_001method是tools/callparams里指明了要调用的工具名和具体的参数。3.2 成功响应Success Response消息格式Server 处理请求成功后会返回一个成功响应。{ jsonrpc: 2.0, id: 1, result: { tools: [ { name: get_weather, description: 获取指定城市的天气信息, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 } }, required: [city] } } ] } }id: 必须与对应请求的id完全一致。result: 请求的执行结果。其内容完全取决于method。例如对于tools/listresult是一个包含tools数组的对象对于tools/callresult会包含工具执行后的输出内容可能是一个文本、一个结构化对象甚至是一个包含多部分内容文本、图片、代码的复杂结构。接上例get_weather调用的成功响应可能如下{ jsonrpc: 2.0, id: req_weather_001, result: { content: [ { type: text, text: 北京当前天气晴气温 25°C西北风 2级。 } ] } }MCP 定义了一个灵活的content数组来承载结果支持text、image、resource等多种类型这为返回丰富内容提供了可能。3.3 错误响应Error Response消息格式如果请求无效或处理出错Server 返回错误响应。{ jsonrpc: 2.0, id: 1, error: { code: -32602, message: Invalid params, data: The parameter city is required but was not provided. } }error: 错误对象。code: 错误码。JSON-RPC 定义了标准错误码如 -32601 方法未找到-32602 无效参数MCP 也可以定义自己的应用级错误码。message: 简短的错误描述。data可选: 提供关于错误的额外信息如详细的验证错误信息。3.4 通知Notification消息格式这是一种特殊的、不需要响应的请求。它只有method和params没有id。在 MCP 中这通常用于 Server 向 Client 主动推送信息例如通知某个资源的内容发生了变化。{ jsonrpc: 2.0, method: notifications/resources/updated, params: { uri: file:///path/to/data.json } }Client 收到后可能会根据通知重新读取相关资源但不需要向 Server 发送响应。实操心得消息id的管理在实现 MCP Client 时管理好请求id是保证通信可靠性的关键。尤其是在高并发或异步场景下必须维护一个从id到请求上下文如回调函数、Promise的映射。一个常见的坑是id重复或映射清理不及时导致内存泄漏。建议使用单调递增的数字或带有时间戳的 UUID 来生成id并设置超时机制来清理过期的映射项。4. 传输层剖析MCP 协议如何“跑”起来定义了消息格式接下来就要解决消息如何在不同进程、甚至不同机器间传递的问题。这就是传输层的职责。MCP 协议设计上与传输层解耦这意味着它可以在多种传输方式上运行。目前社区主要支持和推荐两种方式stdio标准输入输出和SSEServer-Sent Events。4.1 Stdio 传输简单、可靠的进程间通信这是 MCP 最经典、也是最常见的传输方式尤其适用于 Client 和 Server 在同一台机器上以父子进程关系运行的场景。工作原理Client如 Claude Desktop、Cursor 或一个自定义的 Agent 框架启动一个子进程这个子进程就是 MCP Server你的工具。Client 将自己的标准输出stdout连接到 Server 的标准输入stdin。Server 将自己的标准输出stdout连接到 Client 的标准输入stdin。双方通过各自的 stdin 接收消息通过 stdout 发送消息。所有消息都遵循上一节定义的 JSON-RPC 格式每一条完整的 JSON 消息通常以换行符\n分隔即 JSON Lines 格式。流程示意图简化[Client进程] | | (fork/exec) v [Server进程] | | Client.stdout - Server.stdin (Client发送请求) | Server.stdout - Client.stdin (Server发送响应/通知) v优点部署简单无需网络配置非常适合本地工具集成。安全性好通信完全在本地进程间进行不暴露网络端口。资源消耗低没有网络栈开销。跨语言通用任何支持启动子进程和读写标准流的编程语言都能实现。缺点仅限于本地无法实现远程调用。生命周期绑定Server 进程的生命周期通常由 Client 管理。Client 退出Server 一般也会被终止。一个简单的 Stdio Server 伪代码示例Pythonimport sys import json def handle_tools_list(): return { tools: [{ name: echo, description: 回显输入的文字, inputSchema: { type: object, properties: {text: {type: string}}, required: [text] } }] } def main(): # MCP Server 通过 stdin 接收stdout 发送 while True: line sys.stdin.readline() if not line: break try: message json.loads(line) msg_id message.get(id) method message.get(method) if method tools/list: response { jsonrpc: 2.0, id: msg_id, result: handle_tools_list() } sys.stdout.write(json.dumps(response) \n) sys.stdout.flush() # ... 处理其他 method except json.JSONDecodeError: # 发送错误响应 error_resp { jsonrpc: 2.0, id: msg_id, error: {code: -32700, message: Parse error} } sys.stdout.write(json.dumps(error_resp) \n) sys.stdout.flush() if __name__ __main__: main()4.2 SSE 传输面向 HTTP 的轻量级流式通信当工具Server需要以远程服务的形式存在时例如一个部署在云端的数据库查询服务Stdio 就不适用了。这时SSEServer-Sent Events成为了更合适的传输层选择。工作原理MCP Server 作为一个标准的 HTTP 服务器运行并暴露一个特定的 SSE 端点例如/sse。MCP Client 通过 HTTP 长连接连接到这个 SSE 端点。连接建立后Server 会通过这个 HTTP 连接以 SSE 格式data: {json}\n\n持续向 Client 推送消息如通知。当 Client 需要发送请求如调用工具时它通过向 Server 的另一个 HTTP 端点例如/rpc发送 POST 请求来实现。Server 处理该请求并将响应通过 SSE 连接发回或者直接在 POST 请求的响应体中返回。这是一种混合模式请求/响应用 HTTP POST服务器推送用 SSE。这种设计利用了 SSE 在服务器向客户端单向推送上的天然优势同时用简单的 HTTP POST 处理客户端发起的请求。优点支持远程访问工具可以部署在远程服务器上被多个 Agent 共享。基于 HTTP兼容现有的 Web 基础设施易于在云环境部署和扩展。原生支持服务器推送非常适合 MCP 中 Server 主动发送通知的场景。缺点复杂度更高需要实现 HTTP 服务器和 SSE 连接管理。存在网络延迟不适合对延迟极度敏感的本地操作。需要处理身份认证和授权在 Stdio 中这不是问题。注意事项传输层的选择策略开发本地桌面插件或 CLI 工具优先选择Stdio。它与 Claude Desktop、Cursor 等客户端的集成最为顺畅开箱即用。构建共享的、中心化的工具服务例如为团队提供一个公司内部的数据库查询 Agent 工具应选择SSE over HTTP。你需要仔细设计 Server 的/sse和/rpc端点并处理好连接状态管理和心跳保活。网络环境不稳定如果通信双方网络延迟高或易抖动SSE 的长连接可能不稳定需要实现完善的重连机制。Stdio 则没有这个问题。5. 协议握手与初始化建立通信的“第一课”在 Client 和 Server 通过选定的传输层建立连接后并不能立刻开始请求工具列表或调用工具。它们需要先进行一次“握手”交换彼此的“身份”和能力信息这个过程称为初始化Initialization。初始化过程由 Client 主动发起通过调用一个特殊的、名为initialize的 JSON-RPC 方法。Client 发送的初始化请求示例{ jsonrpc: 2.0, id: 0, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: { roots: { listChanged: true }, sampling: { isSupported: false } }, clientInfo: { name: MyAwesomeAgent, version: 1.0.0 } } }protocolVersion: Client 支持的 MCP 协议版本。这确保了 Client 和 Server 使用兼容的协议特性。capabilities: Client 声明自己支持哪些可选的协议能力。例如roots.listChanged为true表示 Client 可以处理 Server 发送的关于根资源列表变更的通知。这类似于 HTTP 的 Feature Detection。clientInfo: Client 的自我介绍方便 Server 进行日志记录或差异化处理。Server 的初始化响应{ jsonrpc: 2.0, id: 0, result: { protocolVersion: 2024-11-05, capabilities: { tools: { listChanged: true } }, serverInfo: { name: WeatherService, version: 0.2.1 } } }protocolVersion: Server 确认使用的协议版本通常与 Client 发送的一致。capabilities: Server 声明自己支持或要求哪些能力。例如tools.listChanged: true表示 Server 会在工具列表变化时发送通知。serverInfo: Server 的自我介绍。只有在收到成功的initialize响应后Client 才能发送其他请求如tools/list或resources/list。随后Client 通常会发送一个initialized通知这是一个没有id的 JSON-RPC 通知告知 Server 初始化流程已完毕可以开始正常工作了。这个握手过程虽然简单但它是保证通信双方兼容性和稳定性的关键一步。它避免了因版本不匹配或能力不支持而导致的后续通信混乱。6. 核心方法详解MCP 协议的能力清单初始化完成后Client 和 Server 就进入了正式的交互阶段。MCP 定义了一套核心的 JSON-RPC 方法构成了协议的能力骨架。理解这些方法是编写或使用 MCP Server 的关键。6.1 工具Tools相关方法这是 MCP 中最活跃的部分用于动态发现和执行操作。tools/list: Client 调用此方法获取 Server 提供的所有工具列表。Server 返回的result中包含一个tools数组每个工具对象都严格遵循声明式的 Schema包含name、description和inputSchema。inputSchema是一个 JSON Schema 对象定义了调用该工具所需的参数这为 LLM 生成正确的参数提供了“说明书”。tools/call: Client 调用此方法来执行一个具体的工具。请求的params中必须包含name工具名和arguments符合inputSchema的参数对象。Server 执行工具逻辑并将结果放在响应的result.content中返回。这里有一个关键点arguments中的值是由 LLM 根据自然语言理解和工具 Schema “猜测”生成的因此 Server 端的实现必须对参数进行严格的验证和清理防止注入攻击或意外错误。notifications/tools/listChanged: 这是一个从 Server 发往 Client 的通知没有id。当 Server 的工具列表发生变化如新增、删除、更新了某个工具时它可以发送此通知。收到通知后Client 应该主动再次调用tools/list来获取最新的工具列表。这实现了工具能力的动态更新。6.2 资源Resources相关方法用于访问相对静态的、可读的数据内容。resources/list: 获取 Server 提供的所有可用资源的列表。返回的result中包含resources数组每个资源对象包含uri唯一标识符如file:///notes/daily.md、mimeType如text/markdown和name等元数据。resources/read: 读取指定uri的资源内容。Server 返回result.content内容格式与资源的mimeType对应。这对于向 LLM 提供背景文档、配置文件等内容非常有用。resources/subscribe/resources/unsubscribe(可选): Client 可以订阅某个资源的变化。当资源内容更新时Server 会通过notifications/resources/updated通知 Client。这对于监控日志文件、实时数据流等场景至关重要。notifications/resources/updated: Server 发送的通知告知 Client 某个已订阅资源的uri内容已更新。6.3 提示词模板Prompts相关方法提示词模板是一种特殊资源旨在帮助 LLM 生成更符合特定场景的提示词。prompts/list: 获取可用的提示词模板列表。prompts/get: 根据模板名和可选参数获取渲染后的提示词文本。例如一个“代码审查”模板可能接受code和language参数Server 会将这些参数填充到预定义的模板中生成一段具体的提示词返回给 ClientClient 再将其用于 LLM 对话。实操心得inputSchema的设计艺术定义工具的inputSchema是 MCP Server 开发中最体现功力的地方之一。一个好的 Schema 应该描述精准description字段要用自然语言清晰说明工具的功能和每个参数的用途这是 LLM 理解工具的主要依据。约束明确充分利用 JSON Schema 的type,enum,pattern(正则),minimum/maximum等关键字对参数进行严格限制。例如对于“城市名”参数可以设置enum: [“北京”, “上海”, “广州”, “深圳”]来避免 LLM 胡编乱造。结构扁平尽量使用扁平的对象结构避免深层嵌套。LLM 在生成复杂嵌套的 JSON 时更容易出错。提供示例在参数的description中或通过 JSON Schema 的examples字段提供示例值能显著提升 LLM 填充参数的准确性。 一个设计糟糕的 Schema 会导致 LLM 频繁调用失败而一个设计精良的 Schema 则能让工具调用行云流水。7. 安全性与错误处理构建健壮的 MCP 交互任何协议在实际应用中都必须考虑安全性和鲁棒性。MCP 作为连接 LLM 和外部世界的桥梁在这方面尤为重要。7.1 安全性考量输入验证与清理首要防线这是 Server 端最重要的安全措施。LLM 生成的arguments是不可信的输入。Server 必须依据inputSchema进行严格验证并对字符串参数进行清理防止 SQL 注入、命令注入、路径遍历等攻击。永远不要将 LLM 生成的参数直接拼接成命令或查询语句执行。权限最小化MCP Server 进程或服务应该以最低必要的权限运行。例如一个文件读取 Server 不应该有写入或删除文件的权限。传输安全对于Stdio由于是本地进程间通信主要风险来自恶意或存在漏洞的 Server 二进制文件。确保从可信来源获取 Server。对于SSE/HTTP必须使用HTTPS来加密传输通道防止中间人攻击。同时需要实现身份认证如 API Key、JWT Token和授权机制确保只有合法的 Client 可以连接。MCP 协议本身不规定具体的认证方式这需要在 HTTP 层自行实现。沙箱化执行对于执行任意代码或命令的工具应考虑在沙箱环境如 Docker 容器、nsjail中运行以隔离潜在风险。7.2 错误处理与重试策略充分利用 JSON-RPC 错误码Server 应返回精确的错误码和消息。例如参数缺失用-32602工具未找到用-32601自定义的业务逻辑错误可以使用-32000到-32099的范围。清晰的错误信息有助于 Client或背后的 Agent 逻辑进行决策例如提示用户补充信息或尝试其他工具。Client 端的超时与重试网络请求或工具执行可能超时。Client 端应为每个请求设置合理的超时时间并实现重试逻辑特别是对于幂等的操作如tools/list、resources/read。重试时应采用退避策略如指数退避避免加重 Server 负担。连接保活与重连针对 SSESSE 长连接可能因网络问题中断。Client 需要监听连接关闭事件并实现自动重连机制。在重连后可能需要重新执行initialize流程并同步状态如重新订阅资源。优雅降级当某个 MCP Server 不可用时Agent 应该有能力跳过该工具或者使用备选方案而不是整个流程崩溃。这要求 Agent 框架具备服务发现和健康检查机制。8. 实战视角从协议到实现的关键抉择理解了协议规范当我们着手实现一个 MCP Server 或集成一个 MCP Client 时还需要做出一系列工程上的抉择。8.1 Server 端实现选型你是要开发一个一次性的小工具还是一个需要服务化、高可用的企业级组件快速原型与 CLI 工具使用官方或社区的SDK是最高效的方式。例如Anthropic 官方提供了TypeScript/JavaScript 的 SDK(modelcontextprotocol/sdk)它封装了 Stdio/SSE 传输、消息序列化、初始化握手等底层细节你只需要专注于实现工具函数和资源逻辑。Python 等语言也有活跃的社区 SDK。这能让你在几分钟内搭建起一个可运行的 Server。高性能或特殊语言需求如果你需要极致性能或者必须使用某种没有成熟 SDK 的语言如 Rust, Go你可以基于 JSON-RPC 2.0 规范从头实现。核心工作是实现传输层读写 stdin/stdout 或 HTTP/SSE 服务器。解析和序列化 JSON-RPC 消息。维护请求id与处理逻辑的映射。实现协议要求的各个方法处理器initialize,tools/list,tools/call等。 虽然工作量较大但能获得完全的控制权。8.2 Client 端集成策略你是在构建一个全新的 Agent 框架还是在现有应用中如 Claude Desktop, Cursor使用 MCP在支持 MCP 的客户端中使用这是最简单的路径。像 Claude Desktop、Cursor、Windsurf 等应用已经内置了 MCP Client。你通常只需要在配置文件中指定你的 MCP Server 的命令行或 SSE 地址它们就能自动连接并加载工具。你只需要关心 Server 的实现。在自定义 Agent 框架中集成如果你在自研 Agent 系统你需要将 MCP Client 集成进去。这意味着实现或引入一个 MCP Client 库用于管理到多个 Server 的连接。在系统启动时初始化所有配置的 Server 连接。将tools/list返回的工具列表动态地转换为你的 Agent 系统所能理解的“工具”格式并注入到给 LLM 的提示词或函数调用列表中。当 LLM 决定使用某个工具时将 LLM 的输出参数转换为 MCPtools/call请求发送给对应的 Server并处理响应和错误。 这相当于在你的框架和 MCP 协议之间建立一个适配层。8.3 调试与监控开发 MCP 组件离不开调试。日志是生命线在 Server 和 Client 的关键节点收到请求、发送响应、发生错误添加详细的日志。对于 Stdio 模式日志可以输出到 stderr这样不会干扰正常的 JSON-RPC 消息流stdout。对于 HTTP 模式使用结构化的日志如 JSON并集成到现有的日志系统中。消息流可视化可以编写一个简单的“中间人”代理它位于 Client 和 Server 之间将所有经过的 JSON-RPC 消息打印出来或保存到文件。这是分析复杂交互问题的利器。使用现有调试工具社区已经有一些工具比如mcp-cli它可以作为一个通用的 MCP Client 来连接和测试你的 Server手动发送请求并查看响应非常适合初期调试。从消息格式的定义到传输层的选型从握手初始化的流程到核心方法的调用再到安全与实践的考量MCP 协议为我们勾勒出了一幅清晰的 AI Agent 扩展蓝图。它通过标准化和声明式设计将 LLM 与外部世界的交互难题转化为了一个可管理、可扩展的工程问题。掌握 MCP就如同掌握了为 AI Agent 打造“瑞士军刀”的标准接口规范。在接下来的子系列文章中我们将深入更多实战场景探讨如何设计复杂的工具 Schema如何构建生产可用的 MCP 服务以及 MCP 在具体 Agent 架构中的最佳实践。