一、问题描述

我有多个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服务部署:
  1. 博查AI联网搜索
  2. 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 流里读

五、测试验证

  1. 重载 Nginx:
sudo nginx -t && sudo nginx -s reload
  1. 手动 curl 测试 SSE 流:
curl http://localhost:7777/mcp/lwss/sse
  1. 观察输出是否类似:
event: endpoint
data: /mcp/lwss/messages/?session_id=abc123

而不是原来的:

data: /messages/?session_id=abc123
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐