MCP Server 搭建与使用指南

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) {
        // 实现工具逻辑,返回结果
    }
}
相关推荐
跨境Jacky1 小时前
Shopee AI 选品:Sorftime CLI 实操教程
跨境电商·mcp·sorftime
李姆斯1 小时前
为啥Agent在coding表现这么好,但是在别的领域就是差的不少?
前端·agent·ai编程
nix.gnehc1 小时前
工具表之外 -- 派生、注入与两条原语
agent
YH55269842 小时前
GPT‑5.6 Sol 原本支持 1M 上下文,Codex 现已放开此前限制,如何看待这次调整?
java·jvm·人工智能·gpt·算法·chatgpt
“初生”2 小时前
用 Codex 做一致性 AI 动画:5 步工作流,角色不再漂移
人工智能·ai·chatgpt
AI_小站2 小时前
刚面完百度的 Agent 开发岗,我才发现:世界就是个巨大的草台班子
java·开发语言·人工智能·spring·百度·langchain
许彰午3 小时前
03-三种开发模式
java·架构
涟漪海洋3 小时前
创建最新的JDK25镜像,非root环境启动
java
weixin_431600443 小时前
Agent Workflow 学习向:Code 节点,比模板更强的变量加工
后端·python·学习·ai·