背景

在已有的 Spring Boot(Java 8)CRM 项目中集成 MCP(Model Context Protocol)Server,使 AI 客户端(如 Claude Desktop)能够通过 MCP 协议调用 CRM 的业务能力。

选择 Solon 的 MCP 实现(solon-ai-mcp),因为它是 Java 生态中较成熟的 MCP Server 方案。

核心挑战

Solon 和 Spring Boot 是两个完全独立的框架,注解体系、容器、HTTP 服务器互不兼容。不能简单地加个 @RestController 就让 Solon 的 @McpServerEndpoint 生效。

需要在 Spring Boot 启动过程中手动拉起一个 Solon 实例,两个 HTTP Server 各用一个端口。

架构概览

JVM 进程

HTTP

SSE

Spring Boot
端口: 8080
CRM 业务 API
Tomcat

Solon
端口: 8088
MCP Server
SmartHttp

CRM 前端 / Feign

AI 客户端
SSE: /sse
MSG: /sse/message

Maven 依赖

<!-- 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>

踩坑记录:依赖问题

问题 报错 原因 解决
缺 HTTP 服务器 curl 连不上 8088,netstat 无监听 solon-ai-mcp 只含协议逻辑,不含 HTTP Server solon-boot-smarthttp
缺 Reactor 类 NoClassDefFoundError: reactor/core/publisher/Sinks MCP SSE 传输层用了 Reactor 的 Sinks,项目本身没有 reactor-core版本必须 >= 3.4.0
Spring Boot 管理的 Reactor 版本太旧 同上(加了依赖但没指定版本) Spring Boot 2.x 管理的 reactor-core < 3.4.0,没有 Sinks 显式指定 <version>3.4.38</version>

代码实现

1. Solon 启动配置 — McpServerConfig.java

核心思路:利用 Spring 的 @PostConstruct 生命周期钩子,在 Spring Bean 初始化阶段启动 Solon。

@Slf4j
@Configuration
public class McpServerConfig {
    @PostConstruct
    public void init() {
        log.info("【McpServer】启动中...");
        // 临时设置 Solon 端口
        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-server.port=8088 在实测中 Solon 未正确识别
  • 系统属性在 Solon 配置加载优先级中最高,且 @PostConstruct 执行时 Spring Boot 已完成端口解析,不会影响 Spring Boot

2. MCP Controller — McpServerController.java

@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);
    }
}

关键点

  • 这个类不需要 Spring 的 @RestController,它是 Solon 的组件
  • @McpServerEndpoint 告诉 Solon 这是一个 MCP Server,SSE 端点为 /sse
  • @ToolMapping 注册一个 MCP Tool,AI 可以发现并调用它
  • @ToolParam 描述参数,AI 据此生成正确的调用

3. 鉴权过滤器 — McpAuthFilter.java

@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);
    }
}

注意这里用的是 Solon 的 Filterorg.noear.solon.core.handle.Filter),不是 Spring 的。

启动链路

CoreApplication.main()

Solon.app() != null ?

return(防重复启动)

SpringApplication.run()

Spring 环境初始化
端口解析(8080)

Bean 创建阶段

McpServerConfig.@PostConstruct

System.setProperty
server.port = 8088

Solon.start()

扫描 McpServerController
注册 Tool

启动 SmartHttp Server
监听 8088

System.clearProperty
恢复 server.port

Web Server 启动
Tomcat:8080

端点说明

端点 方法 说明
http://localhost:8088/sse GET SSE 连接端点,AI 客户端通过此建立长连接
http://localhost:8088/sse/message POST 消息端点,AI 客户端发送 Tool 调用请求

客户端配置

Claude Desktop

{
  "mcpServers": {
    "crm-server": {
      "url": "http://localhost:8088/sse",
      "headers": {
        "Authorization": "Bearer mcp-secret-token"
      }
    }
  }
}

curl 测试

# 测试 SSE 连接
curl -H "Authorization: Bearer mcp-secret-token" http://localhost:8088/sse

文件清单

com/sf/digit/crm/core/mcp/
├── McpServerConfig.java       # Solon 启动配置(@PostConstruct 拉起 Solon)
├── McpServerController.java   # MCP Tool 定义(@McpServerEndpoint + @ToolMapping)
└── McpAuthFilter.java         # Solon Filter 鉴权

总结

要点 说明
两个框架共存 Spring Boot (8080) + Solon (8088),互不干扰
端口隔离 通过 System.setProperty@PostConstruct 中临时设置 Solon 端口
三个必要依赖 solon-ai-mcp + solon-boot-smarthttp + reactor-core >= 3.4.0
鉴权 Solon 的 Filter,在 HTTP 层拦截 MCP 端点
不动核心代码 只需在 CoreApplication 加一个 Solon.app() != null 的防重判断
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐