MCP多路径nginx转发
一、问题描述
我有多个MCP服务,但端口有限,所以需要在一个端口下实现多个路径的服务转发
http://your-host:7777/mcp/<service_id>/sse
http://your-host:7777/mcp/<service_id>/messages/
但问题是我的客户端会发生这样的情况 当开发者尝试通过FastMCP客户端连接到一个基于SSE(Server-Sent Events)的MCP服务器时,客户端未能正确使用配置的基础路径。具体表现为:虽然初始连接使用了正确的基础路径(如http://127.0.0.1:8080/function/mcp/sse),但后续的消息端点请求却错误地跳过了基础路径部分,直接访问了/messages/路径。
例如:
curl https://xxx.xx.xx.xx/mcp/0c6864a6b8a948/sse
# 返回:
event: endpoint
data: /messages/?session_id=8d933235011c45f8abe28e34e55528b0
如此导致必须在nginx中明确的配置location,这就导致“一个端口对应一个服务”,极大的限制了服务的使用——失败
location messages {
proxy_pass http://10.214.81.51:30075/messages;
proxy_set_header Host xxxx.cn;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 可开启缓冲(提高性能)
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 64k;
# 设置合理超时(防止 504)
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # 如果处理时间较长,比如图像审核
# 不强制修改 Content-Type(让后端自己决定)
}
二、问题分析
问题本质
FastMCP 客户端(或底层 SSE 库)在收到
event: endpoint后,错误地将返回的data: /messages/...解析为“绝对根路径”,而不是“相对于当前 SSE 连接 URL 的路径”。
这导致:
- 初始连接:
http://127.0.0.1:8080/function/mcp/sse - 收到 endpoint:
data: /messages/?session_id=xxx - 客户端错误拼接成:
http://127.0.0.1:8080/messages/?session_id=xxx
(丢失了/function/mcp前缀)
而正确行为应是:
http://127.0.0.1:8080/function/mcp/messages/?session_id=xxx
根本原因
SSE 协议本身 没有规定 endpoint 事件的语义,这是 FastMCP 自定义的协商机制。
但很多客户端(包括某些 FastMCP SDK 实现)默认将 /messages/... 视为绝对路径,而非相对路径。
💡 根据 MCP over SSE 规范草案(非官方但社区通用):
endpoint事件中的路径 应被解释为相对于当前 SSE 连接 URL 的路径(relative path)- 但 部分客户端实现未遵循此约定
解决方案 (从服务端入手,兼容所有客户端)
最佳实践:让服务端返回完整 URL,而不是相对路径
这样无论客户端如何解析,都能正确工作。
三、解决方案
1.原理分析
在 Nginx 层“欺骗”客户端 —— 重写 endpoint 响应内容
利用 Nginx 的 sub_filter 模块,在返回给客户端前,把 /messages/... 替换成完整路径。
⚠️ 注意:
sub_filter默认只对text/html生效,需强制开启对text/event-stream的过滤。
Nginx 配置示例:
server {
listen 7777;
# 提取 service_id 并用于重写
location ~ ^/mcp/(?<service_id>[^/]+)/sse$ {
proxy_pass http://172.18.179.194:30032/sse;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
# === 关键:重写 endpoint 中的路径 ===
sub_filter_types text/event-stream; # 允许过滤 SSE 流
sub_filter '/messages/' '/mcp/$service_id/messages/';
sub_filter_once off; # 多次替换
}
location ~ ^/mcp/(?<service_id>[^/]+)/messages/(.*)$ {
proxy_pass http://172.18.179.194:30032/messages/$1?$args;
# ... 其他 proxy 设置(同上)
}
}
效果:
- FastMCP 返回:
data: /messages/?session_id=abc - Nginx 自动替换为:
data: /mcp/lwss/messages/?session_id=abc - 客户端拼接后得到完整正确 URL
2.本地实践案例:
(1)mcp服务部署:
- 博查AI联网搜索
- Mysql数据查询
(2)nginx配置(重点)
server {
listen 7777;
location /mcp/lwss/sse {
proxy_pass http://172.18.179.194:30032/sse;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
# === 新增:重写 endpoint 路径 ===
sub_filter_types text/event-stream;
sub_filter '/messages/' '/mcp/lwss/messages/';
sub_filter_once off;
}
location /mcp/lwss/messages/ {
proxy_pass http://172.18.179.194:30032/messages/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
location /mcp/mysql/sse {
proxy_pass http://172.18.179.194:30033/sse;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
# === 新增:重写 endpoint 路径 ===
sub_filter_types text/event-stream;
sub_filter '/messages/' '/mcp/mysql/messages/';
sub_filter_once off;
}
location /mcp/mysql/messages/ {
proxy_pass http://172.18.179.194:30033/messages/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}
四、nginx参数讲解
| 行 | 作用 |
|---|---|
sub_filter_types text/event-stream; |
允许对 SSE 流(Content-Type: text/event-stream)执行文本替换 |
sub_filter '/messages/' '/mcp/lwss/messages/'; |
将 FastMCP 返回的 /messages/... 替换为带前缀的路径 |
sub_filter_once off; |
确保所有匹配项都被替换(虽然通常只出现一次) |
💡 这个替换发生在 Nginx 返回响应给客户端之前,FastMCP 后端完全无感知。
- SSE 流是从
/mcp/lwss/sse这个接口返回的。 - 在这个流中,服务器会发送一个事件:
event: endpoint
data: /messages/?session_id=xxx
- 我们希望客户端收到的是:
data: /mcp/lwss/messages/?session_id=xxx
所以,需要被重写的 HTTP 响应,是
/sse接口的响应体。
客户端根本不会从/messages/接口读取endpoint信息 —— 它只从/sse流里读
五、测试验证
- 重载 Nginx:
sudo nginx -t && sudo nginx -s reload
- 手动 curl 测试 SSE 流:
curl http://localhost:7777/mcp/lwss/sse
- 观察输出是否类似:
event: endpoint
data: /mcp/lwss/messages/?session_id=abc123
而不是原来的:
data: /messages/?session_id=abc123
更多推荐

所有评论(0)