📖 前言
随着大模型(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 通常包含两个端点:
- SSE 端点:用于建立连接,服务器通过此通道推送通知。
- 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 协议逻辑
这里是核心逻辑。我们需要处理三种主要方法:
initialize:告诉客户端我是谁,我有什能力。tools/list:列出我可以提供的工具(Function Calling 定义)。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"}
}
}'
预期响应: 返回包含 serverInfo 和 capabilities 的 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,大模型通过它"执行"了我们的代码。
接下来的进阶玩法:
- 连接数据库 :在
McpService中注入 MyBatis/JPA Mapper,让 AI 可以查询 SQL 数据。 - 集成 Spring AI:结合 Spring AI 框架,让 Java 方法自动映射为 Tool 定义,减少手动编写 JSON Schema 的工作量。
- 安全认证:在 SSE 和 POST 接口增加 Token 校验,防止未授权调用。
👨💻 作者提示 :MCP 协议仍在快速演进中,建议关注 Anthropic 官方文档获取最新规范。如果你觉得这篇文章有帮助,欢迎 点赞、收藏、关注!