当前位置: 首页 > news >正文

远程 MCP Server——SSE 传输与生产部署

前几节大家把 MCP Server 跑起来了,用的都是 stdio 模式——Cursor 或 Client 直接在本地启动 Server 进程,通过 stdin/stdout 通信。本地开发没问题,但一到生产环境就卡住了:工具服务要独立部署,多个 Agent 都能调用,stdio 就不够用了。

这节讲 SSE 模式——把 MCP Server 变成一个普通的 HTTP 服务,任意 Client 都可以通过网络连进来。

一、stdio 和 SSE 的本质区别

我用一张图说清楚:

stdio 模式:

  • Host/Client 进程 → fork → Server 子进程

  • 通过 stdin/stdout 通信

  • Server 和 Client 必须在同一台机器

  • 一个 Server 只服务一个 Client

SSE 模式:

  • MCP Server 独立部署(标准 Spring Boot Web 服务)

  • Client 通过 HTTP 长连接(SSE)接收推送

  • Server 独立运行,可以同时服务多个 Client

  • Server 挂了不影响 Client 进程

简单说:stdio 是本地管道,SSE 是 HTTP 服务。大家在生产环境基本都会走 SSE。

二、SSE MCP Server 实现

直接在之前的mcp-tools-server项目上改,不用新建项目。工具代码一行都不用改,这是 Spring AI MCP 设计得很好的地方。

第一步:换依赖

pom.xml把原来的spring-ai-starter-mcp-server换成spring-ai-starter-mcp-server-webmvc,webmvc 变体已经内置了 Web 容器,不需要再单独加spring-boot-starter-web

<!-- 去掉原来的 spring-ai-starter-mcp-server --> <!-- 换成 webmvc 变体 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency>

第二步:改配置

application.yml去掉web-application-type: none(SSE 模式需要启动 Web 容器),其他加上端口和 MCP 配置:

server: port: 8090 # MCP Server 独立端口,避开 Agent 应用的 8080 spring: ai: mcp: server: name: jichi-remote-tools version: 1.0.0 type: SYNC sse-message-endpoint: /mcp/messages # SSE 消息端点路径 logging: config: classpath:logback-spring.xml # SSE 模式日志不用限制,正常输出就行

主类和工具类不变,启动后会自动暴露两个端点:

  • GET /sse:Client 建立 SSE 长连接,等待服务器推送

  • POST /mcp/messages:Client 发送请求(工具调用等)

常见问题:改完启动报错或 curl 显示 ECONNREFUSED

检查两个地方:

  1. application.yml里有没有留着web-application-type: none——SSE 模式必须删掉这行,否则 Web 容器不启动

  2. pom.xml有没有加spring-boot-starter-web

两项都确认后mvn clean package重新打包,启动日志里出现Tomcat started on port 8090说明 Server 正常了。

验证服务是否启动正常:

# 访问 SSE 端点,正常会挂起等待(说明服务在跑) curl http://localhost:8090/sse

三、SSE MCP Client

在之前的mcp-tools-client项目里新增文件,把连接本地 Server 的 stdio 传输层换成 SSE 传输层。

注意:项目里原来的LocalMcpClientConfigThirdPartyMcpConfig里的@Bean要先屏蔽掉,否则 Spring 启动时会同时初始化 stdio 连接,找不到本地 jar 就报 Stream closed。最简单的方式是把旧配置类的@Bean注释掉,或者给两套配置加@Profile区分。

新建RemoteMcpClientConfig.java

package com.jichi.mcp.client; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.spec.McpSchema; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @Slf4j public class RemoteMcpClientConfig { @Bean public McpSyncClient remoteToolsClient() { // SSE 传输层:只需要传 Server 的 URL HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder("http://localhost:8090") .build(); McpSyncClient client = McpClient.sync(transport) .clientInfo(new McpSchema.Implementation("jichi-agent", "1.0.0")) .build(); // initialize() 内部自动完成握手通知,不需要额外调用 McpSchema.InitializeResult result = client.initialize(); log.info("[MCP] 已连接远程 Server:{} v{}", result.serverInfo().name(), result.serverInfo().version()); return client; } }

对应的测试 Controller:

package com.jichi.mcp.client; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpSchema; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/remote-mcp") @RequiredArgsConstructor public class RemoteMcpController { private final McpSyncClient remoteToolsClient; @GetMapping("/tools") public List<String> listTools() { return remoteToolsClient.listTools().tools().stream() .map(t -> t.name() + ":" + t.description()) .toList(); } @PostMapping("/call") public String callTool( @RequestParam String toolName, @RequestBody Map<String, Object> args) { McpSchema.CallToolResult result = remoteToolsClient.callTool( new McpSchema.CallToolRequest(toolName, args)); return result.content().stream() .filter(c -> c instanceof McpSchema.TextContent) .map(c -> ((McpSchema.TextContent) c).text()) .findFirst().orElse("(无返回内容)"); } }

测试:

# 查看远程 Server 提供的工具 curl http://localhost:8080/api/remote-mcp/tools # 调用远程工具 curl -X POST "http://localhost:8080/api/remote-mcp/call?toolName=getDateInfo" \ -H "Content-Type: application/json" \ -d '{}'

四、认证与安全

内网环境一般靠网络隔离,对外暴露的 MCP Server 就需要加认证了。我给两个方案,按场景选。

4.1 API Key(简单直接,够用就好)

在 Server 侧加一个 Filter,通过FilterRegistrationBean显式注册,明确指定拦截路径,更可靠:

package com.jichi.mcp.server; import jakarta.servlet.*; import jakarta.servlet.http.*; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j public class McpApiKeyFilter implements Filter { private final String validApiKey; public McpApiKeyFilter(String validApiKey) { this.validApiKey = validApiKey; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) request; String apiKey = httpReq.getHeader("X-API-Key"); if (!validApiKey.equals(apiKey)) { log.warn("[MCP] 非法访问,来自 {}", httpReq.getRemoteAddr()); ((HttpServletResponse) response).sendError(401, "Invalid API Key"); return; } chain.doFilter(request, response); } }

在配置类里注册,并绑定到 MCP 相关路径:

package com.jichi.mcp.server; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpSecurityConfig { @Bean public FilterRegistrationBean<McpApiKeyFilter> mcpApiKeyFilter() { String apiKey = System.getenv("MCP_API_KEY"); if (apiKey == null) { throw new IllegalStateException("环境变量 MCP_API_KEY 未设置"); } FilterRegistrationBean<McpApiKeyFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new McpApiKeyFilter(apiKey)); registration.addUrlPatterns("/sse", "/mcp/*"); // 只拦截 MCP 端点 registration.setOrder(1); registration.setName("mcpApiKeyFilter"); return registration; } }

Client 连接时带上 Header:

// 注意:MCP_API_KEY 环境变量必须设置,否则 header value 为 null 会抛 NullPointerException String apiKey = System.getenv("MCP_API_KEY"); HttpClientSseClientTransport.Builder transportBuilder = HttpClientSseClientTransport.builder("http://localhost:8090"); if (apiKey != null) { transportBuilder.customizeRequest(builder -> builder.header("X-API-Key", apiKey)); } HttpClientSseClientTransport transport = transportBuilder.build();

本地测试时:如果 Server 没有开 API Key 认证,不要加customizeRequest,直接.build()即可。

五、stdio 还是 SSE,怎么选

我给大家一个简单的判断标准:

用 stdio:

  • 本地开发调试

  • Cursor 本地接入

  • 工具只需要在本机运行

  • 不想折腾部署

用 SSE:

  • 生产环境部署

  • 多个 Agent 共享同一批工具

  • 工具服务要独立升级(不影响 Agent 应用重启)

  • 工具需要访问内网资源(数据库、内部 API)

  • 需要做认证鉴权

大家在公司做项目,基本都是 SSE。个人 Cursor 插件用 stdio 就够了。

http://www.cnnetsun.cn/news/3008582.html

相关文章:

  • B站视频转换终极指南:如何用m4s-converter一键保存珍贵内容
  • 开源PLC编程终极指南:如何用OpenPLC Editor零成本掌握工业自动化
  • iPhone本地大模型实战:Gemma 2量化部署与Core ML优化指南
  • 别天天只知道群发!教你 搭建个人微信增量语料库,低成本喂饱本地大模型
  • 大模型离题现象解析:区别于幻觉的隐蔽性语义漂移
  • 知识点之项目中的 Embedding 模型如何选型?
  • IntelliJ IDEA Ubuntu安装卡在“Loading plugins…”?——Plugin Repository证书链失效、APT代理劫持与DNSSEC验证失败三重故障定位法
  • 【源码解析】musl libc 中 shmget/shmctl 的三层兼容设计
  • 深入理解 ftok:从源码手写一个 IPC key 生成函数
  • Web测试入门:从手工到自动化,构建你的测试知识体系与实战项目
  • OpenHarmony学习笔记【总篇:从入门到放弃】
  • musl libc 中 exit() 的实现:一行代码背后的并发哲学
  • 3大价值维度+5级能力跃迁:Chat2DB从开源工具到企业级数据管理平台的演进路径
  • LLaMA泄露事件:基础大模型治理的临界点与实践启示
  • 3步掌握文档下载:彻底解决30+平台付费限制难题
  • 【小白向】一键部署 OpenClaw v2.7.9,零基础快速搭建本地自动化 AI 智能体(最新安装包)
  • AppAuth-Android安全加固实战:防御中间人攻击与数据泄露
  • Node-Forge深度指南:JavaScript跨平台加密与TLS协议实践
  • Python恶意样本分析实战:从伪装到行为还原
  • 基于Hugging Face的自适应概念解释系统设计
  • 如何用ColorControl:一款免费开源的多设备显示管理工具,彻底告别繁琐的设备切换操作?
  • 如何构建企业级在线考试平台:学之思开源系统的架构深度解析
  • Sherlock.js 终极指南:如何用自然语言解析JavaScript事件
  • PPTist:免费网页版PPT制作工具,3分钟快速创建专业演示文稿
  • 2026年,GEO优化为何成为企业必争之地?源码开源揭秘
  • 计算机毕业设计之“明丽书屋”图书管理系统
  • Apache Spark完整指南:从零开始掌握大数据处理的终极武器
  • 嵌入式内存控制器UPM编程:RAM Word微指令深度解析与应用实践
  • Java数组深度解析:从基础到架构的实战指南(下)
  • Facebook出海营销新突破:三不限账户全解析