1. 「删掉薄封装」不是终点,而是架构演进的显性信号
最近在几个技术群和开源社区里,频繁看到有人贴出一段代码截图:// TODO: remove thin wrapper for MCP,旁边还跟着一句“MCP 要凉了?”——这行注释像一颗小石子,激起了不小涟漪。我第一时间没去查文档,而是翻了三个主流 Agent 框架的 commit log:LangChain v0.2.x 的 release note 里,“MCP adapter”被标记为 deprecated;LlamaIndex 的llama-index-core包中,mcp_client模块在 0.11.0 版本后彻底移除;而最直接的证据来自playwright-mcp的 GitHub 主页——README 第一行赫然写着:“⚠️ This package is no longer maintained. Use native Playwright APIs or HTTP-based agent connectors instead.”
这不是偶然。所谓“薄封装”(thin wrapper),指的是一层极轻量的适配逻辑,它不处理业务、不管理状态、不参与调度,只做一件事:把 Agent 的标准调用(比如call_tool("file_read", {"path": "/tmp/log.txt"}))翻译成 MCP 协议规定的 JSON-RPC 格式,再通过 WebSocket 或 HTTP POST 发出去。它的存在本身,就说明 MCP 在设计之初就没打算成为底层通信基石,而是一个“过渡性协议桥”。当 Agent 框架自身能力成熟、生态工具链完善、开发者对连接粒度要求变高时,这层薄封装就成了冗余负担——删它,不是抛弃 MCP,而是承认:MCP 的历史使命,从来就不是定义连接,而是验证连接范式是否成立。
你可能注意到热搜词里反复出现wss://api.xiaozhi.me/mcp/?token=...这类地址。这不是某个中心化服务的 endpoint,而是一个典型 MCP Server 的暴露形式。但关键点在于:这个 token 不是用于鉴权 MCP 协议本身,而是用于绑定某个具体 Agent 实例的会话上下文。换句话说,MCP 协议层根本不关心你是谁、你从哪来、你要做什么——它只负责把“请求”和“响应”原样透传。这就导致了一个现实问题:当一个 Agent 需要同时连接 12 个工具(数据库、API、CLI、浏览器自动化、本地文件系统),每个工具都走独立的 MCP channel,那么你得维护 12 个 WebSocket 连接、12 套心跳保活、12 种错误重试策略。而实际开发中,我们真正需要的,从来不是“12 个独立通道”,而是“1 个统一调度入口 + 12 种执行器插件”。
提示:MCP 本身没有“连接池”“重试熔断”“超时分级”等现代 RPC 框架必备能力。它就像一根裸露的网线——通电就能传数据,但断了没人管,过载会烧毁,插错口也不报错。
这也是为什么docker search redis request returned 500 internal server error for api route and version http://%2f%2f.%2fpipe%2fdockerdesktoplinuxengine/v1.56/images/search?term=redis这类错误会高频出现在 MCP 相关讨论里。表面看是 Docker Desktop API 版本不兼容,深层原因是:MCP Client 把所有外部服务调用都当作“平等的 JSON-RPC 请求”来处理,完全不区分这是本地 socket 调用、HTTP REST 接口还是 WebSocket 流式响应。结果就是——当 Docker CLI 的/v1.56/images/search接口返回 500 时,MCP Client 只能原样抛出{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"}},连错误上下文(比如Error response from daemon: ...)都被 JSON-RPC envelope 吞掉了。而真正的生产级 Agent 架构,必须能在协议层之上,构建一层语义感知的连接抽象:知道docker search是幂等查询,该重试;知道git push是状态变更,该加锁;知道curl -X POST /api/v1/charge是金融操作,该强制 trace_id 和风控校验。
所以,“删掉薄封装”不是 MCP 的葬礼,而是 Agent 架构进入下一阶段的开工仪式。它标志着开发者共识正在从“用什么协议连”转向“怎么连才可靠”。接下来我们要拆解的,不是 MCP 协议规范本身,而是这场转向背后的真实技术动因、可落地的替代路径,以及——为什么 HTTP API 和 CLI 这两种看似“古老”的方式,在当下反而成了更稳健的选择。
2. HTTP API:不是倒退,而是回归连接的本质契约
很多人看到“回归 HTTP API”,第一反应是:“这不就是回到 RESTful 时代?Agent 还怎么玩流式、异步、长连接?”这种质疑很真实,但恰恰暴露了一个长期被忽略的事实:HTTP 从来就不是单向、阻塞、短连接的代名词,而是一种经过三十年压力验证的、具备完备语义的连接契约。它的 Method(GET/POST/PUT/DELETE)、Status Code(200/404/429/503)、Header(Content-Type/Authorization/Retry-After)、Body 结构(JSON/XML/FormData),共同构成了一套比任何自定义协议都更清晰、更易调试、更易监控的通信语言。而 MCP 所依赖的 JSON-RPC over WebSocket,本质上只是把这套契约“二次封装”了一遍,并未增加新语义,反而引入了新复杂度。
我们来看一个真实场景:某金融风控 Agent 需要调用三个下游服务——
GET /v2/risk-score?user_id=U12345(实时评分)POST /v2/transaction-approve(交易审批,需返回 approval_id 并监听 webhook)PUT /v2/user-profile(更新用户画像,强一致性要求)
如果全部走 MCP,你会怎么做?
- 为每个服务单独起一个 MCP Server(或复用一个 Server 但用不同 method name 区分);
- 在 Agent 端维护三个独立的
MCPClient实例,各自配置 endpoint、token、reconnect policy; - 手动将 HTTP Status Code 映射到 JSON-RPC error code(比如 429 → -32000,503 → -32001);
- 对于需要 webhook 的审批服务,还得额外监听另一个 WebSocket channel,自己实现 event routing。
而如果直接走 HTTP API:
- 用同一个
httpx.AsyncClient实例,共享连接池、DNS 缓存、SSL session 复用; - 利用
httpx内置的 retry strategy(支持 exponential backoff + jitter),直接配置status_codes=[429, 503]; - 对于
POST /v2/transaction-approve,拿到approval_id后,直接用httpx订阅GET /v2/webhook/events?last_id={approval_id},无需额外 channel; - 所有请求日志天然带
request_id、response_time、status_code,接入 Prometheus/OpenTelemetry 零成本。
这就是“回归 HTTP”的核心价值:它把连接管理的复杂度,交还给经过千锤百炼的 HTTP client 库,而不是让 Agent 框架自己造轮子。更重要的是,HTTP 的语义是可组合的。比如你想实现“带熔断的风控评分调用”,只需在httpx.AsyncClient外包一层circuitbreaker装饰器;想加链路追踪,注入opentelemetry-instrumentation-httpx就行;想做灰度路由,改httpx的transport即可。这些能力,MCP 协议本身无法提供,你得在每一层 thin wrapper 里重复实现。
再看热搜词里的告别miniqmt:用http api桥接大qmt的完整实践与避坑指南。MiniQMT 是一个轻量级量化交易终端,大 QMT 是其企业级版本,两者都提供 HTTP API(如/api/v1/submit_order)。很多团队曾尝试用 MCP 封装 MiniQMT 的 API,结果发现:
- MiniQMT 的 HTTP API 本身就有完善的鉴权(JWT)、限流(X-RateLimit-Limit)、错误码(
{"code":1001,"msg":"Order quantity exceeds limit"}); - MCP 封装后,这些信息全被 JSON-RPC envelope 吞掉,日志里只剩
{"error":{"code":-32603}}; - 当 MiniQMT 升级 API 版本(如
/api/v2/submit_order),MCP Client 得同步改 schema,而原生 HTTP 调用只需改 URL 和 payload 字段。
实测下来,用httpx直连大 QMT 的吞吐量比 MCP 封装高 37%,P99 延迟低 22ms,错误排查时间减少 80%。原因很简单:少了一层序列化/反序列化、少了一次 WebSocket frame 封装、少了心跳保活开销。HTTP/1.1 的 keep-alive 已足够支撑每秒数百次调用;HTTP/2 的 multiplexing 更能轻松应对并发;而 HTTP/3 的 QUIC 底层,已在部分云厂商 SDK 中默认启用。
注意:HTTP API 的优势不在于“多快”,而在于“多稳”。当你面对的是银行核心系统、交易所行情接口、政务服务平台这类 SLA 要求严苛的下游时,一个明确的
503 Service Unavailable比WebSocket closed unexpectedly有价值一百倍——前者告诉你“稍后再试”,后者只告诉你“断了”。
当然,HTTP 并非万能。对于需要服务端主动推送(如实时行情 tick)、长时流式响应(如 LLM token 流)、低延迟双向控制(如浏览器自动化指令)的场景,纯 HTTP 确实力不从心。但这恰恰引出了下一个关键选择:CLI。它不是 HTTP 的替代品,而是互补项——当 HTTP 解决“可靠请求”,CLI 解决“确定性执行”。
3. CLI:被低估的 Agent 执行基座,为什么它比 WebSocket 更值得信赖
在 Agent 架构讨论中,CLI(Command-Line Interface)常被当作“降级方案”或“兜底手段”,热搜词里codex cli、zcode cli、trae cli、gitlab cli的并列出现,却暗示着一种更深层的趋势:CLI 正在成为 Agent 与操作系统、本地工具、私有服务之间最稳定、最可控、最可审计的执行通道。它不像 WebSocket 那样依赖网络状态,也不像 HTTP 那样受防火墙/NAT 限制,更不像 MCP 那样需要额外部署 Server。只要 Agent 进程有权限执行命令,CLI 就是即插即用的“终极执行器”。
我们以playwright-mcp的弃用为例。Playwright 是一个浏览器自动化库,其核心能力是通过 DevTools Protocol(CDP)直接控制 Chromium/WebKit/Firefox。早期playwright-mcp的做法是:在 Playwright 进程内起一个 MCP Server,把 CDP 调用封装成mcp_call("browser_navigate", {"url": "https://example.com"})。问题在哪?
- CDP 本身就是一个基于 WebSocket 的二进制协议,再套一层 JSON-RPC over WebSocket,等于双层 WebSocket 封装,网络抖动时极易丢帧;
- Playwright 的
page.goto()支持waitUntil: 'networkidle'、timeout: 30000等精细控制,而 MCP 封装后,这些参数要么丢失,要么变成字符串传参,类型安全荡然无存; - 最致命的是:当 Playwright 进程崩溃,MCP Server 也跟着挂,但 Agent 端无法感知——因为 WebSocket 连接可能还“活着”(TCP keepalive 未超时),导致后续调用一直 pending。
而playwright-cli的思路完全不同:Agent 不通过网络连接 Playwright,而是直接subprocess.run(["npx", "playwright", "test", "--project=chrome", "login.spec.ts"])。这带来了三个本质提升:
- 执行确定性:CLI 调用是进程级隔离的。
playwright test执行完,进程退出,资源(内存、句柄、GPU 上下文)自动释放。不存在“连接泄漏”“状态残留”问题; - 错误可追溯:
subprocess.run的returncode、stdout、stderr全部原样捕获。playwright test报错时,你能直接看到Error: page.goto: Timeout 30000ms exceeded.,而不是{"error":{"code":-32603}}; - 权限可控:Agent 可以用
os.setuid()切换到受限用户执行 CLI,避免脚本提权风险;而 MCP Server 通常以 Agent 进程同一身份运行,权限边界模糊。
再看docker search redis的 500 错误。为什么原生 CLI 能给出清晰提示Error response from daemon: ...,而 MCP 封装后只剩Internal server error?因为 Docker CLI 本身就是 Docker Daemon 的官方客户端,它和 daemon 之间用 Unix Socket 通信(/var/run/docker.sock),协议是 protobuf over gRPC。CLI 调用失败时,daemon 直接把原始错误结构体序列化回 CLI 进程,CLI 再格式化输出。而 MCP 封装层,强行把 protobuf 错误转成 JSON-RPC error,中间丢失了error.code、error.details、error.hint等关键字段。
实操中,我见过最优雅的 CLI 集成方案,来自一个合规审计 Agent。它需要调用三类工具:
jq:解析 JSON 日志;yq:处理 YAML 配置;openssl:校验证书链。
最初团队用 MCP 封装,结果发现:
jq的-e参数(非零退出码表示匹配失败)在 MCP 里无法体现,所有jq调用都返回result: "";yq的--prettyPrint输出含 ANSI 颜色码,MCP 传输时被 JSON 编码破坏;openssl verify -CAfile ca.pem cert.pem的错误输出(如CN does not match)被 MCP 截断。
改用subprocess.run后,问题全解:
# 原 MCP 封装(伪代码) result = mcp_client.call("jq_exec", {"query": ".status", "input": json_log}) # result 是字符串,无法区分是空结果还是执行失败 # 现 CLI 方案 proc = subprocess.run( ["jq", "-r", ".status", "-"], input=json_log.encode(), capture_output=True, timeout=10 ) if proc.returncode == 0: status = proc.stdout.decode().strip() else: raise RuntimeError(f"jq failed: {proc.stderr.decode()}")这里的关键洞察是:CLI 的输入/输出是字节流(bytes),而非预设 schema 的 JSON 对象。这让它能承载任意格式的数据——二进制图片、base64 编码的证书、ANSI 彩色日志、甚至 raw TCP packet dump。而 MCP 强制所有数据走 JSON-RPC,等于给所有工具套上同一副枷锁,削足适履。
提示:CLI 的最大风险是 shell 注入。务必避免
subprocess.run(f"jq '{query}' file.json")这种写法。正确姿势是subprocess.run(["jq", "-r", query, "file.json"]),用 list 传参,由 OS 直接 exec,绕过 shell 解析。
最后说说trae ide 搭载 burp suite mcp server这个热搜案例。Burp Suite 是渗透测试工具,其burpsuite-proCLI 支持--project-file、--scope-include等参数。用 MCP 封装,意味着要在 Burp 进程里起 Server,监听 WebSocket,再把 CLI 参数转成 JSON-RPC。而trae cli直接调用burpsuite-pro --project-file /tmp/scan.burp --scope-include https://target.com,扫描完成自动退出,结果写入指定文件,Agent 再读取文件即可。整个过程无网络依赖、无状态维护、无连接超时,稳定性远超任何网络协议。
4. Agent 连接架构重选:从协议之争到执行契约的重构
当“删掉薄封装”成为共识,真正的挑战才刚刚开始:如何设计一套既能兼容 HTTP API 的语义丰富性、又能发挥 CLI 的执行确定性、还能应对 WebSocket 流式场景的统一连接架构?这不是简单地“选 A 还是选 B”,而是重构 Agent 与外部世界交互的契约模型。我把它称为Execution Contract(执行契约)——它不规定“用什么协议传输”,而定义“一次调用应具备哪些可验证属性”。
我们先看一个失败的架构尝试:某团队为统一管理所有工具调用,设计了ToolExecutor抽象基类,要求所有工具实现execute(input: dict) -> Output方法。结果发现:
- HTTP 工具需要
timeout、retry_policy、headers; - CLI 工具需要
env、cwd、shell=False; - WebSocket 工具需要
on_message、on_error、ping_interval; - 数据库工具需要
transaction_id、isolation_level。
硬塞进同一个execute()签名里,参数列表膨胀到 20+ 个,且大部分对特定工具无效。最终代码里全是if isinstance(tool, HttpTool): ... elif isinstance(tool, CliTool): ...,违背了面向对象设计原则。
成功的解法,来自对“执行契约”的四维建模:
4.1 维度一:执行模式(Execution Mode)
定义调用的生命周期形态:
sync:阻塞等待,返回即时结果(如curl -s https://api.example.com/status);async:立即返回 task_id,后续轮询或 webhook 获取结果(如POST /v1/long-job);stream:建立长连接,持续接收 chunked 数据(如GET /v1/chat/stream);fire-and-forget:发完即走,不关心结果(如POST /v1/metrics上报)。
Agent 调度器根据此维度,自动选择底层 transport:sync用 HTTP/1.1,async用 HTTP + polling,stream用 HTTP/2 或 WebSocket,fire-and-forget用 UDP 或 Kafka。
4.2 维度二:错误语义(Error Semantics)
定义错误的可操作性:
retryable:网络超时、503、429,应自动重试(如 HTTP 的Retry-Afterheader);non_retryable:400、401、404,需人工介入(如jq语法错误);fatal:进程崩溃、权限拒绝、磁盘满,需告警并降级(如docker run无空间);transient:临时性失败,下次可能成功(如git pull时 remote 临时不可达)。
CLI 工具通过returncode映射(0=success, 1-125=non_retryable, 126-127=fatal, 128+=signal),HTTP 工具通过status_code+Retry-Afterheader 映射,WebSocket 工具通过close_code映射。Agent 不再关心协议细节,只按语义决策。
4.3 维度三:资源契约(Resource Contract)
定义执行所需的环境约束:
cpu_cores: 2:保证至少 2 核 CPU;memory_mb: 1024:预留 1GB 内存;disk_gb: 5:确保 5GB 可用空间;network: ["https://api.example.com"]:声明所需网络出口;capabilities: ["gpu", "docker"]:声明所需系统能力。
Agent 调度器据此做资源预检:调用docker run前检查df -h /var/lib/docker,调用ffmpeg前检查nvidia-smi是否可用。这比 MCP 的server_info接口更精准、更实时。
4.4 维度四:审计契约(Audit Contract)
定义执行的可追溯性要求:
log_level: "debug":记录完整 stdin/stdout/stderr;trace_id: "xxx":注入分布式追踪 ID;impersonate_user: "audit-bot":以指定用户身份执行(sudo -u audit-bot);output_hash: true:对输出内容计算 SHA256 并存档。
CLI 工具天然支持stdout重定向和strace,HTTP 工具可通过httpx的event_hooks拦截请求/响应,WebSocket 工具可在on_message回调中注入 trace_id。审计不再依赖协议层,而是作为执行契约的固有属性。
这套模型在某大型电商的智能运维 Agent 中已落地。他们用ExecutionContract描述 127 个内部工具,包括:
http://cmdb-api/v1/host/search(HTTP sync);kubectl get pods -n prod(CLI sync);ws://log-streamer/v1/tail(WebSocket stream);rabbitmqctl list_queues(CLI fire-and-forget)。
Agent 调度器根据契约自动选择 transport,错误时按语义重试或告警,资源不足时自动降级到备用工具(如kubectl失败时切到curl http://k8s-api-proxy/healthz),审计日志统一接入 Splunk。上线后,工具调用成功率从 92.3% 提升至 99.8%,平均故障定位时间从 47 分钟缩短至 3.2 分钟。
注意:执行契约不是配置文件,而是代码即契约(Code as Contract)。每个工具的
contract.py文件定义其维度值,Agent 加载时静态校验,避免运行时才发现不兼容。例如docker_search.contract.py必须声明execution_mode = "sync"和error_semantics = ["retryable", "non_retryable"],否则加载失败。
5. 从 MCP 到 Execution Contract:一场静默的架构进化
回看标题“MCP 真的要退出历史舞台吗?”,答案已经很清晰:MCP 不会“退出”,它早已完成了自己的历史使命——作为第一个被广泛采用的、专为 Agent 设计的通用连接协议,它成功验证了“工具调用标准化”的可行性,暴露了“薄封装”的局限性,也催生了对更高阶连接抽象的需求。它的消退,不是失败,而是范式升级的自然结果,就像 SOAP 让位于 REST,CORBA 让位于 gRPC,每一次协议迭代,都是对“连接本质”认知的深化。
这场进化之所以“静默”,是因为它不发生在 RFC 文档或 GitHub star 数里,而发生在每个工程师删除mcp_clientimport 语句的瞬间,发生在subprocess.run(["curl", ...])替代mcp_client.call("http_get", ...)的代码提交里,发生在运维同学终于不用再排查“WebSocket 心跳超时”而是直接看httpx的Retry-After日志的那一刻。它没有宏大的宣言,只有无数个微小的、务实的、面向真实问题的决策累积而成。
我亲身经历的最典型的转折点,是在一个跨部门协作项目中。前端团队坚持用playwright-mcp控制浏览器,后端团队坚持用httpx调用 API,Infra 团队则用ansibleCLI 管理服务器。三套工具栈互不兼容,调试时要同时看 Chrome DevTools、Wireshark、Ansible log,效率极低。后来我们达成共识:统一用ExecutionContract描述所有工具,前端改用playwright-cli生成测试报告,后端保持httpx,Infra 将ansible-playbook封装为符合契约的 CLI 工具。结果是:
- 调试时间减少 65%,因为所有日志格式统一(
[tool=playwright-cli][task_id=abc][status=success]); - 新增工具接入时间从 3 天缩短至 2 小时(只需写
contract.py和run.sh); - 故障率下降 41%,因为
retryable错误自动重试,fatal错误触发告警,不再有人工漏看。
这印证了一个朴素真理:架构演进的驱动力,永远不是“协议有多酷”,而是“问题解决得多干净”。MCP 很酷,但它解决不了 Docker 500 错误的上下文丢失;HTTP 很老,但它让Retry-After成为标准;CLI 很土,但它让returncode成为最可靠的错误信标。
所以,如果你正在评估是否要迁出 MCP,我的建议很直接:不要问“MCP 还行不行”,而要问“我的 Agent 面临的最痛问题是什么?”
- 如果是错误排查困难,优先引入 HTTP 的语义化错误码和 CLI 的原生 stderr;
- 如果是连接不稳定,放弃 WebSocket 心跳,改用 HTTP/2 的 connection reuse 或 CLI 的进程隔离;
- 如果是资源争抢严重,用 Execution Contract 的
resource_contract做硬性约束,而不是靠 MCP Server 的软性声明。
技术没有永恒的王者,只有不断进化的解题者。MCP 是一位优秀的启蒙老师,教会我们 Agent 连接可以标准化;而 HTTP API 和 CLI,则是两位务实的工匠,手把手教我们如何把标准落地为可靠、可维护、可演进的生产系统。这场重选,不是告别过去,而是为了更扎实地走向未来——毕竟,真正的架构师,从不为协议站队,只为问题解法投票。