Raz Blog twitter/x github

对 MCP 的深入剖析:一次批判性的审视

"MCP 是一个开放协议,用于标准化应用程序如何为 LLM 提供上下文。可以将 MCP 视为 AI 应用程序的 USB-C 端口。正如 USB-C 提供了一种将设备连接到各种外围设备和配件的标准化方式一样,MCP 提供了一种将 AI 模型连接到不同数据源和工具的标准化方式。" ― Anthropic

TL;DR

我希望这最终能证明是我自己的技术问题,并希望我遗漏了什么。 在过去的一个月里,MCP (Model Context Protocol) 非常火爆,它可以使 LLM 成为代理并与世界互动。这个想法很简单:让我们标准化一个 API,以便 LLM/Agents 与世界互动以及如何告知 LLM/Agent 关于它的信息。 事情进展非常迅速,IBM 最近发布了他们自己的与 MCP “正交的标准”,称为 Agent Communication Protocol (ACP),紧随其后的是 Google 宣布的 Agent2Agent (A2A)。 MCP 服务器和客户端每天都在构建和发布,可以在 mcp.sopulsemcp.com 等网站上找到。 然而,我惊讶于明显缺乏成熟的工程实践。所有主要的参与者都花费数十亿美元来训练和调整他们的模型,然后转过身来,据我所知,让实习生编写文档,提供低于标准的 SDK,并且在实施指导方面几乎没有提供任何内容。 这种趋势似乎在 MCP 中仍在继续,导致了一些非常奇怪的设计决策、糟糕的文档,甚至更糟糕的是实际协议的规范。 我的结论是,整个 HTTP 传输建议的设置(SSE+HTTP 和可流式 HTTP)应该被抛弃,并替换为模仿 stdio 的东西...... WebSockets。

背景

大约三周前,我决定加入 MCP 的行列,尝试一下看看它如何在我们的环境中使用。我非常想在开始使用抽象之前了解事物在底层是如何实际运作的人。这里我们有一个新的协议,它可以通过不同的传输方式工作——多么令人兴奋! Anthropic 是 MCP 标准化工作的幕后公司,而且 MCP 似乎是 Anthropic 的 CEO 认为大多数代码将在一年左右的时间内由 LLM 编写的主要原因之一。基于使用它的感觉,对编码工具的押注似乎是标准化工作的指导原则。

协议

简而言之,它是一个具有预定义方法/端点的 JSON-RPC 协议,旨在与 LLM 结合使用。 这不是这篇文章的重点,但协议本身也有一些值得批评的地方。

传输

与 2005 年后的许多应用程序一样,它们据称是“本地优先的”(具有讽刺意味),并且 MCP 似乎非常符合这种情况。 查看传输协议,您可以了解他们的想法——如果他们的目的是为您的笔记本电脑上的编码构建 LLM 工具。 他们可能正在查看本地 IDE(或更实际的 Cursor 或 Windsurf),以及如何让 LLM 与本地文件系统、数据库、编辑器、语言服务器等进行交互。 基本上有两种主要的传输协议(或者三种):

  1. stdio
  2. “通过 HTTP 进行的一些操作,Web 似乎是我们可能应该支持的东西。”

Stdio

使用 stdio 本质上意味着启动本地 MCP 服务器,将服务器的 stdoutstdin 管道连接到客户端,然后开始发送 JSON,并使用 stderr 进行日志记录。 它有点打破了使用这些流进行双向通信的 Unix/Linux 管道范例。 当需要双向通信时,我们通常会使用套接字、unix 套接字甚至网络套接字。 然而,它简单易懂,可以在所有操作系统中开箱即用,无需处理套接字等等。 因此,即使有一些批评,我也可以理解。

HTTP+SSE / 可流式 HTTP

HTTP 传输是另一回事。 同一个错误有两个版本:HTTP+SSE(服务器发送事件)传输,它正在被使用带有 SSE 的 REST 语义的“可流式 HTTP”(一个自创的术语)所取代。 但最重要的是,还有很多额外的混乱和角落情况。 它可以总结为:“由于我们喜欢 SSE 进行 LLM 流式传输,因此我们不使用 WebSockets。 相反,我们实际上是在 SSE 之上实现 WebSockets,并将其称为“可流式 HTTP”,以使人们认为这是一种可以接受/已知的方式。” 他们在 modelcontextprotocol/pull/206 中讨论了 WebSockets 的问题(以及使用 Streamable HTTP 的原因),进行了一些非常奇怪的扭曲和稻草人论证来使用 WebSockets。 至少线程中的另一个人似乎同意我的观点:modelcontextprotocol/pull/206#issuecomment-2766559523

步入疯狂

我开始在 Golang 中实现一个 MCP 服务器。 没有官方的 Go SDK,我想了解该协议。 事实证明,这对心理健康来说是个错误......

警告信号...

查看 https://modelcontextprotocol.io,文档写得很差(所有 LLM 供应商似乎都在进行编写令人困惑的文档的内部竞争)。 该规范忽略或忽略了协议的重要方面,并且没有提供对话流程的示例。 事实上,整个网站似乎不是为了阅读标准而设计的; 相反,它会将您推向有关如何实现其 SDK 的教程。 所有示例服务器均以 Python 或 JavaScript 实现,目的是让您使用 stdio 在本地下载并运行它们。 对于您希望在其他人的计算机上运行的任何东西,Python 和 JavaScript 可能是最糟糕的语言选择之一。 作者似乎意识到了这一点,因为所有示例都以 Docker 容器的形式提供。

老实说……你上次运行 pip install 并且没有陷入依赖地狱是什么时候? 我认为 AI 领域的人只真正了解 Python,并且“好吧,它在我的计算机上工作”的方法仍然被认为是可接受的,我是否自命不凡/妄加评判? 任何尝试从 Hugging Face 运行任何东西的人都应该清楚地看到这一点。 如果您想在本地运行 MCP,您难道不希望使用 Rust、Go 甚至基于 VM 的选项(例如 Java 或 C#)等可移植语言吗?

问题所在

当我开始实现该协议时,我立即感到必须对其进行逆向工程。 SSE 部分的重要方面在文档中缺失,并且似乎还没有人实现“可流式 HTTP”; 甚至他们自己的工具(如 npx @modelcontextprotocol/inspector@latest)也没有。(公平地说,这可能是我自己的技术问题,拉取了错误的版本,因为几周后我再次检查时它已经可用。您还可以在 inspect.mcp.garden 找到版本,这可能更方便。) 一旦掌握了架构,您很快就会意识到实现 MCP 服务器或客户端可能是一项巨大的工作。 问题在于 SSE/Streamable HTTP 实现试图像套接字一样工作,模拟 stdio,但实际上不是,并且试图 一次性完成所有事情

HTTP+SSE 模式

modelcontextprotocol.io/specification/2024-11-05/basic/transportsHTTP+SSE 模式 中,为了实现全双工,客户端建立一个到(例如)GET /sse 的 SSE 会话以进行读取。 第一次读取提供了一个可以发布写入的 URL。 然后,客户端继续使用给定的端点进行写入,例如,对 POST /a-endpoint?session-id=1234 的请求。 服务器返回 202 Accepted,没有正文,并且请求的响应应该从 /sse 上预先存在的开放 SSE 连接中读取。

“可流式 HTTP” 模式

modelcontextprotocol.io/specification/2025-03-26/basic/transports“可流式 HTTP” 模式 中,他们意识到,与其在第一个请求中提供一个新的端点,不如使用 HTTP 标头作为会话 ID,并使用 REST 语义作为端点。 例如,GETPOST /mcp 可以打开一个 SSE 会话并返回一个 mcp-session-id=1234 HTTP 标头。 为了发送数据,客户端向 POST /mcp 发出请求,并添加 HTTP 标头 mcp-session-id=1234。 响应可能:

要结束会话,客户端可以发送也可以不发送带有标头 mcp-session-id=1234DELETE /mcp。 服务器必须维护状态,并且没有明确的方法知道客户端何时放弃会话,除非客户端很好地正确结束了它。

这对 SSE 模式意味着什么?

这是一个如此有问题的设计,我不知道从哪里开始。 虽然 SSE 模式的一些关键功能没有记录,但一旦您对其进行逆向工程,它就相当简单了。 但这仍然给服务器实现带来了巨大且不必要的负担,服务器实现需要“加入”跨调用的连接。 做任何实际的事情几乎都会迫使您使用消息队列来回复任何请求。 例如,以任何冗余方式运行服务器将意味着 SSE 流可能来自一个服务器到客户端,而请求被发送到完全不同的服务器。

这对 “可流式 HTTP” 意味着什么?

可流式 HTTP 方法将其提升到另一个层次,带来了一系列安全问题和模糊的控制流程。 虽然保留了 SSE 模式的所有不良部分,但 Streamable HTTP 似乎是 SSE 模式之上的一系列混乱的超集。 就实施而言,我只是触及了表面,但从我在文档中了解到的... 可以通过 3 种方式创建一个新会话:

可以通过 4 种不同的方式打开 SSE:

可以通过 3 种不同的方式回复一个请求:

一般影响

凭借其发起会话、打开 SSE 连接和响应请求的多种方式,这引入了显著的复杂性。 这种复杂性具有以下几个一般影响:

安全影响

可流式 HTTP 的“灵活性”引入了几个安全问题,这里只是一些:

授权

该协议的最新版本包含一些关于如何进行授权的非常主观的要求。 modelcontextprotocol.io/specification/2025-03-26/basic/authorization

  • 使用基于 HTTP 的传输的实现应该符合此规范。
  • 使用 STDIO 传输的实现不应遵循此规范,而应从环境中检索凭据。

我的理解是,对于 stdio,随便做什么。 对于 HTTP,您最好跳过这些 OAuth2 步骤。 如果我使用 HTTP 作为传输方式,为什么我需要实现 OAuth2,而 API 密钥对于 stdio 来说就足够了?

应该怎么做

我不知道,只是对此感到有点悲伤... 似乎整个行业都在尿裤子——现在感觉很棒,但以后会很难处理。 只有一个 JSON RPC 协议,并且 Stdio 显然是首选的传输协议。 然后,我们应该尝试使 HTTP 传输尽可能地像 Stdio,并且只有在我们真正、真正需要的时候才真正偏离。

真的就这些。 我们应该能够在 WebSockets 上完成与在 Stdio 上完成的相同的事情。 WebSockets 是通过 HTTP 进行传输的合适选择。 我们可以摆脱复杂的跨服务器会话状态管理。 我们可以摆脱大量的角落情况,等等。 当然,有些事情(例如授权)在某些情况下可能会有点复杂(并且在某些情况下更容易); 一些防火墙可能会阻止 WebSockets; 小会话可能会有额外的开销; 恢复中断的会话可能更难。 但正如_他们_所说:

客户端和服务器可以实现额外的自定义传输机制以满足其特定需求。 该协议与传输无关,可以在支持双向消息交换的任何通信通道上实现 modelcontextprotocol.io/specification/2025-03-26/basic/transports#custom-transports 作为一个行业,我们应该针对最常见的用例进行优化,而不是针对角落用例进行优化。

旁注:替代方案和补充

如上所述,似乎出现了更多的协议。 MCP 实际上是“一种向 LLM 公开 API 的协议”(它可以创建一个代理)。 来自 IBM 和 Google 的最新协议(ACP 和 A2A)实际上是“一种向 LLM 公开代理的协议”(它可以创建代理的代理)。 浏览 A2A 规范,似乎对它们的需求非常有限。 尽管他们声称是正交的,但 A2A 中的大多数事情都可以通过 MCP 按原样或通过少量添加来完成。 归结为两个完整的协议,它们也可以是 MCP 服务器中的工具。 甚至 IBM 似乎也承认他们的协议不是真正必要的:

“代理可以被视为 MCP 资源,并进一步作为 MCP 工具调用。 这种 ACP 代理的视图允许 MCP 客户端发现和运行 ACP 代理......” ― IBM / agentcommunicationprotocol.dev/ecosystem/mcp-adapter 我的最初感觉是,ACP 协议主要像是 IBM 试图推广他们的“代理构建工具”BeeAI A** 协议带给我们的都是一个理智的传输层和一种发现代理的方式。 14 competing standards