如果要给“MCP(Model Context Protocol)”找一个最接地气的理解方式,我通常会建议先忘掉那些配置教程和 SDK 文档,回到一个很朴素的问题:当一个大模型应用想使用外部工具时,它到底缺什么?
过去一年里,我接手的不少项目都踩在同一条路上:模型本身已经很强了,但每次要给 Agent 加一个能力,就要在代码里多写一层适配逻辑。今天接数据库,明天接设计稿,后天接一个测试工具,每个都要重新实现一套调用方式。MCP 的火爆本质上不是因为它又发明了一种工具调用格式,而是它把“模型与外部世界的交互”从零散的编码行为,变成了一种可协商、可发现、可复用的能力协议。这篇文章我想从第一性原理出发,把 MCP 的设计动机、运行时角色、以及“工具调用”到“能力协议”的范式转变拆开讲清楚,顺便聊一些我在实际集成中的体会和判断。
1. 模型接入外部工具的碎片化困局,才是 MCP 出现的真正前提
1.1 看似热闹的 Function Calling,解决不了多 Agent 多工具的组合问题
先回到一个基础事实:大语言模型本身只擅长处理 token,它没有执行动作的器官。所谓“让模型调用工具”,本质上是模型输出结构化的调用意图,然后由宿主程序去执行真正的函数。OpenAI 的 Function Calling、Anthropic 的 Tool Use,本质上都在做这一件事。
但这带来一个很隐蔽的复杂度:每次模型要和外部工具通信,你都要在应用代码里,把工具的入参、出参、鉴权、地址全部硬编码进去。假设你有 3 个 Agent 应用,每个都要对接 5 个工具,那么就需要写 15 份胶水代码。这些代码的形态高度相似,又无法互用:A 项目里写好的 MySQL 查询适配器,没法直接搬到 B 项目里用,因为 B 项目用的工具调用结构不一样。
这个“M×N 问题”正是 MCP 要去掉的。MCP 的思路是:让外部能力以统一协议的形态暴露出来,Agent 应用只实现一次协议客户端,剩下的所有工具接入都变成“添加一个 server 地址”或“启动一个子进程”的事。
1.2 为什么说 MCP 更接近一个“通用能力端口”,而不是某种 SDK
如果只把 MCP 理解为一种工具调用的新格式,那是低估它了。我更愿意把它比作“USB 接口”:早年各种外设都有自己的专用接口,后来行业统一成 USB,设备只要符合规范就能即插即用。MCP 做的其实是同一件事——把“一个外设工具怎么被模型使用”做成标准端口。
我当时用 Claude Desktop、Cursor、Codex 这些不同宿主去接同一个 MCP Server 时,明显感受到这种标准端口的价值。同一套 Figma MCP Server,既可以被 Cursor 加载,也可以在 Codex 里注册,不需要给每个宿主写不同的插件逻辑。真正做到了一次封装、多处复用。
1.3 实际生态里的信号:工具方主动做 MCP Server 已经成为一种趋势
如果只是理论上有用,MCP 不会在这么短时间内扩散。真正让我觉得趋势成型的是工具方开始主动提供 MCP Server——蓝湖有蓝湖 MCP,Figma 社区有很多开源 MCP 实现,MySQL 这类数据源有官方或社区适配,甚至 Matlab、Unity、Cocos Creator、x64dbg、Wazuh 这些垂直工具都有了对应方案。这说明 MCP 已经不只是一个开发者圈子里的协议,而正在变成应用层能力接入的公共约定。
所谓“从工具调用到能力协议”的第一个关键转折,就是视角变了:以前我们在代码里问“我该调用哪个函数”,现在我们在架构层面问“这个能力以什么协议暴露出来、如何被发现、如何被模型理解”。
2. MCP 运行时的三方角色,以及一次完整能力协商是如何走完的
2.1 Host、Client、Server:把职责拆开,才能让心智负担降下来
MCP 的运行时模型由三个角色组成:
| 角色 | 职责 | 通俗理解 |
|---|---|---|
| Host | 用户交互界面与工作流编排者 | 你正在用的 Agent 应用,如 Cursor、Codex、Claude Desktop |
| Client | 协议客户端,维持与服务端的连接和消息收发 | Host 内部替你去跟工具打交道的“协议翻译” |
| Server | 对外暴露能力的一方,提供工具、资源或提示 | 一个个能力提供者,例如 MySQL MCP Server、Figma MCP Server |
很多人第一次看会以为 Host 就是 Client,实际上在官方术语里两者是分开的。一个 Host 内部可以维护多个 Client 连接,每个 Client 对应一个 Server。这样设计的好处是逻辑清晰:Host 管人机交互,Client 管连接生命周期,Server 只管能力实现。我在实践中最常用的认知是:Host 是应用,Client 是通道,Server 是能力仓库。
2.2 一次调用从开始到结束,到底发生了什么
协议层面并不复杂:MCP 底层走的是 JSON-RPC 2.0 消息,传输层有两种常见形态,一种是本地子进程场景下的 stdio,一种是跨网络场景下的 Streamable HTTP。
一次标准流程可以分为五个阶段:
- 连接初始化:Client 向 Server 发送 initialize 请求,声明自己支持的协议版本与能力;Server 返回自己支持的版本、能力和服务信息。
- 通知已初始化:Client 发送 notifications/initialized 通知,表示初始化完成,可以进入正常通信阶段。
- 发现能力清单:Client 调用 tools/list,Server 把当前暴露的所有工具列表返回给 Client,包括工具名、描述和参数 JSON Schema。
- 调用具体工具:Agent 根据任务判断需要哪个工具,Host 代为发送 tools/call 请求,Server 执行后返回结构化结果。
- 错误或结果回传:如果执行过程异常,Server 通过 JSON-RPC 错误结构返回错误码与描述,而不是胡编乱造一个结果。
我用一个极简的例子还原这个过程。假设本地有一个文件工具 Server,Client 想知道它能做什么,会发起 tools/list,得到类似结构:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "read_file", "description": "读取指定路径的文本文件内容", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "文件绝对路径" } }, "required": ["path"] } } ] } }这个返回看起来很简单,但它极其关键。它意味着工具的能力不是写在 Agent 代码里的,而是由 Server 在运行时主动声明出来的。模型也好、Host 也好,拿到这份清单后就可以根据当前任务动态决定是否调用某个工具——这就是工具发现的本质。
如果模型决定要读某个文件,Host 会发送 tools/call 请求:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/tmp/example.txt" } } }Server 返回执行结果后,这一次调用才算闭环。
2.3 参数“初始化”这件事为什么值得单独拿出来说
很多人会忽略 initialize 这个握手步骤的价值,但我恰恰认为它是理解 MCP 的一个钥匙。它做的事情是能力协商:Client 和 Server 在真正干活之前,先交换双方的协议版本、支持的能力边界,避免用不匹配的语义对话。
这和人与人之间的协作很像——两个人对接一个任务,如果一方默认对方会中文,另一方只会英文,那后面的沟通就一定会出问题。MCP 通过 initialize 把双方的共同认知边界提前定好,后续的消息才具备一致的解释空间。
这也是为什么你在给 Codex 添加 MCP Server 时,如果填错了协议版本或 Server 地址,日志里最先报的往往是握手失败。只有 initialize 成功了,后面的 tools/list 才可能执行。
3. “工具调用”到“能力协议”的范式转变:工具是被发现的,不是被硬编码的
3.1 Function Calling 与 MCP 的根本差异不在格式,而在发现机制
很多开发者会纠结:我自己用 Function Calling 也能实现工具的执行,为什么非要引入 MCP?这里的关键差异不是“能不能调用工具”,而是“工具是由谁来发现的、以什么方式被发现”。
用 Function Calling 的方式,开发者在代码里定义好函数和参数格式,模型在推理时输出一个函数调用。整个过程里,工具的schema是编译进代码的,模型没有任何主动探索能力,宿主也没有一套通用的方式来获知“当前连接的工具还有哪些能力”。本质上还是“调用一个已知函数”。
而在 MCP 的语境下,工具清单是在运行时通过协议拿到的。Server 可以随时增减工具,Client 每次都能拿到最新的能力列表。模型不再只是调用一个预先写好的函数,而是面对一份由能力提供者声明的清单进行选择和编排。这一步从“硬编码调用”到“运行时发现”的变化,是我认为整篇文章中最核心的范式转弯。
3.2 把工具理解成“能力”,意味着模型参与的不只是执行,而是决策
看那些关于 MCP 的讨论,尤其是“Computer Use 和 MCP 的区别”“MCP 和 Skill 的区别”这类问题,背后其实是同一个混淆:分不清执行、技能、能力协议这三个层次。
| 对比维度 | Function Calling | Agent Skill / Prompt 技能 | MCP Server |
|---|---|---|---|
| 本质 | 函数调用的结构化接口 | 预设的提示词与流程知识 | 能力的协议化描述与执行 |
| 能否动态发现 | 不能,schema 在代码中写死 | 通常静态挂载 | 可以运行时发现 |
| 谁来做决策 | 模型选函数,宿主执行 | 模型按技能模板执行 | Agent 根据任务清单选择能力 |
| 解决的核心问题 | 如何把模型输出变成函数执行 | 如何让模型掌握复杂任务方法论 | 如何让能力跨应用复用、可协商、可发现 |
| 典型场景 | 单应用内工具调用 | RAG、多步骤工作流 | 跨 Agent 应用复用同一外部工具能力 |
Skill 更像是一个 Agent 的“大脑操作方法”,比如告诉模型遇到某类问题该怎么拆解、用户意图如何判断。而 MCP Server 提供的是“手和脚”,是真正能去操作外部世界的那层执行能力。MCP 与 Skill 根本不冲突,可以同时存在;技能提供思路,MCP 提供动作通道。
3.3 为什么“声明式工具描述”是能力协议中最有信息量的一环
如果说 MCP 协议本身是骨架,那工具描述就是血肉。一个工具能不能被模型在合适的时机选中,和它的 name、description、inputSchema 是否足够清晰高度相关。
我在接 Figma MCP 时印象很深。早期社区里做的 Figma MCP Server 工具描述写得比较粗糙,字段里只有参数名和类型,没有解释这个参数在 Figma 文档结构里是什么意思,模型经常在“组件实例”和“组件主符号”之间选错参数。后来新的 Server 实现把 description 写清楚,告诉模型什么场景应该调用哪个工具、Figma 文件节点的层级关系怎么理解,工具注册不上、调用不准确的问题立刻减少很多。
这说明一件事:MCP 协议本身只是管道,真正的智能在“工具语义的清晰表达”上。这也是能力协议和普通 API 接口最大的不同——普通 API 是写给程序员看的,字段不够清楚可以查文档;MCP 的工具清单是写给模型看的,模型没法像人一样去搜索引擎里查你埋藏在文档深处的字段含义,它只能根据协议里给出的信息做判断。
所以我在实际做 Server 时,会花很多精力打磨 description。每一个工具的描述里,我会写明:
- 这个工具解决什么问题;
- 什么场景应该调用它;
- 什么场景不应该调用它;
- 关键参数的实际业务含义;
- 返回结果里哪些字段值得重点关注。
听起来像是写文档,但这些内容直接决定了模型调用工具的准确率。把 MCP Server 当成一个纯技术接口来写,很容易得到一堆空泛的 tools/list,实际跑起来却永远选不对工具。
3.4 “能力协议”还意味着工具的使用过程可以被标准化观察
以前接一个工具,怎么调、日志怎么打、异常怎么抛,都是自定义的。MCP 把工具调用过程标准化之后,还带来一个容易被忽略的好处:可观测性。
因为所有能力请求都走相同的协议规范,Host 可以统一的日志体系打印出:Agent 在什么时刻选择了哪个工具、传入了什么参数、拿到什么结果、整个链路耗时多少。这对排查 Agent 的“黑盒行为”非常有用。
比如之前我在 Codex 里接 Figma MCP,遇到“总是工具注册不上”之类的现象时,如果不是协议层有标准日志,我根本没法判断是 Server 没启动、鉴权失败、还是返回的 schema 太大导致模型无法正确处理。有了协议层日志后,问题定位会快很多。这也是为什么我后来在团队里强推“所有能力接入优先走 MCP”,其中一个理由就是可维护性会好很多。
4. 亲手搭一个极简 MCP Server:理解协议生命周期最快的路径
4.1 不依赖 SDK,用 Python 标准库实现协议骨架
很多人会觉得 MCP 很高深,非要先装一套 SDK 才能开始。其实协议底层就是 JSON-RPC over stdio,完全可以不依赖任何 SDK,用几十行代码把生命周期跑通。这种“白手起家”的方式对理解最有效。
下面是一个极简的 MCP Server 骨架,支持 initialize、notifications/initialized、tools/list、tools/call 四种消息:
import sys import json def send_message(msg: dict): sys.stdout.write(json.dumps(msg) + "\n") sys.stdout.flush() def handle_message(line: str): try: msg = json.loads(line) except json.JSONDecodeError: return method = msg.get("method") msg_id = msg.get("id") params = msg.get("params", {}) if method == "initialize": send_message({ "jsonrpc": "2.0", "id": msg_id, "result": { "protocolVersion": params.get("protocolVersion", "2024-11-05"), "capabilities": {"tools": {"listChanged": False}}, "serverInfo": {"name": "demo-server", "version": "0.1.0"} } }) elif method == "notifications/initialized": # 通知类消息没有 id,也无需回包 return elif method == "tools/list": send_message({ "jsonrpc": "2.0", "id": msg_id, "result": { "tools": [ { "name": "echo", "description": "原样返回传入的消息内容", "inputSchema": { "type": "object", "properties": { "message": { "type": "string", "description": "要回显的内容" } }, "required": ["message"] } } ] } }) elif method == "tools/call": tool_name = params.get("name") arguments = params.get("arguments", {}) if tool_name == "echo": send_message({ "jsonrpc": "2.0", "id": msg_id, "result": { "content": [ { "type": "text", "text": arguments.get("message", "") } ] } }) else: send_message({ "jsonrpc": "2.0", "id": msg_id, "error": { "code": -32602, "message": f"unknown tool: {tool_name}" } }) if __name__ == "__main__": for line in sys.stdin: if line.strip(): handle_message(line)这段代码没有引入任何 MCP 官方库,但如果你把一个标准 MCP Client 指向它,完全能够正常完成握手和工具调用。原因就在于 MCP 的 stdio 模式本质就是换行分隔的 JSON-RPC 消息,只要消息结构符合协议规范,语言和框架都无关紧要。
4.2 手动输入协议消息,观察每一步返回
把上面的脚本保存成 server.py,用管道方式启动,可以在终端里手动敲入消息观察交互。启动方式是:
python3 server.py然后在 stdin 中逐行输入下面的消息。先发送 initialize:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-client","version":"0.0.1"}}}程序会返回协议版本和 Server 信息。接着发送 initialized 通知:
{"jsonrpc":"2.0","method":"notifications/initialized"}通知无需回包。然后发送 tools/list:
{"jsonrpc":"2.0","id":2,"method":"tools/list"}你会看到 Server 返回一个包含 echo 工具的清单,里面是完整的 name、description、inputSchema。这验证了 MCP 中“工具发现”的机制:Client 不需要事先知道工具长什么样,只要问一次,Server 就把能力说清楚。
最后调用工具:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello mcp"}}}输出结果是一个结构化 content 数组,里面包含文本内容。
当你亲手走完这个过程,会意识到一个很重要的结论:MCP 并不可怕,它甚至简单到可以用几十行代码模拟。真正的复杂度在于,工业级的 MCP Server 需要处理鉴权、多会话、资源订阅、采样、分页、大模型上下文窗口等大量真实场景,但核心骨架永远是这套发现和调用的握手逻辑。
4.3 从模拟到真实项目:什么时候上 SDK 和现有 Server
我并不是建议真实项目里用裸代码写 MCP Server。这只是帮助理解的方式。真实开发中,如果只跑个 Demo,可以用官方 SDK——TypeScript SDK 生态最完整,Python SDK 原型速度快,Java SDK 在服务化部署里更常见。如果连开发都不想开发,那就直接去找现成的 Server。
我常用的判断方法是:
- 只是想在 Claude Desktop、Cursor、Codex 里快速用某个能力,先搜现成 MCP Server;
- 需要连接内部系统、私有协议、专有工具,再动手实现;
- 实现时优先用官方 SDK,因为它把协议版本协商、传输层封装、鉴权等底层细节处理好了,不容易踩版本坑。
5. 选型判断与工程实践:到底用现成 MCP,还是自己实现一个 Server?
5.1 先给结论:能复用现成的,不要自己造 Server
MCP 和普通代码库不一样,它天然带有生态属性。如果一个能力已经有比较多人使用、迭代稳定,那它踩过的坑大概率比你预想的多。比如数据库类 MCP Server,我自己就见过不少自己实现的版本,功能也能跑,但错误处理、超时机制、Schema 兼容性都比社区成熟方案差不少。
“需要自己实现 MCP 还是直接用现成的”,我的回答取决于三个条件:
| 判断条件 | 做法 |
|---|---|
| 能力是否通用:数据库、浏览器、文件系统、设计稿导出等 | 优先搜索并复用现有 Server |
| 是否涉及内部私有协议或数据 | 社区没有,必须自己实现 |
| 现有实现是否满足稳定性、安全要求 | 不稳定再自己重构,而不是一开始就造轮子 |
你在网上看到很多人问“Codex 如何接入 MCP”“Cursor 配置 MySQL 的 MCP”,甚至“VSCode Copilot 连接 Figma MCP”,本质上都是“如何把一个已存在的 MCP Server 挂载到一个 Host”。这些操作基本上不需要写代码,只需要在配置文件里写 server 地址和启动命令。只有当一个 Server 不存在或不能满足需求时,才需要考虑自己实现。
5.2 自己实现 MCP Server 的几条实战原则
如果真的需要自己实现,我总结了这几年做工具接入的几条经验。
第一,粒度要小。一个 MCP Server 最好只负责一个领域,不要试图做“万能 Server”。数据库的只做数据库,设计稿的只做设计稿,测试工具链的只做测试。粒度太大,tools/list 会返回几百个工具,模型要在一个巨大的清单里挑选,反而容易选错,而且每次发现工具消耗的上下文 token 也高。
第二,描述是产品的一部分。我在 3.3 里已经强调过,这里还要再补充一个细节:工具描述应当写“模型的决策边界”,而不只是写“功能说明”。比如一个 MySQL 查询工具,如果你在 description 里写清楚“只能执行只读 SELECT,不能执行写操作;如果用户请求修改数据,应建议使用 database_write 工具”,模型才有足够信息避免错误使用。
第三,必须有可观测性。MCP 是给 Agent 用的外部能力,它经常运行在无人值守的自动任务里。如果 Server 没有完善的日志和错误上报,一旦模型在某个环节调错了工具或参数,排查起来会非常被动。我一般会在 Server 里记录每次 tools/call 的完整请求参数、耗时和返回结果摘要,方便复盘。
第四,图方便也要注意安全边界。MCP Server 一旦给了 Agent,它的沙箱能力就是 Agent 的延申。如果一个 Server 能访问本地文件系统,那么模型权限与 Host 权限完全一致。在设计 Server 的能力范围时,必须做最小权限控制,而不是把主机的所有能力都开放出去。
5.3 多 Agent 场景下,MCP 怎么编排才不失控
现在“MCP 多智能体”也是热门话题。很多人以为多 Agent 就是要搞一套复杂的 Agent 通信协议,其实在我的实践中,MCP 可以作为多 Agent 共享能力总线的一部分。
多个 Agent 作为不同的 Host,它们可以同时连接同一个 MCP Server,各自通过独立的 Client 会话调用同一组工具。如果一个任务需要多个工具配合,正确的做法不是让单个 Server 实现所有工具,而是让多个 Server 并行存在,由 Agent 统一编排。正是因为 MCP 天然支持多 Server,宿主才能在一场会话里同时连接数据库 Server、浏览器 Server 和设计稿 Server,按需取用。
这里有一个需要特别注意的问题:多个 Agent 并发调用同一个 Server 时,Server 的会话隔离要做好。如果 Server 内部保存了某个 Client 的上下文状态,就要用 session 区分,不能混在一起。否则并发场景下非常容易出现数据串名。
最后的实操体会
如果只看协议内容,MCP 并不复杂,它就是一整套基于 JSON-RPC 的标准化握手、能力发现和调用规范。但真正让我觉得值得深入理解的,是它背后对“工具”这个概念的重构:工具不再只是代码里一个被硬编码的函数,而是一种可以被动态发现、被模型在运行时理解的能力声明。
我自己的体会是,MCP 的普及会逐渐改变我们设计软件的方式。以前设计一个系统,先想接口文档;现在面向 Agent 场景设计系统,会先想这个系统向模型暴露能力时,工具清单怎么写、语义边界怎么划。这已经超越工具调用本身,进入能力协议的范畴了。
再分享一个落地层面的小经验:如果你第一次接触 MCP,搭建一个最小的 Server 完整跑一遍每个环节,比在图形界面里点几百次配置更有的价值。只有亲手看过能力协商的每一步,你才能理解为什么社区里有那么多现成的 Server 可以复用,也才知道什么时候应该自己动手扩展一个。