MCP-server

先说 MCP 是什么

MCP 全称 Model Context Protocol,可以理解成 AI 应用调用外部工具的标准化接口 。 它底层基于 JSON-RPC 2.0 ,客户端通过 tools/list 发现工具,再通过 tools/call 调用工具。

作用:在商城场景里,订单查询、物流查询、优惠券、保修这些能力如果直接写死在 Agent 里,后面每加一个工具都要改主应用。把它们抽成 MCP Server 之后,Agent 只需要按协议调用工具,模型侧和工具侧就解耦了。

部署形态:MCP Server 是独立服务

当前实现里,MCP Server 是一个独立的 Spring Boot 服务,不和 RAG 主应用部署在同一个 JVM 里。 MCP Server 的配置非常直观。它就是一个普通 Spring Boot 应用,默认端口 9099:

复制代码
server:
  port: ${MCP_SERVER_PORT:9099}
​
huawei-mall:
  runtime-secret: ${HUAWEI_MALL_RUNTIME_SECRET:}

通信方式:Streamable HTTP + JSON-RPC 2.0

RAG Bootstrap 作为 MCP Client,启动时读取通过 MCP Server 地址,官方 MCP Java SDK 建立连接。 具体参数是:

  • SDK:MCP Java SDK 1.1.2

  • 底层消息:JSON-RPC 2.0

  • 传输方式:Streamable HTTP

  • 端点 :http://localhost:9099/mcp

  • 不是 stdio,也不是旧版独立 SSE Transport

这里要区分两件事:

  • 浏览器聊天 SSE :是前端从 Bootstrap 拉流式回答;

  • MCP Transport SSE/Streamable HTTP:是 MCP Client 和 MCP Server 之间的协议层通信。

Streamable HTTP 是 MCP 协议里推荐的远程通信传输方式,2025-03-26 版本引入,用来替代旧的 HTTP+SSE 传输。

在商城 RAG 里,MCP Server 用 HttpServletStreamableHttpTransportProvider 把 MCP 端点注册到 /mcp,Bootstrap 用 HttpClientStreamableHttpTransport 连接,就是这个机制。

二、MCP Client:Bootstrap 启动时自动连接

mcp端点

主要用于日志标识,比如打印"连接 MCP Server: name=xxx";url 用于建立 HTTP 传输连接。

2)返回值:返回类型是 void

它不是通过返回值把工具列表交出去,而是通过副作用完成注册:

  • 把 McpSyncClient 加入 clients 集合;

  • 把每个工具包装成 McpClientToolExecutor;

  • 注册到 toolRegistry

配置里写的是:

复制代码
rag:
  mcp:
    servers:
      - name: default
        url: ${MALL_AGENT_MCP_URL:http://localhost:9099}

代码里会补 /mcp:

bash 复制代码
String mcpUrl = serverUrl.endsWith("/mcp")
        ? serverUrl
        : serverUrl + "/mcp";

代码处理

registerRemoteTools

下面我先介绍一段代码: 根据配置里的 MCP Server 地址,完成"连接 → 初始化 → 发现工具 → 注册执行器"的启动阶段自动装配。

1)参数是:一个 MCP Server 的配置对象,包含 name 和 url

name 主要用于日志标识,比如打印"连接 MCP Server: name=xxx";url 用于建立 HTTP 传输连接。

2)返回值:返回类型是 void

它不是通过返回值把工具列表交出去,而是通过副作用完成注册:

  • 把 McpSyncClient 加入 clients 集合;

  • 把每个工具包装成 McpClientToolExecutor;

  • 注册到 toolRegistry

java 复制代码
1、解析并补全 MCP 端点
String serverName = server.getName();
String serverUrl = server.getUrl();
String mcpUrl = serverUrl.endsWith("/mcp") ? serverUrl : serverUrl + "/mcp";
如果配置里已经带了 /mcp,就不再重复拼接;否则自动补上。这样配置层可以更灵活,既支持 http://localhost:9099,也支持 http://localhost:9099/mcp。application.yaml:94
​
2、建立 Streamable HTTP 传输
HttpClientStreamableHttpTransport transport =
        HttpClientStreamableHttpTransport.builder(mcpUrl).build();
这里使用的是 MCP 推荐的 Streamable HTTP 传输,底层基于 HTTP + JSON-RPC 2.0。
​
 3、构建 MCP 客户端并初始化
McpSyncClient client = McpClient.sync(transport)
        .clientInfo(new Implementation("ragent-bootstrap", "1.0.0"))
        .build();
client.initialize();
clients.add(client);
initialize() 是 MCP 协议要求的握手步骤。客户端会声明自己的能力和版本,服务端返回自身能力,双方协商协议版本和能力。
当前项目使用的是 MCP Java SDK 1.1.2,协议版本由 SDK 握手协商,业务代码没有写死版本号。pom.xml:30
clients.add(client) 说明这个客户端会被 Bootstrap 持有,后续工具调用时可能复用同一个连接。
​
4、拉取远程工具列表
ListToolsResult result = client.listTools();
List<Tool> tools = result.tools();
​
5、空列表保护 + 执行器注册
for (Tool tool : tools) {
    McpClientToolExecutor executor = new McpClientToolExecutor(client, tool);
    toolRegistry.register(executor);
}
每个远程工具都会被包装成 McpClientToolExecutor,然后注册到 toolRegistry。McpClientAutoConfiguration.java:81
这样业务层后续只需要通过 toolId 拿执行器,不需要知道它来自哪个 Server,也不需要直接操作 MCP Client。
​
6、 异常隔离
​
private void registerRemoteTools(McpClientProperties.ServerConfig server)

McpClientToolExecutor #execute

复制代码
@Override
public CallToolResult execute(Map<String, Object> parameters)

这个方法是 MCP 远程工具在 Bootstrap 侧的最终执行入口。McpClientToolExecutor.java:45

它的职责很窄:把业务层已经准备好的参数,通过 MCP 客户端转发给远程 Server,并把结果原样返回;如果调用失败,则返回一个标准错误结果。

java 复制代码
1、返回值
返回类型是 CallToolResult。
正常时:返回 MCP Server 返回的原始结果;
异常时:返回一个包含错误文本的 CallToolResult,并标记 isError(true)。
2、参数
Map<String, Object>这是调用工具时传入的参数,已经过实体解析、参数门禁和身份签名注入
3、实现思路
1)空值保护避免传入 null 导致下游 SDK 报错
2)通过mcpClient.callTool发起远程调用
CallToolResult result = mcpClient.callTool(
        new CallToolRequest(toolDefinition.name(), args)
);
这里真正发出 MCP 协议的 tools/call 请求。toolDefinition.name() 决定调用哪个工具,args 是工具参数。
3)成功日志
这里只记录工具名、脱敏后的参数、返回内容数量和耗时,不记录完整返回体,避免日志过大。
4)异常兜底
异常没有继续向上抛,而是转换成标准 MCP 错误结果。这样上层业务可以统一按 CallToolResult 处理,不需要区分"远程调用异常"和"工具返回错误"。
复制代码

McpToolRegistry.java #register

java 复制代码
 1、参数
McpToolExecutor 待注册的工具执行器,封装了 MCP 客户端、工具定义和 execute 方法
2、返回值 无返回值,返回类型是 void
它的效果是副作用式的:把执行器写入内部的 executorMap,完成工具路由表的更新。
3、实现思路:
1)空值保护
先防两个空:执行器本身为 null,或者执行器里的 toolDefinition 为 null。这两种情况都没法注册,直接忽略。
2)toolId 校验
toolId 是路由的 key,必须非空。
3)写入路由表
McpToolExecutor existing = executorMap.put(toolId, executor);
executorMap 是一个 Map<String, McpToolExecutor>,key 是 toolId,value 是执行器。put 返回的是旧值,如果非 null 说明之前已经注册过同名工具。
4)日志区分
 
@Override
    public void register(McpToolExecutor executor);   

三、身份校验

在这个项目里,认证不是"一层 Sa-Token 包所有",而是分成两段:前端到 Bootstrap 用 Sa-Token,Bootstrap 到 MCP Server 用 HMAC 运行时签名。这两段保护的对象、位置和协议都不一样,不能混在一起说。

1. 前端到 Bootstrap:Sa-Token 是入口认证

前端发起普通 HTTP 请求或 SSE 聊天请求时,会从本地存储里取出 Token,并放到 Authorization Header 中。api.ts:21chatStore.ts:427

这个请求的目标是 Bootstrap,不是 MCP Server。

Bootstrap 侧通过 Sa-Token 配置读取这个 Header:

java 复制代码
sa-token:
  token-name: Authorization
  timeout: 2592000
  is-concurrent: true
  is-share: false
  token-style: simple-uuid

然后通过拦截器对所有业务路径做登录校验:

java 复制代码
registry.addInterceptor(new SaInterceptor(handler -> {
    if (request.getDispatcherType() == DispatcherType.ASYNC) {
        return;
    }
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        return;
    }
    StpUtil.checkLogin();
}))
.addPathPatterns("/**")
.excludePathPatterns("/auth/**", "/error");

所以 Sa-Token 保护的是 /rag/**、/writing/**、/user/** 这些 Bootstrap 业务接口。校验失败时,会抛出未登录或 Token 失效异常,由 Web 异常处理链返回认证错误。SaTokenConfig.java:56

2. 校验通过后,身份被写进 UserContext

Sa-Token 只负责判断"这个请求是不是已登录用户"。真正后续业务要用到的身份,是在另一个拦截器里写入的:

java 复制代码
String loginId = StpUtil.getLoginIdAsString();
UserDO user = userMapper.selectById(loginId);
​
UserContext.set(
    LoginUser.builder()
        .userId(user.getId().toString())
        .username(user.getUsername())
        .role(user.getRole())
        .build()
);

请求结束后再清理:

java 复制代码
UserContext.clear();

后续商城工具执行时使用的身份,就来自这个 UserContext。UserContextInterceptor.java:59

3、真正的 MCP 身份校验:HMAC 运行时签名

Bootstrap 在调用私有商城工具前,会从 UserContext 取出已登录用户身份,然后构造运行时参数:

复制代码
_runtimePrincipal
_runtimeIssuedAt
_runtimeProof
字段 作用 说明
_runtimePrincipal 身份主体 表示当前登录用户是谁
_runtimeIssuedAt 时间戳 表示这个证明是什么时候签发的
_runtimeProof 签名 证明上述信息确实由 Bootstrap 签发

其中:

复制代码
_runtimeProof =
    HMAC-SHA256(toolName + "\n" + principal + "\n" + issuedAt)

这部分由 HuaweiMallRuntimeArguments 生成。HuaweiMallRuntimeArguments.java:54

MCP Server 侧收到后,会验证:

  • principal 格式是否合法;

  • 时间戳是否超过 60 秒;

  • HMAC 签名是否正确;

    MCP Server 持有的共享密钥,按同样规则重新计算:

    复制代码
    HMAC-SHA256(toolName + "\n" + principal + "\n" + issuedAt)
  • 比较时使用常量时间比较,防止时序攻击。

对应实现在:

  • HuaweiMallRuntimeProof.requirePrincipalHuaweiMallRuntimeProof.java:36

  • HuaweiMallMcpExecutor 中的校验逻辑HuaweiMallMcpExecutor.java:100

所以 MCP Server 并不是靠 HTTP Header 认人,而是靠 Bootstrap 签发的、带时效的、绑定工具名的 HMAC 证明 来确认这次调用是否可信。

HMAC 运行时签名 介绍

HMAC 运行时签名本质上是 Bootstrap 和 MCP Server 之间的一种服务间信任凭证:Bootstrap 用共享密钥对"工具名 + 用户身份 + 时间戳"做签名,MCP Server 收到后重新计算并比对,从而确认这次工具调用确实来自可信的 Bootstrap,并且代表某个已登录用户。

可以把它理解成:Sa-Token 是用户进入 Bootstrap 的门票,HMAC 是 Bootstrap 进入 MCP Server 工具层的临时通行证明。

四、mcp-server工具调用实现

商城 MCP Server 的工具体系可以拆成三层,每一层职责边界都很清晰:

复制代码
MCP 工具定义与 Schema
    ↓
协议调用适配、鉴权
    ↓
业务参数校验、数据库查询、统一结果封装

对应的三个核心文件是:

• 工具定义与协议适配:HuaweiMallMcpExecutor.java 负责"对外声明我有哪些工具、参数长什么样"

业务实现与结果模型:HuaweiMallToolService.java 负责"收到调用后做鉴权、剥离鉴权字段、把请求转给业务层"

注册:McpServerConfig.java负责"真正查库、校验、把结果标准化"。

当前商城定义了8个只读工具:

复制代码
searchProducts
getProductAvailability
getOrder
getLogistics
getCouponEligibility
getWarrantyStatus
getSparePartPrice
getRepairProgress

每个工具通过Spring @Bean 暴露为一个 SyncToolSpecification。

例如保修查询:

java 复制代码
@Bean
public McpServerFeatures.SyncToolSpecification warrantyTool() {
    return spec(
        "getWarrantyStatus",
        "Query warranty of one owned device by its exact serial number",
        props(
            "sn",
            p("string", "Exact device SN")
        ),
        List.of("sn")
    );
}

2. 公共 spec() 生成工具定义

1)这个 spec() 是整个 MCP 工具声明体系的核心工厂方法,所有具体工具 Bean(如 warrantyTool())最终都调用它来产出 SyncToolSpecification。

2)参数:方法接收四个入参,正好对应 MCP 协议 tools/list 时客户端看到的工具元信息:

复制代码
name 工具唯一标识,如 "getWarrantyStatus",同时充当后续 call() 里的路由键
​
description 工具用途说明,写给模型看,影响模型是否选择调用它
​
properties Map<String, Object> JSON Schema 的 properties,声明每个参数的类型、描述、枚举等结构
​
required 必填参数名列表,如 List.of("sn")

3)返回值 McpServerFeatures.SyncToolSpecification

复制代码
这是一个 MCP Server 框架要求的工具规格对象,内部同时装着两样东西:

这是一个 MCP Server 框架要求的工具规格对象,内部同时装着两样东西:

  1. Tool :对外暴露的工具元数据(name + description + inputSchema),用于 tools/list 协议;

  2. callHandler :一个 lambda (exchange, request) -> call(name, request),当真正收到 tools/call 时执行。

所有工具最终都经过:

java 复制代码
1、第一步:构造 Tool 对象
用 Builder 模式把 name、description、inputSchema 组装成一个 Tool。
2、绑定执行入口,产出 SyncToolSpecification
return new McpServerFeatures.SyncToolSpecification(
        tool,
        (exchange, request) -> call(name, request)
);
​
private McpServerFeatures.SyncToolSpecification spec(
        String name,
        String description,
        Map<String, Object> properties,
        List<String> required);
​

架构

复制代码
具体工具 Bean (warrantyTool / spareTool / ...)
        ↓ 调用
spec(name, description, properties, required)    ← 本方法
        ↓ 产出
SyncToolSpecification(tool + callHandler)
        ↓ 被 Spring 收集
McpServerConfig.mcpServer(toolSpecs)
        ↓ 统一注册
McpSyncServer
相关推荐
ZealSinger1 小时前
Boot4升级PostConstruct不跑了
java·spring boot·spring·升级迁移
Code_Solitude1 小时前
C语言:关于二维数组的作业总结
java·c语言·前端
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi
专业程序开发源1 小时前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计
vx_Biye_Design2 小时前
springboot一站式旅游管理平台81037-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·课程设计·express·旅游
写后端的胖头鱼2 小时前
时间复杂度 & 空间复杂度
java·数据结构·算法·时间复杂度·空间复杂度
嵌入式学习菌2 小时前
ESP32 ModbusTCP 分片缓存
java·后端·spring
vx_Biye_Design2 小时前
springboot宠物寄养服务预约与监管系统82684-计算机课程设计、毕业设计
java·vue.js·spring boot·elasticsearch·课程设计·express·宠物