上个月连续加班三天,就为一个 MCP Remote Server 连不上。

具体症状:Claude Code 配置文件里写了远程服务的地址,启动之后 Agent 能正常加载 MCP Server 列表,但一调用工具就报 Connection refused。试了十几种方法——换端口、换协议、重启 Docker、重装依赖——全没用。后来发现原因蠢到我不想说:服务器上防火墙只开了 443 和 80,我配了个 3444 端口。

这事让我意识到,MCP 协议本身虽然设计得很优雅,但 Remote Server 的配置环节坑太多了。这篇把我的踩坑记录扒出来,给同样在折腾的人少走点弯路。

坑 1:端口被防火墙拦住,你却以为协议写错了

这是最蠢的一个坑,但最容易被忽略。

MCP Remote Server 默认走 SSE(Server-Sent Events),需要 Agent 端通过网络连接到服务端。这意味着服务端的端口必须对 Agent 开放。听起来像常识对吧?但你在本地开发时根本不会碰到这个问题——localhost 默认所有端口通的。等上到生产或者跨服务器调用时防火墙立马教做人。

// 你配的:
{
  "mcpServers": {
    "my-remote-server": {
      "url": "http://192.168.1.100:3444/sse"
    }
  }
}
// 服务器上 iptables -L 一看:
Chain INPUT (policy DROP)
target     prot opt source        destination
ACCEPT     tcp  --  0.0.0.0/0    tcp dpt:443
ACCEPT     tcp  --  0.0.0.0/0    tcp dpt:80
// 没有 3444

排查思路很简单:先在服务端 curl http://localhost:3444/sse,能通说明服务是好的。然后从客户端机器 telnet 192.168.1.100 3444,不通就是网络层的问题,跟协议和代码一点关系没有。

我的解决方式是把 MCP Remote Server 挂到反向代理后面,统一走 443 端口,用路径区分。比如 /mcp-server-1/sse/mcp-server-2/sse。这样既躲开端口限制,部署起来也清爽。

MCP remote server architecture diagram

坑 2:分不清 Stdio 和 SSE 的启动方式

MCP 协议有两个传输层:Stdio 和 SSE(Streamable HTTP)。

Stdio 是最早的方案——Agent 作为父进程启动一个子进程,通过 stdin/stdout 通信。好处是零网络配置,本地开发非常方便。坏处是远程根本用不了。

SSE 才是远程调用的正确姿势——Agent 通过 HTTP 连接到已经运行的服务端进程。但这个传输模式需要服务端程序明确实现 SSE 端点,不是所有 MCP Server SDK 都默认支持的。

我踩的坑是:本地用 Stdio 测得好好的,搬到另一台机器上照搬配置,把 command 改成 url 就以为完事了。结果 MCP Server 的 SDK 版本太旧,不支持 SSE 模式,服务端启动了但根本没有 /sse 端点。

// 本地 Stdio 模式(本地开发用这个):
{
  "mcpServers": {
    "db-server": {
      "command": "node",
      "args": ["mcp-server.js"]
    }
  }
}

// 远程 SSE 模式(跨服务器调用用这个):
{
  "mcpServers": {
    "db-server": {
      "url": "http://server-b:443/db-server/sse"
    }
  }
}

检查 SDK 版本:看服务端启动日志里有没有 SSE transport initializedListening on /sse。没有就说明 SDK 没开 SSE 支持。我升到最新版 TypeScript SDK 就解决了。

坑 3:加密和认证——谁都能调你的 MCP Server

MCP 协议本身不定义认证机制。这意味着你只要把 Remote Server 暴露到网络上,任何知道端点地址的人都能调用。没有 Token 验证,没有签名,裸奔。

前期我没在意——反正是内网服务。后来发现不对:K8s 集群里的 Pod 会互相扫描端口,别人测试环境的代码不小心发了个请求打到我的 MCP Server 上,执行了一条危险的数据库查询。还好那是只读工具,没造成损失,但吓得我立刻补了认证。

// 客户端加 Token:
{
  "mcpServers": {
    "secure-server": {
      "url": "https://internal:443/secure-server/sse",
      "headers": {
        "Authorization": "Bearer mcp_sk_xxxxx"
      }
    }
  }
}

// 服务端验证(Node.js 中间件):
app.use('/secure-server', (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (token !== process.env.MCP_API_KEY) {
    return res.status(401).json({ error: 'unauthorized' })
  }
  next()
})

官方已经在推 Streamable HTTP 传输层了,比纯 SSE 多了一些标准化的请求-响应模式,但认证这块还是得自己兜底。我现在的做法是所有 MCP Remote Server 都走 API Gateway,在网关层统一做认证和限流。

坑 4:超时设置太保守,大任务直接断

这坑隐蔽。本地开发测小任务——查一条数据、调用一个函数——毫秒级响应,根本不会触发超时。等上了生产,Agent 让它跑一个全量数据分析,跑了三分钟,连接断了。

MCP Remote Server 用的是 SSE 长连接。SSE 本身有重连机制,但前提是服务端得正确地发 keepalive 心跳。很多 SDK 实现里没开这个。结果 Agent 那边以为服务挂了,服务端还在跑。

// Node.js 服务端加心跳:
const express = require('express')
const app = express()

app.get('/sse', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  })
  // 每 15 秒发一次心跳
  const keepalive = setInterval(() => {
    res.write(':keepalive\n\n')
  }, 15000)
  req.on('close', () => clearInterval(keepalive))
})

还有一个坑是代理层超时。如果你的 MCP Server 前面挂了 Nginx 或者 Cloudflare,它们的默认超时可能只有 60 秒。大任务一跑就超。

# Nginx 加长超时:
proxy_read_timeout 300;
proxy_send_timeout 300;
proxy_buffering off;  # SSE 必须关缓冲

我个人的经验法则:内网直接调用超时设 60 秒够用,过代理至少 300 秒。任务超过 5 分钟的,不应该走同步调用,建议拆成异步任务模式。

坑 5:环境变量没传进去,MCP Server 启动就崩

这个坑特别小,但特别坑。

Stdio 模式下,你在 Claude Code 配置文件里写的 env 字段会被传给子进程的环境变量。但如果你用 Remote Server 模式,URL 远程连接的服务端进程并不是 Agent 启动的——你得自己在服务端那边管理环境变量。

有次我把数据库密码写在客户端的 env 字段里,心想"这下安全了吧"。结果 Remote Server 启动时压根没读到这些变量,数据库连不上,Server 启动就挂了,Agent 只报了一句 MCP server initialization failed

// 客户端这么写——对 Remote Server 来说只对了一半:
{
  "mcpServers": {
    "db-server": {
      "url": "http://server-b:443/db-server/sse",
      "env": {
        "DB_PASSWORD": "xxx"  // Remote 模式下这个字段被忽略了!!!
      }
    }
  }
}

Remote Server 的服务端进程自己有独立的环境。正确的做法是在服务端通过 .env 文件或容器环境变量来管理。env 字段只在 Stdio 模式有效。

理解这个逻辑之后就很简单了:Stdio 模式下 Agent 是你的父进程,变量由 Agent 注入;SSE 模式下 Agent 只是 HTTP 客户端,服务端的环境变量你得自己管。

一点小体会

MCP 协议本身的设计我很喜欢——它让 AI Agent 的能力边界从"回答问题"扩展到了"操作工具"。但 Remote Server 这部分还在快速演进中,SDK 版本之间差距大,文档分散,不同语言的实现各有各的小毛病。

我自己的做法是建了一个 mcp-server-template 的内部模板项目,把认证、心跳、日志、健康检查这些基础设施固化下来,新加一个远程服务直接从模板改。不用每次从头踩一遍。

远程调用这件事,本质上就是给你的 Agent 装了一条长臂。但这只手能不能伸过去、伸过去了能不能抓得住,取决于网络、协议、认证、超时、环境配置每一环都不掉链子。跟之前那篇说的 Copilot 到 Agent 的进化一样——工具越强,边角细节越多,每一处都可能炸。</

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐