local MCP(stdio)和remote MCP(HTTP)的本质区别在于谁运行代码和密钥存储位置,直接决定安全模型和部署形态。
MCP 服务器有两类,它们解决的是不同的问题,但在足够深入之前,几乎没有任何资料告诉你你正在构建的是哪一种。
我是从一个提交表单上搞明白这一点的。详见下文,因为事实证明这是整个生态里最清晰的信号,却被埋在一个脚注里。
本地(stdio)。服务器作为进程运行在用户自己的机器上。客户端——无论是 Claude Desktop、Cursor 还是其他——启动它,并通过 stdin/stdout 与它通信。它是用户安装的一个包。
远程(Streamable HTTP)。服务器是你托管的一个服务。客户端通过 Authorization header 中的 token 连接到一个 URL。本地不安装任何东西。
这就是全部区别,其他一切都是由此决定的。
谁运行代码。本地:用户在自己的硬件上运行。远程:你在你的基础设施上运行。这才是真正的决策点。以下所有考量都由此而生。
密钥存在哪里。本地服务器从用户自己的环境中读取凭证——他们的 shell 配置文件、他们的配置文件。你永远接触不到。远程服务器则要求用户持有你颁发的 token,这意味着你拥有整个凭证生命周期:颁发、限定范围、轮换、吊销。
服务器能访问什么。本地服务器可以读取用户的文件系统、访问 localhost、与他们的 Docker 守护进程通信。远程服务器看不到这些,而且也不应该想看到。
更新路径。远程:你部署,所有人立即用上新版本。本地:用户运行的是他们安装时的那个版本,可能永远不更新。
故障面。本地服务器只影响一台机器。远程服务器一出问题,所有人同时受影响。自选毒药吧。
选本地,如果你需要访问用户的文件系统、本地进程、本地数据库或硬件。或者如果数据必须留在用户机器上。
选远程,如果服务器对接的是你已经运行的服务。如果你的 MCP 服务器的职责是调用你自己的 API,让用户安装一个进程来代理到你的 HTTP 端点纯粹是多余的开销——你实际上是在一个远程调用的外面包了一层本地包装,然后现在要同时维护两者。
经验法则:如果数据在你的基础设施上,服务器就应该是远程的。如果数据在用户的机器上,就应该是本地的。
Anthropic 维护着一个 MCP 服务器目录,他们有两个独立的提交表单——一个给本地,一个给远程。本地那个是围绕 Desktop Extensions 设计的,其声明的要求是:
.mcpb 文件,作为必需的上传项最后一条才是关键。.mcpb 是 Claude Desktop 安装和运行的打包本地扩展。如果你的服务器是远程的,根本没有包可以产生——而这个必填文件字段就是让你发现自己填错表单的地方。
远程表单的链接藏在本表单顶部附近的一行里,很容易一眼扫过。
所以:在填表之前先看清楚传输层要求。如果一个表单要求你提供 .mcpb,或者把 Node.js 列为硬性要求,那它要的是本地服务器。被拒的提交比慢一点但正确的提交更糟糕。
远程意味着你拥有凭证,所以有几件事不再是可选项。
token 范围要尽量窄。一个只能读取单个任务的上下文的 token,泄露时造成的问题远小于一个可以作为用户整个账户行事的 token。
把过期设为默认值,而不是吊销。"可吊销"意味着要靠某人记得去操作。设置过期意味着疏忽是无害的。为会忘记的人设计,因为那个人就是你自己。
永远不要让 token 出现在已提交的文件里。在配置中引用环境变量,然后在 shell 配置文件中导出真实值。一旦凭证到达远程分支,删除那一行毫无用处——值已经在历史记录里了,轮换是唯一真正的解决办法。
我在做 Wagglet,它是远程的。服务器对接的是一个共享任务看板,所以数据是我们的而不是用户的。如果选本地,就意味着要交付一个唯一职责是代理 HTTP 调用到我们自己的 API 的进程,再加上一个打包的 bundle,再加上版本偏斜问题——换来的却是零收益。
文档里有关于连接和凭证模型的内容,如果你想要一个远程端的完整示例可以参考。
披露:我在 Wagglet 工作。上述本地/远程决策的部分是值得你无论构建什么都吸取的经验,不管你在做什么。
数据在你的基础设施上,走远程。数据在用户的机器上,走本地。
在填写任何目录表单之前,先检查它是否要 .mcpb。这是最快判断你站在哪扇门前面的方法。