1. 从「删掉薄封装」说起:MCP 到底在解决什么问题
最近技术圈里关于 MCP 的讨论突然多了起来,起因是不少团队在重构 Agent 连接层时,开始把原先包在 API 外面的那层 MCP 封装直接删掉,换成更轻的 CLI 调用或者原生 HTTP 直连。这个动作被一些人解读为「MCP 要退出历史舞台」,但我在实际项目里反复折腾了几轮之后,发现事情远没有这么简单。MCP 全称 Model Context Protocol,本质上是给大模型和外部工具之间定义的一套标准化通信协议,它要解决的核心问题是:当你有十几个甚至几十个外部服务需要接进 Agent 时,怎么让模型用统一的方式去发现、调用和管理这些工具,而不是每接一个服务就写一套胶水代码。
我最早接触 MCP 是在一个内部知识库问答项目里,当时需要让 Agent 同时访问文档检索、数据库查询、工单系统三个后端。如果每个后端都单独写 function calling 的 schema,光是维护参数描述和错误处理就够喝一壶的。MCP 的思路是把这些工具都抽象成 server,Agent 作为 client 通过标准协议去拉取工具列表、调用工具、接收结果,这样新增一个工具只需要起一个 MCP server,Agent 侧几乎不用改代码。这个设计在工具数量多、变动频繁的场景下确实省事,也是它一开始被追捧的原因。
但问题也随之而来。很多团队在落地时发现,MCP 的抽象层在某些简单场景下反而成了负担。比如你只是想让 Agent 调一个内部 HTTP 接口查天气,走 MCP 就得先起一个 server 进程,定义 tool schema,处理协议握手,再让 Agent 去连。这一套下来,代码量比直接写个 HTTP 请求多了好几倍。于是「删掉薄封装」的声音就出来了——当封装带来的复杂度超过了它解决的问题时,删掉它反而是理性的选择。这也是为什么最近关于 MCP 是否会被 CLI、原生 HTTP 取代的讨论这么热。
2. MCP、CLI、HTTP 三种连接方式的本质差异
要判断 MCP 会不会退出历史舞台,得先把这三种连接方式放在同一个维度上对比。它们并不是简单的替代关系,而是各自适用于不同的抽象层级和场景。
2.1 协议层 vs 调用层:抽象位置不同
MCP 是一个协议层的标准,它定义的是「工具如何被描述、发现和调用」这套元规则。你可以把它理解成 USB 接口标准——不管你是键盘、鼠标还是U盘,只要符合 USB 协议,主机就能识别和使用。MCP server 就是符合这个标准的设备,Agent 是主机。它的价值在于标准化带来的互操作性:一个写好的 MCP server,理论上可以被任何支持 MCP 的 Agent 框架直接使用,不需要为每个框架单独适配。
CLI 则是调用层的封装。它把某个具体工具的调用方式固化成一个命令行入口,比如codex cli或者gitlab cli,Agent 通过执行命令并解析输出来完成任务。CLI 的优势是简单直接,不需要额外的协议握手,进程启动即用,特别适合那些本身就有成熟命令行工具的服务。但它的缺点是每个 CLI 的输入输出格式都不一样,Agent 侧需要针对每个 CLI 写解析逻辑,工具一多就回到胶水代码的老路。
HTTP 是最底层的连接方式,Agent 直接发请求到服务的 API 端点。它的通用性最强,任何有 HTTP 接口的服务都能接,但同样面临 schema 不统一的问题。而且 HTTP 调用需要处理认证、重试、超时、错误码解析这些细节,如果每个服务都手写一遍,维护成本很高。不过 HTTP 连接复用是个关键优化点——通过连接池复用 TCP 连接,可以显著降低高频调用时的延迟,这一点在 Agent 需要短时间内发起大量请求时特别重要。
2.2 三种方式的适用场景对照
| 维度 | MCP | CLI | 原生 HTTP |
|---|---|---|---|
| 抽象层级 | 协议层 | 调用层 | 传输层 |
| 工具发现 | 自动拉取工具列表 | 需预先知道命令 | 需预先知道端点 |
| 新增工具成本 | 起一个 server | 装一个 CLI | 写请求代码 |
| 跨框架复用 | 强 | 弱 | 中 |
| 调试难度 | 中(需看协议日志) | 低(直接跑命令) | 低(直接看请求) |
| 适合场景 | 工具多且变动频繁 | 工具有现成 CLI | 服务有稳定 API |
| 性能开销 | 中(协议握手) | 低(进程启动) | 低(连接复用后) |
从这张表能看出来,MCP 并不是在所有场景下都最优。当你的工具数量少、变动不频繁时,CLI 或 HTTP 直连的性价比明显更高。这也是为什么很多团队在项目初期用 MCP 快速搭起架子后,随着对具体工具调用路径的熟悉,开始把一些稳定的、高频的调用从 MCP 里拆出来,改成更直接的 CLI 或 HTTP 调用。这个「删掉薄封装」的过程,本质上是一次性能和维护成本的再平衡,而不是对 MCP 价值的否定。
2.3 为什么「薄封装」会成为被删的对象
所谓「薄封装」,指的是那些在 MCP 之上又包了一层、但没增加实质价值的中间层。我见过不少项目,明明 MCP server 已经提供了工具调用能力,团队又在 Agent 侧写了一个 wrapper,把 MCP 的调用再包一层,加上自己的日志、重试、参数转换。这层 wrapper 在项目初期看起来能统一风格,但随着时间推移,它变成了一个需要同步维护的负担——MCP server 改了接口,wrapper 要跟着改;Agent 框架升级了,wrapper 也要适配。更麻烦的是,这层封装往往掩盖了底层协议的真实行为,出问题时排查链路变长。
删掉这层薄封装,直接让 Agent 连 MCP server,或者干脆换成 CLI/HTTP,反而让调用链路更清晰。我在一个客服工单 Agent 项目里就做过这个操作:原先 Agent 通过一个自研的 tool adapter 调 MCP server,adapter 里做了参数校验和结果格式化。后来发现这些工作完全可以在 MCP server 内部做,adapter 纯粹是多余的。删掉之后,调用延迟降了大概 15%,而且排查问题时直接看 MCP 协议日志就行,不用再穿透两层。
3. Agent 连接架构重选:从单一方案到混合策略
「删掉薄封装」只是表象,真正值得讨论的是 Agent 连接架构该怎么重选。我的经验是,不存在一种连接方式能通吃所有场景,成熟的方案往往是混合策略:根据工具的特性、调用频率、稳定性要求,分别选择 MCP、CLI 或 HTTP。
3.1 按工具生命周期选择连接方式
工具在项目里的生命周期长短,是选择连接方式的一个重要依据。那些还在快速迭代、接口频繁变动的工具,适合用 MCP 接入,因为 MCP 的 schema 描述和工具发现机制能让 Agent 自动适应变化,不需要每次改接口都去改 Agent 代码。而那些已经稳定运行、接口几个月不变的工具,就没必要走 MCP 了,直接用 HTTP 或 CLI 更省事。
我在一个数据分析 Agent 里就是这么分的:数据源接入层用 MCP,因为不同数据源的字段和查询方式经常调整,MCP 的动态发现能力省了很多事;而像发送通知、写日志这类稳定操作,直接用 HTTP 调内部服务,代码简单到不需要任何封装。这样混搭下来,整体代码量比全走 MCP 少了将近四成,而且每部分的维护边界都很清晰。
3.2 按调用频率和性能要求选择
高频调用的工具对延迟敏感,这时候连接复用的价值就体现出来了。HTTP 连接复用通过 keep-alive 保持 TCP 连接,避免每次请求都重新握手,在高频场景下能把延迟压到很低。MCP 如果走的是 stdio 或 SSE 传输,每次调用的协议开销相对固定,在极高频率下可能成为瓶颈。CLI 则是每次调用都要启动进程,频率高的时候进程创建的开销不容忽视。
我实测过一个场景:Agent 需要在一轮对话里连续查询 50 次股票数据。用 HTTP 连接复用,总耗时大概 1.2 秒;换成 CLI 每次启动进程,总耗时涨到 4 秒多;MCP 走 stdio 大概 2 秒左右。这个差距在交互式场景里用户是能感知到的。所以对于高频、低延迟要求的工具,我倾向于直接用 HTTP 并开启连接复用,而不是套一层 MCP。
3.3 按安全边界选择
Agent 安全是个绕不开的话题。当 Agent 能调用外部工具时,如何限制它的权限、防止越权操作,是架构设计必须考虑的。MCP 在这方面有一定优势,因为 server 侧可以统一做权限校验和审计,Agent 只能调用 server 暴露出来的工具,不能直接访问底层资源。而 HTTP 直连的话,认证信息往往要放在 Agent 侧,一旦 Agent 被注入攻击,凭证泄露的风险更高。
不过这个优势也不是绝对的。如果 MCP server 本身没做好权限控制,只是简单转发请求,那和 HTTP 直连的安全水平差不多。关键还是看 server 侧有没有做细粒度的权限管理和调用审计。我在设计时通常会把敏感操作(比如写数据库、发消息)放在 MCP server 后面,由 server 统一鉴权;而只读的、低风险的操作可以直接走 HTTP,减少一层转发。
4. 实操:把 MCP 调用改造成 CLI 直连的完整过程
光说理论不够,我拿一个真实改造案例来拆解。项目背景是一个代码助手 Agent,原先通过 MCP 调用一个代码检索工具,后来因为检索频率很高,MCP 的协议开销成了瓶颈,决定改成 CLI 直连。
4.1 改造前的调用链路
改造前,Agent 调用检索工具的链路是这样的:Agent 发起 MCP tool call,经过 MCP client 序列化成协议消息,通过 stdio 发给 MCP server,server 解析后调用底层检索库,拿到结果再序列化回传,client 反序列化后交给 Agent。这条链路里,协议序列化和反序列化占了相当一部分开销,尤其是检索结果比较大的时候。
我先做了个基准测试,单次检索平均耗时 180 毫秒,其中协议处理大概占 40 毫秒。检索频率是每分钟 30 次左右,算下来协议开销每分钟 1.2 秒,虽然不算致命,但积少成多。
4.2 改造方案设计
改造的目标是去掉 MCP 这层,让 Agent 直接调用检索工具的 CLI。具体做法是:把检索逻辑封装成一个独立的命令行程序,接受查询参数,输出 JSON 格式的结果。Agent 侧通过执行命令并解析 stdout 来获取结果。
这里有个关键决策:为什么选 CLI 而不是 HTTP?因为检索工具本身是个本地库,没有现成的 HTTP 服务,起一个 HTTP server 反而增加了部署复杂度。CLI 可以直接调用本地库,进程启动开销在可接受范围内,而且调试时直接跑命令就能验证,非常方便。
CLI 的接口设计我遵循了几个原则:输入用命令行参数,避免交互式输入;输出统一用 JSON,方便 Agent 解析;错误信息走 stderr,退出码区分成功和失败。这样 Agent 侧只需要处理三种情况:退出码为 0 解析 stdout,退出码非 0 读 stderr 报错,超时则终止进程。
4.3 关键代码实现
CLI 程序的核心逻辑用 Python 写,大致结构如下:
import sys import json import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument("--query", required=True) parser.add_argument("--top-k", type=int, default=5) args = parser.parse_args() try: results = search(args.query, args.top_k) print(json.dumps({"ok": True, "data": results}, ensure_ascii=False)) except Exception as e: print(json.dumps({"ok": False, "error": str(e)}), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()Agent 侧调用时,用 subprocess 执行命令并捕获输出:
import subprocess import json def call_search_cli(query, top_k=5, timeout=10): cmd = ["python", "search_cli.py", "--query", query, "--top-k", str(top_k)] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) if result.returncode == 0: return json.loads(result.stdout) else: return {"ok": False, "error": result.stderr} except subprocess.TimeoutExpired: return {"ok": False, "error": "timeout"}这段代码里,超时处理是必须的。CLI 进程如果卡住不退出,Agent 会一直等下去,所以一定要设 timeout,超时后杀掉进程并返回错误。另外 stdout 和 stderr 要分开捕获,避免错误信息混进正常输出里导致 JSON 解析失败。
4.4 改造后的效果与注意事项
改造完成后,单次检索平均耗时降到 140 毫秒左右,比之前快了约 22%。更重要的是,调用链路变短了,出问题时直接看 CLI 的输出就能定位,不用再去翻 MCP 协议日志。代码量也少了,原先 MCP server 加 client 的代码有三百多行,现在 CLI 加调用逻辑不到一百行。
不过有几个坑要注意。第一,CLI 进程的启动开销虽然不大,但如果检索频率极高(比如每秒几十次),进程创建的开销会累积,这时候要考虑用长驻进程加 IPC 的方式,而不是每次起新进程。第二,CLI 的输出格式要严格约定,一旦格式变了 Agent 解析就会失败,所以最好在 CLI 里加个版本号,Agent 侧做兼容检查。第三,环境依赖要处理好,CLI 依赖的库要在部署环境里装好,否则会出现本地能跑线上报错的情况。
提示:CLI 改造适合那些调用逻辑相对独立、输入输出明确的工具。如果工具需要维护复杂的会话状态,CLI 的无状态特性反而会成为障碍,这种情况还是留在 MCP 里更合适。
5. 常见问题与排查技巧实录
在 MCP 改造和连接架构调整的过程中,我踩过不少坑,这里整理成速查表,方便对照排查。
5.1 连接层常见问题速查
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| MCP 工具列表拉取失败 | server 未启动或协议版本不匹配 | 检查 server 进程和协议版本号 | 重启 server,对齐协议版本 |
| CLI 调用返回空结果 | 命令参数格式错误或路径不对 | 手动跑一遍命令看输出 | 修正参数,用绝对路径 |
| HTTP 请求超时 | 连接未复用或服务端限流 | 看连接池配置和服务端日志 | 开启 keep-alive,加退避重试 |
| Agent 解析结果报错 | 输出格式与预期不符 | 打印原始输出对比 schema | 统一输出格式,加版本校验 |
| 高频调用延迟升高 | 进程创建或协议握手开销累积 | 压测对比不同连接方式 | 改用长驻进程或连接复用 |
5.2 几个容易忽略的细节
第一个细节是错误信息的传递。MCP 协议里错误是通过特定的错误码和消息传递的,改造时如果直接把底层错误抛给 Agent,Agent 可能无法理解。我通常会在 CLI 或 HTTP 层做一次错误归一化,把各种底层错误映射成 Agent 能处理的几种类型,比如「参数错误」「服务不可用」「超时」「权限不足」。这样 Agent 侧的错误处理逻辑就能统一,不用为每个工具单独写。
第二个细节是并发控制。Agent 可能会同时发起多个工具调用,如果这些调用都走 CLI,会同时起多个进程,对系统资源有压力。我一般会加一个并发上限,比如最多同时跑 5 个 CLI 进程,超出的排队等待。HTTP 调用则要注意连接池大小,池子太小会导致请求排队,太大又浪费资源,需要根据实际 QPS 调。
第三个细节是日志和可观测性。改造后调用链路变短了,但日志不能少。我会在 CLI 和 HTTP 调用处都加上结构化日志,记录调用参数、耗时、结果状态,方便后续分析。MCP 的话,协议层本身有日志,但往往比较冗长,需要过滤出关键信息。
5.3 一个真实的排查案例
有次线上 Agent 突然大量报「工具调用失败」,但手动跑 CLI 又是正常的。排查了半天,发现是部署环境里 CLI 依赖的一个库版本和本地不一致,导致某些查询会抛异常。这个问题在本地完全复现不了,因为本地库版本是对的。后来我们在 CLI 启动时加了一个依赖版本检查,版本不对就直接报错退出,避免跑到一半才失败。这个教训让我意识到,CLI 改造虽然简单,但环境一致性必须重视,最好用容器把 CLI 和它的依赖一起打包,避免环境差异导致的问题。
6. 我的选型建议:什么情况下该保留 MCP
聊了这么多改造和替代方案,并不是说 MCP 就该被淘汰。相反,在特定场景下 MCP 的价值是 CLI 和 HTTP 替代不了的。
当你的 Agent 需要接入大量第三方工具,而且这些工具来自不同团队、接口风格各异时,MCP 的标准化价值就凸显出来了。它让每个工具提供方只需要实现一次 MCP server,就能被所有支持 MCP 的 Agent 使用,省去了点对点适配的工作。这种网络效应在工具生态足够大时非常明显。
另外,当工具需要动态发现和组合时,MCP 的工具列表拉取机制比硬编码的 CLI 或 HTTP 端点灵活得多。Agent 可以根据任务需要,从 MCP server 拉取当前可用的工具,动态决定调用哪些,这在构建通用 Agent 时很有用。
所以我的建议是:不要因为「删掉薄封装」这个动作就全盘否定 MCP,也不要因为 MCP 是标准就无脑全用。关键是想清楚每个工具的特性——它稳定吗?调用频繁吗?需要跨框架复用吗?安全要求高吗?把这些想清楚,自然就知道该用哪种连接方式了。混合策略往往是最务实的答案,该用 MCP 的地方用 MCP,该直连的地方直连,别为了架构统一而牺牲实际效率。
我在最近一个项目里就是这么做的:核心的数据接入和敏感操作走 MCP,高频的查询和通知走 HTTP 直连,本地工具走 CLI。三套并存,各司其职,整体跑下来比之前全走 MCP 的方案稳定不少,维护起来也轻松。架构选型没有银弹,适合自己的才是最好的。