实战:用 Spring Boot 搭建 Model Context Protocol (MCP) 服务

📖 前言

随着大模型(LLM)应用的爆发,如何让模型安全、标准地连接本地数据和工具成为了核心痛点。Model Context Protocol (MCP) 是由 Anthropic 提出的一项开放标准,旨在为 AI 助手与系统(数据库、工具、API)之间提供通用的连接接口。

想象一下:MCP 就是 AI 时代的 USB 接口。

本文将手把手教你使用 Spring Boot 构建一个标准的 MCP 服务(Server),通过 SSE(Server-Sent Events)和 JSON-RPC 协议,暴露本地方法供大模型调用。


🏗️ 架构设计

在开始写代码之前,我们先理清 MCP 的 HTTP 通信流程。MCP over HTTP 通常包含两个端点:

  1. SSE 端点:用于建立连接,服务器通过此通道推送通知。
  2. POST 端点:客户端(大模型或 MCP 宿主)通过此接口发送 JSON-RPC 请求。

通信时序图 (Mermaid)


🛠️ 代码实战

1. 项目初始化

创建一个 Spring Boot 项目 (JDK 17+),在 pom.xml 中引入必要的依赖。主要依赖是 Web 模块和用于处理 JSON 的 Jackson。

xml 复制代码
<dependencies>
    <!-- Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Lombok (可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 定义 JSON-RPC 模型

MCP 基于 JSON-RPC 2.0。我们需要定义请求和响应的基础类。

JsonRpcRequest.java

java 复制代码
package com.example.mcp.model;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;

@Data
public class JsonRpcRequest {
    private String jsonrpc = "2.0";
    private String method;
    private JsonNode params;
    private Object id;
}

JsonRpcResponse.java

java 复制代码
package com.example.mcp.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonRpcResponse {
    private String jsonrpc = "2.0";
    private Object result;
    private Object error;
    private Object id;

    // 成功的静态工厂方法
    public static JsonRpcResponse success(Object id, Object result) {
        return new JsonRpcResponse("2.0", result, null, id);
    }
}

3. 核心服务层:处理 MCP 协议逻辑

这里是核心逻辑。我们需要处理三种主要方法:

  1. initialize:告诉客户端我是谁,我有什能力。
  2. tools/list:列出我可以提供的工具(Function Calling 定义)。
  3. tools/call:真正执行业务逻辑。

McpService.java

java 复制代码
package com.example.mcp.service;

import com.example.mcp.model.JsonRpcRequest;
import com.example.mcp.model.JsonRpcResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class McpService {

    private final ObjectMapper objectMapper = new ObjectMapper();

    public JsonRpcResponse handleRequest(JsonRpcRequest request) {
        String method = request.getMethod();

        try {
            switch (method) {
                case "initialize":
                    return handleInitialize(request);
                case "tools/list":
                    return handleListTools(request);
                case "tools/call":
                    return handleCallTool(request);
                case "notifications/initialized":
                    // 客户端确认初始化完成,通常无需回复内容,但需保持连接
                    return null; 
                default:
                    throw new RuntimeException("Method not found: " + method);
            }
        } catch (Exception e) {
            // 简单错误处理
            return new JsonRpcResponse("2.0", null, Map.of("code", -32603, "message", e.getMessage()), request.getId());
        }
    }

    // 1. 握手协议
    private JsonRpcResponse handleInitialize(JsonRpcRequest request) {
        Map<String, Object> result = new HashMap<>();
        result.put("protocolVersion", "2025-11-05");
        result.put("capabilities", Map.of("tools", Map.of())); // 声明支持工具
        result.put("serverInfo", Map.of("name", "SpringBoot-MCP-Demo", "version", "1.0.0"));
        return JsonRpcResponse.success(request.getId(), result);
    }

    // 2. 定义工具列表
    private JsonRpcResponse handleListTools(JsonRpcRequest request) {
        // 定义一个简单的加法工具
        Map<String, Object> addTool = Map.of(
            "name", "calculate_sum",
            "description", "计算两个数字的和",
            "inputSchema", Map.of(
                "type", "object",
                "properties", Map.of(
                    "a", Map.of("type", "number", "description", "第一个数字"),
                    "b", Map.of("type", "number", "description", "第二个数字")
                ),
                "required", List.of("a", "b")
            )
        );
        
        // 定义一个系统信息工具
        Map<String, Object> sysInfoTool = Map.of(
            "name", "get_system_info",
            "description", "获取当前服务器运行环境信息",
            "inputSchema", Map.of("type", "object", "properties", Map.of())
        );

        return JsonRpcResponse.success(request.getId(), Map.of("tools", List.of(addTool, sysInfoTool)));
    }

    // 3. 执行工具逻辑
    private JsonRpcResponse handleCallTool(JsonRpcRequest request) {
        String name = request.getParams().get("name").asText();
        Map<String, Object> arguments = objectMapper.convertValue(request.getParams().get("arguments"), Map.class);

        String resultText = "";

        if ("calculate_sum".equals(name)) {
            double a = Double.parseDouble(arguments.get("a").toString());
            double b = Double.parseDouble(arguments.get("b").toString());
            resultText = String.valueOf(a + b);
        } else if ("get_system_info".equals(name)) {
            resultText = System.getProperty("os.name") + " - Java " + System.getProperty("java.version");
        } else {
            throw new RuntimeException("Unknown tool: " + name);
        }

        // MCP 要求的返回格式 content: [{type: "text", text: "..."}]
        Map<String, Object> content = Map.of(
            "content", List.of(Map.of("type", "text", "text", resultText))
        );
        
        return JsonRpcResponse.success(request.getId(), content);
    }
}

4. Controller 层:暴露 SSE 和 HTTP 接口

这是与外部世界交互的入口。

McpController.java

java 复制代码
package com.example.mcp.controller;

import com.example.mcp.model.JsonRpcRequest;
import com.example.mcp.model.JsonRpcResponse;
import com.example.mcp.service.McpService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping("/mcp")
public class McpController {

    private final McpService mcpService;
    // 用于保存SSE连接(生产环境可能需要更复杂的Session管理)
    private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();

    public McpController(McpService mcpService) {
        this.mcpService = mcpService;
    }

    /**
     * 1. SSE 连接端点
     * 客户端连接到这里监听服务端事件
     */
    @GetMapping("/sse")
    public SseEmitter handleSse(HttpServletResponse response) {
        // 设置 SSE 超时时间,0表示无限
        SseEmitter emitter = new SseEmitter(0L);
        String sessionId = UUID.randomUUID().toString();
        
        emitters.put(sessionId, emitter);

        emitter.onCompletion(() -> emitters.remove(sessionId));
        emitter.onTimeout(() -> emitters.remove(sessionId));

        try {
            // MCP 标准:连接建立后,服务端发送 endpoint 事件,告知客户端去哪里发 POST 消息
            // 注意:这里硬编码了本地地址,实际部署需改为配置的域名/IP
            String endpointUrl = "/mcp/messages?sessionId=" + sessionId;
            emitter.send(SseEmitter.event().name("endpoint").data(endpointUrl));
            System.out.println("Client connected, session: " + sessionId);
        } catch (IOException e) {
            emitters.remove(sessionId);
        }

        return emitter;
    }

    /**
     * 2. 消息接收端点 (POST)
     * 客户端发送 JSON-RPC 请求到这里
     */
    @PostMapping("/messages")
    public JsonRpcResponse handleMessage(
            @RequestParam(required = false) String sessionId,
            @RequestBody JsonRpcRequest request) {
        
        System.out.println("Received: " + request.getMethod());
        
        // 处理核心业务逻辑
        JsonRpcResponse response = mcpService.handleRequest(request);
        
        return response;
    }
}

🧪 测试与验证

要验证我们的服务是否符合 MCP 标准,我们可以使用 curl 或者 Anthropic 官方提供的 mcp-inspector(如果你有 Node.js 环境)。

这里我们使用简单的 HTTP 请求流程来模拟验证。

1. 启动服务

运行 Spring Boot 应用,默认端口 8080。

2. 模拟连接 (SSE)

在终端使用 curl 监听 SSE:

bash 复制代码
curl -N http://localhost:8080/mcp/sse

预期输出:

复制代码
event:endpoint
data:/mcp/messages?sessionId=xxxx-xxxx-xxxx...

(保持这个窗口打开,或者记下 sessionId)

3. 发送 Initialize 请求 (POST)

打开一个新的终端窗口,发送初始化请求:

bash 复制代码
curl -X POST http://localhost:8080/mcp/messages \
-H "Content-Type: application/json" \
-d '{
  "jsonrpc": "2.0",
  "method": "initialize",
  "id": 1,
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {"name": "curl-client", "version": "1.0"}
  }
}'

预期响应: 返回包含 serverInfocapabilities 的 JSON。

4. 调用工具 (Tools Call)

测试我们编写的加法工具:

bash 复制代码
curl -X POST http://localhost:8080/mcp/messages \
-H "Content-Type: application/json" \
-d '{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "id": 2,
  "params": {
    "name": "calculate_sum",
    "arguments": {"a": 10, "b": 25.5}
  }
}'

预期响应:

json 复制代码
{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "35.5"
      }
    ]
  },
  "id": 2
}

💡 总结与展望

通过上述步骤,我们成功使用 Spring Boot 构建了一个最小化的 MCP Server

  • 我们实现了 tools/list,大模型通过它"看见"了我们的功能。
  • 我们实现了 tools/call,大模型通过它"执行"了我们的代码。

接下来的进阶玩法:

  1. 连接数据库 :在 McpService 中注入 MyBatis/JPA Mapper,让 AI 可以查询 SQL 数据。
  2. 集成 Spring AI:结合 Spring AI 框架,让 Java 方法自动映射为 Tool 定义,减少手动编写 JSON Schema 的工作量。
  3. 安全认证:在 SSE 和 POST 接口增加 Token 校验,防止未授权调用。

👨‍💻 作者提示 :MCP 协议仍在快速演进中,建议关注 Anthropic 官方文档获取最新规范。如果你觉得这篇文章有帮助,欢迎 点赞、收藏、关注


相关推荐
葫芦和十三5 分钟前
图解 MongoDB 22|读写关注:持久性与一致性的档位选择
后端·mongodb·agent
葫芦和十三7 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp7 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑8 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯8 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan11 小时前
多Agent之间的区别
后端
青石路12 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充13 小时前
1.面向对象设计思想
后端
IT_陈寒13 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro14 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端