首页 / 资讯中心 / 文章详情

Spring 自定义 MCP sse-endpoint:从配置骨架到联调验证

Spring 自定义 MCP sse-endpoint:从配置骨架到联调验证 ★ FEATURED ARTICLE
1. 为什么 Spring 里改 sse-endpoint 总是不生效如果你正在用 Spring AI 搭 MCP Server大概率踩过这个坑在application.yml里老老实实写了spring.ai.mcp.server.sse-endpoint: /demo/sse服务能正常启动日志也没报错但用 Postman 去请求http://localhost:8080/demo/sse返回的却是 404。换成默认的/sse反而通了。这不是你配置写错了而是当前 Spring AI 的 MCP 自动配置里sse-endpoint这个属性并没有被真正读取到WebMvcSseServerTransportProvider的构造参数里。换句话说YAML 里那个 key 只是个“摆设”框架内部始终用默认的/sse作为端点。这个问题在 MCP Client 侧同样存在——Client 的自动配置类里压根没有暴露自定义 sse-endpoint 的入口你想改都找不到地方改。这篇就围绕这个真实场景展开怎么在 Spring 项目里通过自定义Configuration覆盖官方自动配置让 Server 和 Client 两端都能用上你想要的 sse-endpoint并且把整条链路和统一 Key/API 通道TaoToken联调打通。适合已经能跑起 Spring Boot、正在接 MCP、需要把 AI 工具接到统一通道上的开发者。下面给的配置骨架可以直接复制改改包名就能用。2. 前置准备TaoToken 通道与依赖确认在动配置之前先把“通道”这件事理清楚。MCP 本身解决的是工具调用协议但模型请求最终要落到一个统一的 API 入口上。我这边习惯用 TaoToken 做统一 Key 管理好处是多个 AI 工具、多个项目共用一个 Key不用每个地方都去配一遍。你需要先拿到一个可用的 API Key。登录官网后进入控制台在 API Keys 页面创建一个新 Key复制出来备用。地址是官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content控制台 / API Keyshttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 的基础地址是https://taotoken.net/api这个不加 UTM直接用于代码里的 base_url。依赖方面确认你的pom.xml里有 Spring AI 的 MCP 相关 starter。以 WebMvc 为例核心是这两个dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency版本上建议用 1.0.0-M6 及之后的里程碑版本早期版本里WebMvcSseServerTransportProvider的构造函数签名不太一样照抄代码可能会编译不过。如果你不确定自己用的版本先在 IDE 里点进这个类看一眼构造参数再对照下面的代码调整。注意MCP 的 SSE 端点和普通 REST 端点不一样它是长连接 事件流Postman 请求时要用 GET 并且保持连接不要用普通的短请求去测。3. 可复制配置Server 端自定义 sse-endpoint 骨架先解决 Server 端。核心思路是自己写一个Configuration手动 new 一个WebMvcSseServerTransportProvider把从McpServerProperties里读到的sseEndpoint和sseMessageEndpoint传进去并用Primary让它覆盖官方自动配置里的那个 Bean。package com.example.mcp.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; import org.springframework.ai.mcp.server.webmvc.transport.WebMvcSseServerTransportProvider; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; Configuration public class MyMcpServerConfig { Bean Primary public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider( ObjectProviderObjectMapper objectMapperProvider, McpServerProperties serverProperties) { ObjectMapper objectMapper objectMapperProvider.getIfAvailable(ObjectMapper::new); return new WebMvcSseServerTransportProvider( objectMapper, serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint()); } Bean public RouterFunctionServerResponse mvcMcpRouterFunction( WebMvcSseServerTransportProvider transportProvider) { return transportProvider.getRouterFunction(); } }对应的application.yml配置spring: ai: mcp: server: name: my-mcp-server version: 1.0.0 sse-endpoint: /demo/sse sse-message-endpoint: /demo/mcp/message这里有两个点容易忽略。第一sse-message-endpoint也要一起配因为 SSE 是双向的客户端发消息走的是 message 端点只改 sse-endpoint 会导致握手成功但消息发不出去。第二Primary必须加否则容器里会有两个同类型的WebMvcSseServerTransportProvider启动时直接报 Bean 冲突。改完之后重启再用 Postman 发一个 GET 请求到http://localhost:8080/demo/sse你会看到连接保持住并返回event: endpoint开头的事件流这就说明自定义端点生效了。4. Client 端同步自定义 SseClientProperties 与排除自动配置Server 端通了不代表 Client 端能用。Client 的自动配置类SseHttpClientTransportAutoConfiguration里读取的是McpSseClientProperties而这个类里根本没有sseEndpoint这个字段所以你在 YAML 里写sse-endpoint它也不认。解决办法是自己定义一个属性类再写一个配置类手动构建 transport。先定义属性类package com.example.mcp.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; Component ConfigurationProperties(prefix spring.ai.mcp.client) public class SseClientProperties { private final MapString, Connection connections new HashMap(); public MapString, Connection getConnections() { return connections; } public static class Connection { private String url; private String sseEndpoint; public String getUrl() { return url; } public void setUrl(String url) { this.url url; } public String getSseEndpoint() { return sseEndpoint; } public void setSseEndpoint(String sseEndpoint) { this.sseEndpoint sseEndpoint; } } }再写 Client 配置类package com.example.mcp.config; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.NamedClientMcpTransport; import org.springframework.ai.mcp.client.autoconfigure.properties.McpSseClientProperties; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.net.http.HttpClient; import java.util.ArrayList; import java.util.List; import java.util.Map; Configuration public class MyMcpSseConfig { private final SseClientProperties sseClientProperties; public MyMcpSseConfig(SseClientProperties sseClientProperties) { this.sseClientProperties sseClientProperties; } Bean public ListNamedClientMcpTransport mcpHttpClientTransports( ObjectProviderObjectMapper objectMapperProvider) { ObjectMapper objectMapper objectMapperProvider.getIfAvailable(ObjectMapper::new); ListNamedClientMcpTransport transports new ArrayList(); for (Map.EntryString, SseClientProperties.Connection entry : sseClientProperties.getConnections().entrySet()) { SseClientProperties.Connection conn entry.getValue(); var transport new HttpClientSseClientTransport( HttpClient.newBuilder(), conn.getUrl(), conn.getSseEndpoint(), objectMapper); transports.add(new NamedClientMcpTransport(entry.getKey(), transport)); } return transports; } }YAML 里对应加spring: ai: mcp: client: connections: my-server: url: http://localhost:8080 sse-endpoint: /demo/sse最后一步很关键官方自动配置类SseHttpClientTransportAutoConfiguration也会注册一个mcpHttpClientTransportsBean和你自己写的这个冲突。必须在启动类上把它排除掉SpringBootApplication(exclude { org.springframework.ai.mcp.client.autoconfigure.SseHttpClientTransportAutoConfiguration.class }) public class McpDemoApplication { public static void main(String[] args) { SpringApplication.run(McpDemoApplication.class, args); } }排除之后重启控制台不再报 Bean 重复注册的错误Client 就会用你自定义的 sse-endpoint 去连 Server 了。5. 联调验证从 Postman 到模型对话的完整链路配置写完得验证整条链路真的通。分三步走。第一步验证 Server 端点。用 Postman 新建一个 GET 请求地址http://localhost:8080/demo/sseHeaders 里加Accept: text/event-stream。发送后不要断开你会看到响应体里持续输出事件流第一行通常是event: endpoint后面跟着data:/demo/mcp/message?sessionIdxxx。这说明 SSE 握手成功Server 已经准备好接收消息。第二步验证 Client 能拉到工具列表。启动 Client 应用观察日志里有没有Discovered tools之类的输出。如果 Client 成功连上 Server它会自动调用tools/list并把 Server 里注册的工具拉过来。你可以在 Server 端写一个简单的Tool方法做测试Component public class DemoTools { Tool(description 返回两个整数之和) public int add(int a, int b) { return a b; } }第三步走一次真实的模型对话。在 Client 端调用模型时把 base_url 指向 TaoToken 的 API 地址Key 用你在控制台创建的那个。配置大概长这样spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini然后发一句“帮我算一下 3 加 5 等于几”如果模型正确调用了add工具并返回 8说明 MCP 工具调用 统一通道整条链路都通了。想单独验证模型通道是否正常可以直接用模型对话页面发一条消息测试https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你打算长期跑编码类 Agent反复调工具、跑多轮对话建议用 Coding Plan 来管理额度比按次调用更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content6. 本篇常见报错排查404 Not FoundServer 端最常见的原因是只改了 YAML 没写自定义 Config或者 Config 里忘了加Primary。检查WebMvcSseServerTransportProvider这个 Bean 是不是你自定义的那个在生效可以在启动日志里加断点确认。Bean 重复注册 / NoUniqueBeanDefinitionExceptionServer 端是WebMvcSseServerTransportProvider冲突Client 端是mcpHttpClientTransports冲突。前者加Primary后者在启动类exclude掉官方自动配置类。Client 连不上 Server日志报 connection refused先确认 Server 的url和sse-endpoint拼起来是不是完整地址。注意url只写到端口sse-endpoint以/开头两者拼接后才是完整路径。另外确认 Server 和 Client 不在同一个端口上避免自己连自己。SSE 握手成功但工具调用无响应大概率是sse-message-endpoint没配或者配错了。SSE 是单向事件流客户端发消息走的是 message 端点两个端点必须成对配置且路径不要重复。模型请求返回 401检查 TaoToken 的 API Key 是否复制完整以及base-url是不是https://taotoken.net/api。如果 Key 没问题去控制台确认一下这个 Key 的额度是否还有剩余。编译报错找不到 HttpClientSseClientTransport 构造函数这是 Spring AI 版本差异导致的。M6 之前和之后的构造函数参数顺序、个数都可能不同。解决办法是点进这个类看当前版本的构造签名按实际参数调整不要硬套本文代码。排查完这些基本就能稳定跑起来了。接入相关的完整参数说明可以对照官方文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content
阅读完成 · 觉得有帮助?
咨询建站