1. Spring Boot 项目里为什么还要塞一个 Solon MCP Server
很多同学第一次听到「Spring Boot 集成 Solon MCP Server」会有点懵:Spring Boot 本身就能写 HTTP 接口,为什么还要在同一个 JVM 里再拉一个 Solon 实例?答案在于 MCP(Model Context Protocol)这套协议目前 Java 生态里落地最顺手的实现是solon-ai-mcp,它把 SSE 长连接、Tool 注册、参数描述这些细节都封装好了,而 Spring Boot 官方并没有对等的 MCP Server 组件。于是现实中的做法就是:CRM、订单、工单这些业务逻辑继续留在 Spring Boot 的 Tomcat 里跑,MCP 这一层单独用 Solon 起一个轻量 HTTP Server,两个框架各占一个端口,互不干扰。
这个场景特别适合已有 Java 8 老项目、又想快速让 AI 客户端(比如 Claude Desktop、Cline、Codex 这类支持 MCP 的工具)调用内部业务能力的团队。你不需要重构现有 Controller,也不用把 Spring 容器改造成 Solon 容器,只要在启动阶段手动拉起一个 Solon 实例,把@McpServerEndpoint标注的类扫描进去就行。听起来简单,但真正动手时会撞上三个坑:依赖缺 HTTP Server、Reactor 版本太旧没有Sinks类、以及 Solon 端口被 Spring Boot 的application.yml覆盖。这篇就把完整链路和排障过程一次讲清楚,最后再把 MCP 端点改到 TaoToken 统一通道做一次连通性验证。
先说清楚两个框架的边界。Spring Boot 用的是@RestController、@Service、Tomcat 线程池;Solon 用的是@McpServerEndpoint、@ToolMapping、SmartHttp。它们的注解体系、IoC 容器、HTTP 服务器完全独立,你不能指望在 Spring 的@RestController上贴一个@ToolMapping就能生效。正确姿势是让 Solon 自己扫描自己的组件,Spring 只负责在合适的生命周期节点把 Solon 拉起来。这个「合适的节点」就是@PostConstruct——此时 Spring 的 Bean 已经创建,端口也解析完了,拉起 Solon 不会影响主服务的 8080。
还有一个容易被忽略的点:MCP 的 SSE 传输层依赖 Reactor 的Sinks类,而 Spring Boot 2.x 默认管理的reactor-core版本低于 3.4.0,根本没有这个类。你如果只加solon-ai-mcp而不显式指定 Reactor 版本,启动时就会看到NoClassDefFoundError: reactor/core/publisher/Sinks。这个报错后面会专门讲怎么排查。
2. 接入前的依赖与 TaoToken 通道准备
在写代码之前,先把 Maven 依赖和 TaoToken 的接入信息准备好。依赖这块有三个是必须的,缺一个都跑不起来。solon-ai-mcp提供 MCP 协议逻辑和注解,solon-boot-smarthttp提供内嵌 HTTP Server(基于 Jetty),reactor-core提供 SSE 传输层需要的Sinks。版本上solon-ai-mcp和solon-boot-smarthttp用 3.2.0,reactor-core必须显式写 3.4.38,不能交给 Spring Boot 的 dependencyManagement 去管。
<!-- Solon MCP 协议实现 --> <dependency> <groupId>org.noear</groupId> <artifactId>solon-ai-mcp</artifactId> <version>3.2.0</version> </dependency> <!-- Solon 内嵌 HTTP 服务器(基于 Jetty) --> <dependency> <groupId>org.noear</groupId> <artifactId>solon-boot-smarthttp</artifactId> <version>3.2.0</version> </dependency> <!-- MCP SSE 传输层依赖 Reactor(必须 >= 3.4.0 才有 Sinks 类) --> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> <version>3.4.38</version> </dependency>为什么reactor-core一定要写死版本?因为 Spring Boot 2.x 的 BOM 里管理的 Reactor 版本普遍在 3.3.x,没有Sinks。你如果只写groupId和artifactId,Maven 会从父 POM 继承一个旧版本,编译能过,运行就炸。这个坑我在一个 Java 8 的 CRM 项目里踩过,日志里只有一行NoClassDefFoundError,排查了半天才发现是版本被覆盖了。
接下来是 TaoToken 通道的准备。TaoToken 在这里的角色是统一入口:你本地 Solon 起的 MCP Server 通过 SSE 暴露工具,AI 客户端侧则通过 TaoToken 的兼容端点来发起模型对话和工具调用,这样密钥、模型 ID、Base URL 都在一处管理,不用在每个客户端里散落配置。你需要先去控制台拿一个 API Key,地址是 https://taotoken.net/api-keys ,拿到的 Key 形如sk-开头的一串字符,后面配置里会用到。
模型 ID 这块,如果你只是验证 MCP 工具调用链路,用claude-sonnet-4-5这类支持工具调用的模型就行;如果是长期跑编码 Agent,可以看 Coding Plan 页面 https://taotoken.net/coding-plan 里的套餐说明。Base URL 统一用 https://taotoken.net/api ,注意这个地址后面不要加斜杠,也不要加/v1,具体路径由客户端自己拼。文档在 https://taotoken.net/doc ,遇到参数不确定的时候翻一下比猜快。
把这三样东西记下来:Base URL、API Key、Model ID。后面无论是配 Claude Desktop、Cline 还是 Codex 的auth.json,都是围绕这三个值展开的。这里先不展开客户端配置,等 Solon 这边跑通了再回头接。
3. 可复制的 Solon 启动配置与 MCP 端点定义
这一节是核心,直接给可复制的代码。先看 Solon 的启动配置类,思路是利用 Spring 的@PostConstruct在 Bean 初始化阶段拉起 Solon,并且临时把server.port系统属性设成 8088,启动完再恢复,避免影响 Spring Boot 自己的 8080。
@Slf4j @Configuration public class McpServerConfig { @PostConstruct public void init() { log.info("【McpServer】启动中..."); // 临时设置 Solon 端口,避免被 application.yml 的 server.port 覆盖 String originalPort = System.getProperty("server.port"); System.setProperty("server.port", "8088"); try { Solon.start(McpServerConfig.class, new String[]{}); } finally { // 恢复原值,避免影响 Spring Boot 端口 if (originalPort != null) { System.setProperty("server.port", originalPort); } else { System.clearProperty("server.port"); } } log.info("【McpServer】启动完成..."); } }为什么要用系统属性而不是命令行参数?因为 Solon 会读取 classpath 下的application.yml,里面如果写了server.port: 8080,会直接覆盖app.yml的配置;而命令行参数--server.port=8088在实测中 Solon 没有正确识别。系统属性在 Solon 的配置加载优先级里最高,且@PostConstruct执行时 Spring Boot 已经完成端口解析,改这个属性不会影响主服务。
然后是 MCP 端点定义。这个类不需要 Spring 的@RestController,它是 Solon 的组件,靠@McpServerEndpoint和@ToolMapping注册工具。
@Slf4j @McpServerEndpoint(sseEndpoint = "/sse") public class McpServerController { @ToolMapping(description = "提报线索(将客户线索提报到CRM系统)") public String submitClue( @ToolParam(description = "客户名称") String customer, @ToolParam(description = "客户来源") String source, @ToolParam(description = "客户电话") String phone, @ToolParam(description = "提报人工号") String userId) { log.info("提报线索:客户名称:{},来源:{},电话:{},工号:{}", customer, source, phone, userId); // 业务逻辑... return "线索提报成功!"; } @ToolMapping(description = "查询线索状态(查询用户提报过的线索的状态)") public String queryClueStatus(@ToolParam(description = "提报人工号") String userId) { log.info("查询线索状态:提报人工号:{}", userId); // 业务逻辑... return JSON.toJSONString(result); } }@McpServerEndpoint(sseEndpoint = "/sse")告诉 Solon 这是一个 MCP Server,SSE 端点是/sse;@ToolMapping注册一个可被 AI 发现的工具;@ToolParam描述参数,AI 据此生成正确的调用。注意这里的 Filter 也要用 Solon 的,不是 Spring 的。
@Slf4j @Component public class McpAuthFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { String path = ctx.path(); // 只拦截 MCP 端点 if (path.startsWith("/sse") || path.startsWith("/sse/message")) { String token = ctx.header("Authorization"); if (!isValidToken(token)) { log.warn("MCP 鉴权失败:path={}", path); ctx.status(401); ctx.output("Unauthorized"); return; } } chain.doFilter(ctx); } private boolean isValidToken(String token) { if (token == null || token.isEmpty()) { return false; } return "Bearer mcp-secret-token".equals(token); } }这里的Filter是org.noear.solon.core.handle.Filter,别导错包。鉴权逻辑很简单,就是比对Authorization头,生产环境建议换成 JWT 或从配置中心读取。
最后是application.yml里需要补的配置。Spring Boot 这边保持原样,Solon 的端口通过系统属性控制,所以application.yml里不需要为 Solon 单独写server.port,否则会互相打架。如果你想让 Solon 读自己的配置,可以在src/main/resources下放一个app.yml,但端口还是以系统属性为准。
# application.yml(Spring Boot 主配置,保持原样) server: port: 8080 # Solon 相关配置建议放 app.yml,避免和 Spring Boot 的 server.port 冲突 # app.yml solon: app: name: crm-mcp-server启动链路是这样的:CoreApplication.main()先判断Solon.app() != null,如果已经启动过就直接 return,防止重复启动;然后SpringApplication.run()初始化 Spring 环境,解析端口 8080;Bean 创建阶段触发McpServerConfig.@PostConstruct,设置系统属性server.port=8088,调用Solon.start(),扫描McpServerController注册 Tool,启动 SmartHttp Server 监听 8088,最后恢复server.port;Spring 的 Tomcat 继续在 8080 启动。两个 HTTP Server 各用各的端口,互不干扰。
4. 验证请求与把 MCP 端点改到 TaoToken 统一通道
代码写完,先做本地连通性验证。启动应用后,看日志里有没有【McpServer】启动完成...,然后用netstat确认 8088 在监听。
# 确认 8088 端口监听 netstat -ano | findstr 8088 # 测试 SSE 连接(带鉴权头) curl -H "Authorization: Bearer mcp-secret-token" http://localhost:8088/sse如果curl能挂住不返回、日志里出现 SSE 连接建立的信息,说明 MCP Server 起来了。如果curl直接报连接拒绝,netstat也没有 8088,那就是 HTTP Server 依赖没加或者 Solon 没启动成功,回到第 5 节排查。
本地通了之后,把 MCP 端点改到 TaoToken 统一通道。这里的「改到」不是改 Solon 的监听地址,而是让 AI 客户端侧通过 TaoToken 的兼容端点来访问模型和工具。以 Claude Desktop 为例,配置里把url指向你本地 Solon 的 SSE 端点,同时在模型侧配置 TaoToken 的 Base URL 和 Key。
{ "mcpServers": { "crm-server": { "url": "http://localhost:8088/sse", "headers": { "Authorization": "Bearer mcp-secret-token" } } } }如果你用的是 Cline 或 Codex 这类支持 MCP 的编码工具,配置结构类似,核心是三件套:Base URL 填 https://taotoken.net/api ,API Key 填你在控制台拿到的sk-开头的串,Model ID 填claude-sonnet-4-5或你套餐里支持的模型。Codex 的auth.json里对应字段是base_url、api_key、model,Cline 的 MCP 配置里则是baseUrl、apiKey、modelId,字段名不同但含义一致。
改完之后做一次完整的工具调用验证:在 AI 客户端里发一句「帮我提报一个线索,客户名称张三,来源官网,电话13800000000,工号1001」,观察两边的日志。Solon 这边应该打印提报线索:客户名称:张三,来源:官网,电话:13800000000,工号:1001,Spring Boot 这边如果工具有回调业务逻辑,也应该有对应日志。如果 AI 客户端提示找不到工具,检查@ToolMapping的description是否写清楚,AI 是靠描述来匹配意图的。
TaoToken 通道在这里的价值是统一管理:你不用在每个客户端里分别填不同的 Key 和 Base URL,换模型、换套餐只改一处。模型对话可以在 https://taotoken.net/model-chat 里先试一下工具调用是否正常,再接到编码工具里。如果是长期跑 Agent 任务,Coding Plan 的额度比按量计费更划算,具体看 https://taotoken.net/coding-plan 。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
这一节把实际会撞到的报错列出来,对照日志定位。
报错一:curl连不上 8088,netstat无监听。原因是solon-ai-mcp只含协议逻辑,不含 HTTP Server。解决方法是加solon-boot-smarthttp依赖。这个报错最隐蔽的地方在于应用启动不报错,日志里也有【McpServer】启动完成...,但端口就是没起来,因为 Solon 找不到可用的 HTTP Server 实现就静默跳过了。
报错二:NoClassDefFoundError: reactor/core/publisher/Sinks。原因是 MCP SSE 传输层用了 Reactor 的Sinks,而项目本身没有加reactor-core,或者 Spring Boot 管理的版本低于 3.4.0。解决方法是显式加reactor-core并指定<version>3.4.38</version>。如果你加了依赖但没指定版本,Maven 会从父 POM 继承旧版本,照样报错。
报错三:401 Unauthorized。这是鉴权失败,检查Authorization头是否带了Bearer前缀,以及 token 是否和McpAuthFilter里比对的一致。注意 Solon 的ctx.header("Authorization")拿到的是完整头值,比对时要包含Bearer。
报错四:local proxy failed。这个通常出现在 AI 客户端侧,说明客户端连不上你配置的 Base URL 或 SSE 端点。先确认 Solon 的 8088 在监听,再确认客户端配置里的url没有多写斜杠或路径。如果 Base URL 填的是 TaoToken 的地址,检查是不是误加了/v1后缀。
报错五:reading choices相关错误。这个一般出现在模型返回体解析阶段,说明请求发出去了但响应格式不对。常见原因是 Model ID 填错,或者 Base URL 指向了一个不兼容 OpenAI 格式的端点。TaoToken 的 Base URL 是 https://taotoken.net/api ,兼容 OpenAI 格式,Model ID 用文档里列出的值。
报错六:OAuth 相关报错。如果你用的是 Claude Code 这类走 OAuth 的工具,报错里出现OAuth字样,通常是认证方式选错了。Claude Code 接入第三方通道时应该用 API Key 方式,而不是 OAuth。配置里把认证类型改成api_key,填 TaoToken 的 Key。Claude Code 的接入文档在 https://taotoken.net/doc 里有专门章节,路径和字段名以文档为准。
排查的时候养成看两边日志的习惯:Solon 的日志在【McpServer】前缀下,Spring Boot 的日志在常规位置。工具调用失败时,先看 Solon 有没有收到请求,再看 Spring 侧的业务逻辑有没有执行,最后看 AI 客户端的返回。三段日志对上了,问题基本就定位了。
6. 把 MCP 通道固定下来的几个实操建议
跑通一次工具调用只是开始,真正要长期用,有几个地方值得固定下来。第一,Solon 的端口不要写死在代码里,虽然示例里用了 8088,但生产环境建议从环境变量读,System.getProperty("mcp.server.port", "8088")这样,避免和别的服务撞端口。第二,鉴权 token 不要硬编码,McpAuthFilter里的mcp-secret-token换成从配置中心或环境变量读取,Spring 的@Value在 Solon 组件里用不了,可以通过静态持有或者启动时传参的方式注入。
第三,@ToolMapping的description要写清楚业务语义,AI 是靠这个描述来匹配用户意图的。比如「提报线索」比「submitClue」对 AI 友好得多,参数描述也一样,「客户名称」比「customer」更容易让 AI 生成正确的调用。第四,工具方法的返回值尽量结构化,返回 JSON 字符串比返回纯文本更利于 AI 解析,但要注意长度,太长的返回会撑爆上下文。
第五,如果你有多个 MCP Server 要暴露,可以共用同一个 Solon 实例,用不同的@McpServerEndpoint路径区分,比如/crm/sse和/order/sse,这样只需要一个 8088 端口。第六,TaoToken 侧的 Key 建议按用途分,验证用的和长期跑 Agent 的分开,方便排查和限额。模型对话验证在 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc ,API Key 管理在 https://taotoken.net/api-keys ,Coding Plan 在 https://taotoken.net/coding-plan 。
最后说一个实际经验:Spring Boot 和 Solon 共存时,最容易出问题的是类加载和静态状态。Solon 的Solon.start()会初始化自己的全局上下文,如果应用有热部署或者多次启动的场景,记得在CoreApplication.main()里加Solon.app() != null的判断,防止重复启动导致端口占用。这个判断放在SpringApplication.run()之前,简单一行,能省掉很多「端口已被占用」的排查时间。