MCP(Model Context Protocol)是 Anthropic 提出的大模型工具调用标准协议,越来越多的 AI 助手(Claude、Cursor、Cherry Studio 等)都在支持它。问题是,官方 Java SDK 和 Spring AI 都要求 Java 17+,而我们大量的企业项目还跑在 Java 8 + SpringBoot 2.x 上。
这篇文章介绍一种零侵入的方案:用 Solon-mcp——目前唯一支持 Java 8 的 MCP 服务端实现——以"双容器嵌入"的方式在你的 SpringBoot 2.x 项目里启动一个轻量级 Solon 容器,专门负责 MCP 协议层。原有业务代码一行不动。
项目地址:github.com/ov-vo/springboot2-solon-mcp-demo
技术栈:Java 1.8 · Spring Boot 2.3.2.RELEASE · Solon 3.3.1 · Maven 3.6+
一、为什么需要这个方案
现在大模型的"工具调用"越来越强,你可以让 AI 助手直接调用你的业务接口查询数据、执行操作。MCP 就是这个调用过程的标准协议。
但 Java 生态的 MCP 支持现状是这样的:
| 方案 | 最低 Java 版本 | 适用场景 |
|---|---|---|
| 官方 Java MCP SDK | Java 17 | 新项目 |
| Spring AI MCP | Java 17(Spring Boot 3.x) | 新项目 |
| Solon-mcp(本文方案) | Java 8 | 存量 SpringBoot 2.x 项目 |
如果你还在维护 Java 8 的项目,Solon-mcp 是目前唯一的选择。
二、双容器架构原理
整个方案的核心思想是让两个容器在同一个 JVM 进程中共存,各司其职:
┌─────────────────────────────────────────────────────────┐
│ JVM 进程 │
│ │
│ ┌─────────────────────┐ ┌───────────────────────┐ │
│ │ SpringBoot 容器 │ │ Solon 容器 │ │
│ │ │ │ │ │
│ │ @Component │ │ MCP 生命周期管理 │ │
│ │ @Service │ │ Tool/Resource/Prompt │ │
│ │ @Configuration │ │ 注册与路由 │ │
│ │ @Bean │ │ SSE 长连接通道 │ │
│ │ │ │ │ │
│ └──────────┬──────────┘ └──────────┬────────────┘ │
│ │ │ │
│ └───────── McpServerConfig ─────────────┘ │
│ (桥接层) │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ SolonServletFilter │ │
│ │ 拦截 /mcp/* 请求 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Tomcat(内嵌) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
关键设计点:
- SpringBoot 容器:负责所有业务 Bean 的创建和依赖注入(@Component、@Service、@Autowired 等),这部分完全不变。
- Solon 容器:负责 MCP 协议生命周期,包括 SSE 连接管理、Tool/Resource/Prompt 注册、JSON-RPC 消息分发。
- McpServerConfig(桥接层):用 Spring 的 @PostConstruct 启动 Solon,用 @Bean 将 Spring 管理的 MCP 端点手动注册到 Solon,用 SolonServletFilter 把 /mcp/* 的 HTTP 请求转发给 Solon 处理。
请求处理流程
当大模型(或 MCP 客户端)发起调用时,流程如下:
- MCP 客户端向
/mcp/demo1/sse发起 HTTP GET 请求建立 SSE 长连接 - Tomcat 接收请求,经过 CorsFilter(跨域)、McpFilter(鉴权)
- SolonServletFilter 将请求桥接至 Solon 容器处理,建立 SSE 通道
- 客户端通过 SSE 通道发送 MCP JSON-RPC 消息(tools/list、tools/call 等)
- Solon 路由到对应端点的 @ToolMapping 方法执行
- 方法由 Spring 管理的 Bean 实例执行,返回结果经 MCP 协议封装后通过 SSE 流推回
三、5 步迁移手册
以下步骤可以直接套用到你的真实 SpringBoot 2.x 项目中。
第 1 步:添加 Maven 依赖
在 pom.xml 中添加 Solon BOM 和核心依赖:
<!-- 在 <properties> 中添加 -->
<solon.version>3.3.1</solon.version>
<!-- 在 <dependencyManagement> 中统一管理 Solon 版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>${solon.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 在 <dependencies> 中添加以下核心依赖 -->
<dependencies>
<!-- 你已有的 SpringBoot 依赖保持不变 -->
<!-- AopUtils 解析代理类需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Solon MCP 核心 -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-ai-mcp</artifactId>
</dependency>
<!-- Solon 基础库(3.3.1 必须显式添加,3.4.x+ 不需要) -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-lib</artifactId>
</dependency>
<!-- Solon Servlet 桥接(SpringBoot 2.x 用 javax.servlet 版本) -->
<!-- SpringBoot 3.x 项目改用 solon-web-servlet-jakarta -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web-servlet</artifactId>
</dependency>
<!-- Reactor(Solon 3.3.1 的 SSE 依赖,3.4.x+ 已间接引入) -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.34</version>
</dependency>
</dependencies>
另外,必须在 maven-compiler-plugin 中开启 -parameters 编译参数:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg> <!-- MCP 需要方法参数名,不加则参数名变成 arg0/arg1 -->
</compilerArgs>
</configuration>
</plugin>
第 2 步:创建 MCP 端点标识接口
这个空接口的唯一作用是让 Spring 通过 List<IMcpServerEndpoint> 自动收集所有 MCP 端点 Bean:
package com.example.mcp.endpoint;
/**
* MCP 服务端点标识接口
* 实现此接口的 Spring Bean 会被 McpServerConfig 自动扫描并注册到 Solon 容器
*/
public interface IMcpServerEndpoint {
}
第 3 步:创建桥接配置类(核心)
这是整个方案的核心文件,负责双容器的启动和桥接:
@Configuration
public class McpServerConfig {
/** Spring 初始化完成后启动 Solon 容器 */
@PostConstruct
public void start() {
Solon.start(McpServerConfig.class,
new String[]{"--cfg=mcpserver.properties"},
app -> app.enableScanning(false)); // 禁止 Solon 扫描,Bean 由 Spring 管理
}
/** Spring 关闭时优雅停止 Solon */
@PreDestroy
public void stop() {
if (Solon.app() != null) {
Solon.stopBlock(false, Solon.cfg().stopDelay());
}
}
/**
* 将所有 Spring 管理的 MCP 端点手动注册到 Solon
* Spring 通过 List<IMcpServerEndpoint> 自动注入所有实现类
*/
@Bean
public McpServerConfig init(List<IMcpServerEndpoint> serverEndpoints) {
for (IMcpServerEndpoint serverEndpoint : serverEndpoints) {
// 必须用 AopUtils 获取真实类,因为 @Transactional 等注解会产生代理类
Class<?> clz = AopUtils.getTargetClass(serverEndpoint);
McpServerEndpoint anno = AnnotationUtils.findAnnotation(clz, McpServerEndpoint.class);
if (anno == null) continue;
McpServerEndpointProvider provider = McpServerEndpointProvider.builder()
.from(clz, anno).build();
provider.addTool(new MethodToolProvider(clz, serverEndpoint));
provider.addResource(new MethodResourceProvider(clz, serverEndpoint));
provider.addPrompt(new MethodPromptProvider(clz, serverEndpoint));
provider.postStart();
// === Solon 3.3.1 特有的兼容性处理 ===
// enableScanning(false) 模式下 SSE 路由可能未完整注册,需通过反射补救
try {
Object transport = provider.getClass()
.getDeclaredField("mcpTransportProvider").get(provider);
if (transport != null) {
transport.getClass()
.getMethod("toHttpHandler", SolonApp.class)
.invoke(transport, Solon.app());
}
} catch (Exception e) {
log.warn("[MCP] Manual route registration skipped: {}", e.getMessage());
}
}
return this;
}
/**
* 注册 MCP 鉴权 + Solon 桥接 Filter
* /mcp/** 不经过 Spring MVC,必须在 Filter 层做鉴权
*/
@Bean
public FilterRegistrationBean<Filter> mcpServerFilter() {
SolonServletFilter solonFilter = new SolonServletFilter();
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter((request, response, chain) -> {
HttpSession session = ((HttpServletRequest) request).getSession(false);
if (session == null || session.getAttribute("user") == null) {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(401);
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return;
}
solonFilter.doFilter(request, response, chain);
});
bean.addUrlPatterns("/mcp/*");
bean.setOrder(1);
return bean;
}
}
有几个关键设计决策值得解释:
- 为什么
enableScanning(false)? 如果让 Solon 也扫描组件,会与 Spring 重复创建 Bean,导致依赖注入混乱。禁用后,Solon 只加载内置插件(Web/SSE),MCP 端点由我们手动注册。 - 为什么要
AopUtils.getTargetClass()? 如果端点类上有@Transactional,Spring 会为它创建一个 CGLIB 代理类,代理类上找不到@McpServerEndpoint注解,必须拿到原始类。 - 那段反射代码是什么? 这是 Solon 3.3.1 版本在
enableScanning(false)模式下的一个 bug——SSE 路由注册不完整,需要通过反射手动补调toHttpHandler()。Solon 3.4.x+ 已修复,不再需要。 - 为什么鉴权放在 Filter 而不是 Interceptor?
/mcp/**路径经过 SolonServletFilter 转发,不会走 Spring MVC 的 DispatcherServlet,因此 HandlerInterceptor 无法拦截它。
第 4 步:编写 MCP 端点
MCP 支持三种能力类型:
| 能力 | 注解 | 大模型如何使用 |
|---|---|---|
| Tool(工具) | @ToolMapping | "查一下北京天气" → 调用 getWeather 方法 |
| Resource(资源) | @ResourceMapping | "应用版本是多少" → 读取 config://app-version |
| Prompt(提示词) | @PromptMapping | "生成一个提问" → 使用 askQuestion 模板 |
Tool 端点示例(天气查询):
@Service // 用 Spring 的 @Service,不是 Solon 的
@McpServerEndpoint(name = "demo1", sseEndpoint = "/mcp/demo1/sse")
public class WeatherMcpEndpoint implements IMcpServerEndpoint {
@ToolMapping(description = "查询指定城市的天气预报")
public String getWeather(
@Param(name = "location", description = "城市名称") String location) {
// 这里可以调用任何 Spring Bean,比如 @Autowired 进来的 Service
return location + ": 晴,14°C,东南风 3 级";
}
}
Resource + Prompt 端点示例:
@Service
@McpServerEndpoint(name = "demo2", sseEndpoint = "/mcp/demo2/sse")
public class AssistantMcpEndpoint implements IMcpServerEndpoint {
// 静态资源(无参数)
@ResourceMapping(uri = "config://app-version", description = "获取应用版本号")
public String getAppVersion() {
return "v1.2.3";
}
// 动态资源(URI 模板参数)
@ResourceMapping(uri = "db://users/{user_id}/email", description = "根据用户ID查询邮箱")
public String getEmail(@Param(name = "user_id", description = "用户ID") String user_id) {
return user_id + "@example.com";
}
// 提示词模板
@PromptMapping(description = "生成关于某个主题的提问")
public Collection<ChatMessage> askQuestion(
@Param(name = "topic", description = "主题") String topic) {
return Arrays.asList(ChatMessage.ofUser("请解释一下'" + topic + "'的概念?"));
}
}
有一个很好用的特性:你可以在已有的 @RestController 上直接叠加 @McpServerEndpoint,同一个方法同时支持 HTTP 和 MCP 调用,完全不用写重复代码:
@McpServerEndpoint(sseEndpoint = "/mcp/api/sse")
@RestController
@RequestMapping("/api")
public class OrderController {
@ToolMapping(description = "查询订单详情")
@GetMapping("/order/{id}")
public Order getOrder(
@Param(name = "orderId", description = "订单ID")
@PathVariable String id) {
return orderService.findById(id);
}
}
第 5 步:添加 Solon 配置文件
在 src/main/resources/ 下创建 mcpserver.properties(可以为空,但必须存在):
# Solon MCP 配置文件
# 当前为空,使用默认配置
# 如需自定义端口等,可在此添加
⚠️ 注意:文件名后缀必须是 .properties,不能是 .yml。Solon 默认的 PropsLoader 不支持 YAML 格式(除非额外添加 solon-config-yaml 依赖)。这个坑我踩过,从 yml 改名后立刻解决。
四、启动验证
启动后,控制台会出现以下日志,确认两个 MCP 端点已成功注册:
[Solon] App: Start loading
[Solon] App: Plugin starting
[Solon] App: End loading elapsed=20ms
Mcp-Server started, name=demo1, channel=sse, sseEndpoint=/mcp/demo1/sse, toolRegistered=1
Mcp-Server started, name=demo2, channel=sse, sseEndpoint=/mcp/demo2/sse,
toolRegistered=0, resourceRegistered=2, promptRegistered=1
Started McpApplication in 0.883 seconds
用 curl 快速验证 SSE 连接是否正常:
# 先登录获取 Cookie
curl -c cookies.txt -X POST http://localhost:8080/login \
-d "username=admin&password=admin123"
# 验证 SSE 端点
curl -b cookies.txt -N http://localhost:8080/mcp/demo1/sse
正常返回:
event:endpoint
data:/mcp/demo1/sse/message?sessionId=xxxx-xxxx-xxxx
五、测试工具
项目内置了一个功能完整的 Python MCP 测试工具(mcp-tester/),零外部依赖,只用 Python 标准库,在完全离线的环境也能跑。
几个常用命令:
cd mcp-tester
# 全量测试(自动执行 SSE 连接 → Initialize → tools/list → tools/call → ...)
python mcp_tester.py http://localhost:8080/mcp/demo1/sse \
--login-url http://localhost:8080/login \
--username admin --password admin123
# 健康检查(只验证连接和握手,快速)
python mcp_tester.py --health http://localhost:8080/mcp/demo1/sse
# 单工具调用
python mcp_tester.py --call-tool getWeather \
--args '{"location": "上海"}' \
http://localhost:8080/mcp/demo1/sse
# 同时测试多个端点
python mcp_tester.py \
http://localhost:8080/mcp/demo1/sse \
http://localhost:8080/mcp/demo2/sse
测试报告输出:
============================================================
Weather MCP (http://localhost:8080/mcp/demo1/sse)
============================================================
[PASS] SSE 连接 (125.3ms)
[PASS] Initialize 握手 (89.7ms)
[PASS] notifications/initialized (12.1ms)
[PASS] tools/list (45.2ms)
[PASS] tools/call: getWeather (234.8ms)
[WARN] resources/list (56.3ms) ← WARN 表示该端点未实现此能力,非错误
[WARN] prompts/list (43.1ms)
------------------------------------------------------------
[PASS] Total: 7 | Passed: 5 | Failed: 0 | Warned: 2 | Time: 606.5ms
============================================================
六、接入 MCP 客户端
服务启动后,可以直接接入各主流 MCP 客户端:
Cherry Studio:设置 → MCP 服务器 → 添加 → 类型选 SSE → 填入 http://localhost:8080/mcp/demo1/sse
Claude Desktop(claude_desktop_config.json):
{
"mcpServers": {
"weather": {
"url": "http://localhost:8080/mcp/demo1/sse"
}
}
}
Cursor:Settings → MCP → Add Server → SSE → 填入端点 URL
七、踩坑记录
这里汇总了开发过程中遇到的所有坑,给后来者省时间:
| 坑 | 现象 | 解决方法 |
|---|---|---|
| 配置文件后缀 | 启动报 profile is not supported: mcpserver.yml |
改为 .properties 后缀 |
| 缺少 -parameters 编译参数 | 大模型调用工具时参数传错,参数名变成 arg0/arg1 | maven-compiler-plugin 加 <arg>-parameters</arg> |
| AOP 代理导致注解丢失 | @McpServerEndpoint 端点注册不上 | 用 AopUtils.getTargetClass() + AnnotationUtils.findAnnotation() |
| Solon 3.3.1 路由注册不完整 | curl 测试 SSE 端点返回 404 | 反射调用 toHttpHandler() 补注册路由 |
| 缺少 solon-lib 依赖 | 启动时 ClassNotFound 异常 | 显式添加 solon-lib(3.3.1 必须,3.4.x+ 不需要) |
| 缺少 reactor-core 依赖 | SSE 连接时 NoClassDefFoundError: reactor/core/publisher/Sinks |
显式添加 reactor-core 3.4.34 |
| 鉴权放在 Interceptor | /mcp/** 路径鉴权失效,未登录也能访问 | 改为在 Servlet Filter 层做鉴权 |
| Solon 3.3.1 resources/read Bug | 读取 Resource 时 SSE 连接断开 | 升级到 Solon 3.4.x+(已修复) |
Solon 3.3.1 vs 3.4.x+ 对比
如果你新建项目,建议直接用 3.4.x+,省掉几个 workaround:
| 差异点 | 3.3.1 | 3.4.x+ |
|---|---|---|
| 需要显式添加 solon-lib | 是 | 不需要 |
| 需要显式添加 reactor-core | 是 | 不需要 |
| 需要反射调用 toHttpHandler() | 是 | 不需要 |
| resources/read Jackson Bug | 存在 | 已修复 |
八、项目结构速览
springboot2-solon-mcp-demo/
├── pom.xml
├── src/main/java/com/example/mcp/
│ ├── McpApplication.java # SpringBoot 启动类
│ ├── config/
│ │ ├── McpServerConfig.java # ⭐ 双容器桥接核心(本文重点)
│ │ └── WebMvcConfig.java # Spring MVC 拦截器配置
│ ├── controller/
│ │ └── LoginController.java # 登录/退出接口
│ ├── interceptor/
│ │ └── AuthInterceptor.java # Spring MVC 路径鉴权
│ └── endpoint/
│ ├── IMcpServerEndpoint.java # MCP 端点标识接口
│ ├── WeatherMcpEndpoint.java # Tool 示例
│ └── AssistantMcpEndpoint.java # Resource + Prompt 示例
├── src/main/resources/
│ ├── application.yml # SpringBoot 配置
│ └── mcpserver.properties # Solon 配置(可空,必须存在)
└── mcp-tester/
├── mcp_tester.py # 完整测试工具
├── sse_client.py # SSE 协议客户端
└── reporter.py # 测试报告生成器
九、迁移检查清单
应用到你的真实项目时,逐项核对:
- ☐
pom.xml添加了 solon-parent BOM(dependencyManagement) - ☐ 添加了 solon-ai-mcp、solon-lib、solon-web-servlet、spring-boot-starter-aop、reactor-core
- ☐ maven-compiler-plugin 配置了
-parameters - ☐ SpringBoot 2.x 用
solon-web-servlet(javax.servlet),不是 jakarta 版 - ☐ 创建了
IMcpServerEndpoint空接口 - ☐ 创建了
McpServerConfig(start/stop/init/mcpServerFilter 四个方法) - ☐
Solon.start()设置了enableScanning(false) - ☐ 配置文件命名为
mcpserver.properties(不是 .yml) - ☐ MCP 端点用 Spring 的
@Service(不是 Solon 的注解) - ☐ MCP 端点实现了
IMcpServerEndpoint接口 - ☐
@Param注解显式指定了name属性 - ☐ 如需鉴权,在 Filter 层实现(不是 Interceptor)
- ☐ 启动后 Solon 日志确认端点注册成功
结语
MCP 把大模型和业务系统之间的接口标准化了。对于还跑在 Java 8 上的企业存量系统,Solon-mcp 是目前唯一的入场券。
这套方案的核心价值在于零侵入——你不需要把项目迁移到 Java 17,不需要重构任何业务代码,加几个配置文件和一个配置类,就能让大模型直接调用你的接口。在银行、保险、政务等 Java 8 项目大量存在的行业,这个方案的价值会更明显。
完整代码见 GitHub 仓库,有问题欢迎提 Issue。