MCP Server 搭建与使用指南
一、概述
基于 Java 8 + Spring Boot 2.x 手写实现的 MCP (Model Context Protocol) Server,采用 SSE (Server-Sent Events) 传输层,遵循 JSON-RPC 2.0 消息格式。协议详情自行了解。
技术选型:
- 传输层:Spring MVC
SseEmitter(无需额外依赖) - 序列化:fastjson
- 协议版本:MCP
2024-11-05
二、架构
客户端 (Claude Desktop / Cursor / ...)
│
│ GET /mcp/sse ← 建立 SSE 长连接,获取 sessionId
│ POST /mcp/message ← 发送 JSON-RPC 请求
│
▼
McpServerController ← SSE 端点 & 消息接收
│
▼
McpMessageHandler ← JSON-RPC 路由分发
│
▼
McpTool (接口) ← 工具抽象
├── EchoTool ← 示例:回显工具
└── ... ← 扩展:自定义工具
三、完整代码
3.1 McpTool.java --- 工具接口
java
import java.util.Map;
public interface McpTool {
String getName();
String getDescription();
Map<String, Object> getInputSchema();
Object execute(Map<String, Object> args);
}
3.2 EchoTool.java --- 示例工具
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class EchoTool implements McpTool {
private static final Logger logger = LoggerFactory.getLogger(EchoTool.class);
@Override
public String getName() {
return "echo";
}
@Override
public String getDescription() {
return "Echoes back the input message";
}
@Override
public Map<String, Object> getInputSchema() {
Map<String, Object> schema = new HashMap<String, Object>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<String, Object>();
Map<String, Object> messageProp = new HashMap<String, Object>();
messageProp.put("type", "string");
messageProp.put("description", "The message to echo back");
properties.put("message", messageProp);
schema.put("properties", properties);
String[] required = {"message"};
schema.put("required", required);
return schema;
}
@Override
public Object execute(Map<String, Object> args) {
String message = args != null ? (String) args.get("message") : "";
logger.info("[MCP-EchoTool] execute, input message={}", message);
return message;
}
}
3.3 McpMessageHandler.java --- 消息路由
java
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class McpMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(McpMessageHandler.class);
@Autowired
private List<McpTool> tools;
public JSONObject handleMessage(JSONObject request) {
String method = request.getString("method");
Object id = request.get("id");
logger.info("[MCP-Handler] Received request, method={}, id={}, params={}", method, id, request.get("params"));
if (method == null) {
logger.warn("[MCP-Handler] Method is null, ignoring request");
return null;
}
JSONObject response;
switch (method) {
case "initialize":
logger.info("[MCP-Handler] Processing initialize, clientInfo={}", request.getJSONObject("params"));
response = buildInitializeResponse(id);
break;
case "notifications/initialized":
logger.info("[MCP-Handler] Client initialized notification received");
return null;
case "tools/list":
logger.info("[MCP-Handler] Processing tools/list, registered tools count={}", tools.size());
response = buildToolsListResponse(id);
break;
case "tools/call":
response = buildToolCallResponse(id, request.getJSONObject("params"));
break;
default:
logger.warn("[MCP-Handler] Unknown method: {}", method);
response = buildErrorResponse(id, -32601, "Method not found: " + method);
break;
}
logger.info("[MCP-Handler] Response built, method={}, id={}, hasError={}", method, id, response.containsKey("error"));
return response;
}
private JSONObject buildInitializeResponse(Object id) {
JSONObject result = new JSONObject();
result.put("protocolVersion", "2024-11-05");
JSONObject capabilities = new JSONObject();
JSONObject toolsCap = new JSONObject();
toolsCap.put("listChanged", false);
capabilities.put("tools", toolsCap);
result.put("capabilities", capabilities);
JSONObject serverInfo = new JSONObject();
serverInfo.put("name", "sdp-tec-mcp-server");
serverInfo.put("version", "1.0.0");
result.put("serverInfo", serverInfo);
return buildResponse(id, result);
}
private JSONObject buildToolsListResponse(Object id) {
JSONArray toolArray = new JSONArray();
for (McpTool tool : tools) {
JSONObject toolObj = new JSONObject();
toolObj.put("name", tool.getName());
toolObj.put("description", tool.getDescription());
toolObj.put("inputSchema", tool.getInputSchema());
toolArray.add(toolObj);
}
JSONObject result = new JSONObject();
result.put("tools", toolArray);
return buildResponse(id, result);
}
private JSONObject buildToolCallResponse(Object id, JSONObject params) {
if (params == null) {
logger.warn("[MCP-Handler] tools/call with null params, id={}", id);
return buildErrorResponse(id, -32602, "Invalid params");
}
String toolName = params.getString("name");
JSONObject arguments = params.getJSONObject("arguments");
logger.info("[MCP-Handler] Processing tools/call, id={}, toolName={}, arguments={}", id, toolName, arguments);
McpTool targetTool = null;
for (McpTool tool : tools) {
if (tool.getName().equals(toolName)) {
targetTool = tool;
break;
}
}
if (targetTool == null) {
logger.warn("[MCP-Handler] Tool not found: {}", toolName);
return buildErrorResponse(id, -32602, "Tool not found: " + toolName);
}
Map<String, Object> args = new HashMap<String, Object>();
if (arguments != null) {
args.putAll(arguments);
}
logger.info("[MCP-Handler] Executing tool={}, args={}", toolName, args);
long startTime = System.currentTimeMillis();
Object execResult;
try {
execResult = targetTool.execute(args);
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
logger.error("[MCP-Handler] Tool execution failed, tool={}, elapsed={}ms", toolName, elapsed, e);
return buildErrorResponse(id, -32603, "Tool execution error: " + e.getMessage());
}
long elapsed = System.currentTimeMillis() - startTime;
logger.info("[MCP-Handler] Tool executed successfully, tool={}, elapsed={}ms, result={}", toolName, elapsed, execResult);
JSONObject result = new JSONObject();
JSONArray content = new JSONArray();
JSONObject textContent = new JSONObject();
textContent.put("type", "text");
textContent.put("text", String.valueOf(execResult));
content.add(textContent);
result.put("content", content);
result.put("isError", false);
return buildResponse(id, result);
}
private JSONObject buildResponse(Object id, JSONObject result) {
JSONObject response = new JSONObject();
response.put("jsonrpc", "2.0");
response.put("id", id);
response.put("result", result);
return response;
}
private JSONObject buildErrorResponse(Object id, int code, String message) {
JSONObject response = new JSONObject();
response.put("jsonrpc", "2.0");
response.put("id", id);
JSONObject error = new JSONObject();
error.put("code", code);
error.put("message", message);
response.put("error", error);
return response;
}
}
3.4 McpServerController.java --- SSE 端点控制器
java
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("mcp")
public class McpServerController {
private static final Logger logger = LoggerFactory.getLogger(McpServerController.class);
private final Map<String, SseEmitter> sessions = new ConcurrentHashMap<String, SseEmitter>();
@Autowired
private McpMessageHandler messageHandler;
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
String sessionId = UUID.randomUUID().toString();
logger.info("[MCP-SSE] New connection request, assigned sessionId={}", sessionId);
SseEmitter emitter = new SseEmitter(0L);
sessions.put(sessionId, emitter);
logger.info("[MCP-SSE] Active sessions count: {}", sessions.size());
emitter.onCompletion(new Runnable() {
@Override
public void run() {
logger.info("[MCP-SSE] Session completed, sessionId={}", sessionId);
sessions.remove(sessionId);
}
});
emitter.onTimeout(new Runnable() {
@Override
public void run() {
logger.warn("[MCP-SSE] Session timed out, sessionId={}", sessionId);
sessions.remove(sessionId);
}
});
try {
String endpoint = "/mcp/message?sessionId=" + sessionId;
emitter.send(SseEmitter.event().name("endpoint").data(endpoint));
logger.info("[MCP-SSE] Sent endpoint event to client, sessionId={}, endpoint={}", sessionId, endpoint);
} catch (IOException e) {
logger.error("[MCP-SSE] Failed to send endpoint event, sessionId={}", sessionId, e);
sessions.remove(sessionId);
}
return emitter;
}
@PostMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
public String handleMessage(@RequestParam String sessionId, @RequestBody String body) {
logger.info("[MCP-MSG] Received message, sessionId={}, body={}", sessionId, body);
SseEmitter emitter = sessions.get(sessionId);
if (emitter == null) {
logger.warn("[MCP-MSG] Session not found, sessionId={}", sessionId);
return "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Session not found\"}}";
}
JSONObject request = JSONObject.parseObject(body);
JSONObject response = messageHandler.handleMessage(request);
if (response != null) {
try {
String responseStr = response.toJSONString();
logger.info("[MCP-MSG] Sending SSE response, sessionId={}, response={}", sessionId, responseStr);
emitter.send(SseEmitter.event().name("message").data(responseStr));
} catch (IOException e) {
logger.error("[MCP-MSG] Failed to send SSE response, sessionId={}", sessionId, e);
sessions.remove(sessionId);
}
} else {
logger.info("[MCP-MSG] No response to send (notification), sessionId={}", sessionId);
}
return "accepted";
}
}
四、MCP 协议交互流程
客户端 服务端
│ │
│──── GET /mcp/sse ──────────────────────▶│ 1. 建立 SSE 连接
│◀─── event:endpoint ────────────────────│ 2. 返回消息发送地址
│ data:/mcp/message?sessionId=xxx │
│ │
│──── POST initialize ──────────────────▶│ 3. 握手初始化
│◀─── event:message (server info) ───────│
│ │
│──── POST notifications/initialized ───▶│ 4. 客户端就绪通知
│ │
│──── POST tools/list ──────────────────▶│ 5. 获取工具列表
│◀─── event:message (tools array) ───────│
│ │
│──── POST tools/call ──────────────────▶│ 6. 调用工具
│◀─── event:message (tool result) ───────│
│ │
五、API 详细说明
5.1 建立 SSE 连接
GET http://domain.a.com/mcp/sse
无入参。 响应为 SSE 事件流,首先收到 endpoint 事件:
event: endpoint
data: /mcp/message?sessionId=50894338-1019-44ac-8b3d-5484fa07e526
5.2 initialize --- 握手初始化
POST http://domain.a.com/mcp/message?sessionId={sessionId}
Content-Type: application/json
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"capabilities": {}
}
}
SSE 响应示例:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "sdp-tec-mcp-server", "version": "1.0.0" }
}
}
5.3 notifications/initialized --- 客户端就绪通知
POST http://domain.a.com/mcp/message?sessionId={sessionId}
Content-Type: application/json
json
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
无 id 字段,服务端不返回响应。
5.4 tools/list --- 获取工具列表
POST http://domain.a.com/mcp/message?sessionId={sessionId}
Content-Type: application/json
json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
SSE 响应示例:
json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "echo",
"description": "Echoes back the input message",
"inputSchema": {
"type": "object",
"properties": {
"message": { "type": "string", "description": "The message to echo back" }
},
"required": ["message"]
}
}
]
}
}
5.5 tools/call --- 调用工具
POST http://domain.a.com/mcp/message?sessionId={sessionId}
Content-Type: application/json
json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {
"message": "hello world"
}
}
}
SSE 响应示例:
json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "hello world" }
],
"isError": false
}
}
六、curl 测试
bash
# 1. 建立 SSE 连接(保持前台,观察事件输出)
curl -N http://domain.a.com/mcp/sse
# 2. 另开终端,设置 sessionId
SESSION_ID="替换为第1步返回的实际值"
# 3. initialize
curl -X POST "http://domain.a.com/mcp/message?sessionId=$SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test-client","version":"1.0.0"},"capabilities":{}}}'
# 4. notifications/initialized
curl -X POST "http://domain.a.com/mcp/message?sessionId=$SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 5. tools/list
curl -X POST "http://domain.a.com/mcp/message?sessionId=$SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# 6. tools/call (echo)
curl -X POST "http://domain.a.com/mcp/message?sessionId=$SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello world"}}}'
七、客户端配置
Claude Desktop
编辑 claude_desktop_config.json:
json
{
"mcpServers": {
"sdp-tec": {
"url": "http://domain.a.com/mcp/sse"
}
}
}
Cursor
在 MCP 设置中添加:
json
{
"mcpServers": {
"sdp-tec": {
"type": "sse",
"url": "http://domain.a.com/mcp/sse"
}
}
}
八、日志说明
所有日志带统一前缀,便于生产环境检索:
| 前缀 | 来源 | 内容 |
|---|---|---|
[MCP-SSE] |
McpServerController | SSE 连接建立、断开、超时 |
[MCP-MSG] |
McpServerController | 消息接收、响应发送 |
[MCP-Handler] |
McpMessageHandler | 请求路由、工具执行、耗时统计 |
[MCP-EchoTool] |
EchoTool | 工具执行入参 |
关键日志示例:
[MCP-SSE] New connection request, assigned sessionId=50894338-1019-44ac-8b3d-5484fa07e526
[MCP-SSE] Active sessions count: 1
[MCP-MSG] Received message, sessionId=50894338-..., body={"jsonrpc":"2.0","id":1,"method":"initialize",...}
[MCP-Handler] Received request, method=initialize, id=1, params={...}
[MCP-Handler] Processing tools/call, id=3, toolName=echo, arguments={"message":"hello world"}
[MCP-Handler] Tool executed successfully, tool=echo, elapsed=1ms, result=hello world
九、扩展新工具
新增工具只需实现 McpTool 接口并加上 @Component 注解,自动注册,无需修改其他代码:
java
@Component
public class MyNewTool implements McpTool {
@Override
public String getName() { return "my_tool"; }
@Override
public String getDescription() { return "描述工具的用途"; }
@Override
public Map<String, Object> getInputSchema() {
// 定义 JSON Schema 格式的入参
}
@Override
public Object execute(Map<String, Object> args) {
// 实现工具逻辑,返回结果
}
}