实战:用 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 官方文档获取最新规范。如果你觉得这篇文章有帮助,欢迎 点赞、收藏、关注


相关推荐
步步为营DotNet25 分钟前
深入解读CancellationToken:.NET异步操作的精准控制
java·前端·.net
无奈何杨25 分钟前
业务接入风控决策,挑战验证与结果同步
后端
曹牧25 分钟前
Java中使用List传入Oracle的IN查询
java·oracle·list
青衫码上行27 分钟前
【JavaWeb学习 | 第17篇】JSP内置对象
java·开发语言·前端·学习·jsp
2201_7578308730 分钟前
线程池超详细解释
java
JaguarJack30 分钟前
FrankenPHP 是否是 PHP 的未来?
后端·php
编程修仙32 分钟前
第二篇 搭建第一个spring程序
java·数据库·spring
VX:Fegn089532 分钟前
计算机毕业设计|基于springboot + vue手办商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
麦麦鸡腿堡33 分钟前
Java_网络上传文件与netstat指令
java·服务器·网络