add 增加 ruoyi-common-mcp 模块 支持作为server提供mcp接口提供数据 与 接入多个client做数据采集

This commit is contained in:
疯狂的狮子Li
2026-05-18 17:24:59 +08:00
parent a2300b3a29
commit f237d0c391
15 changed files with 548 additions and 0 deletions
+11
View File
@@ -52,6 +52,8 @@
<warm-flow.version>1.8.7</warm-flow.version>
<!-- mqtt客户端 -->
<mica-mqtt.version>2.5.12</mica-mqtt.version>
<!-- Spring AI 2.0 预览版,正式版发布后仅需调整此版本号 -->
<spring-ai.version>2.0.0-M6</spring-ai.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
@@ -113,6 +115,15 @@
<scope>import</scope>
</dependency>
<!-- Spring AI MCP 依赖配置 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
+5
View File
@@ -60,6 +60,11 @@
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mcp</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
@@ -281,3 +281,49 @@ mqtt.client:
keystore-pass:
truststore-path:
truststore-pass:
--- # MCP 服务端配置
spring.ai.mcp:
server:
# 与 ruoyi-admin 共用端口,默认端点为 /mcp
enabled: true
protocol: STREAMABLE
name: ${spring.application.name}
version: ${project.version}
type: SYNC
annotation-scanner:
enabled: true
streamable-http:
mcp-endpoint: /mcp
--- # MCP 客户端配置
spring.ai.mcp:
client:
# 需要接入外部 MCP Server 时再打开,并配置 streamable-http/sse/stdio connections
enabled: false
name: ${spring.application.name}-mcp-client
version: ${project.version}
type: SYNC
toolcallback:
enabled: false
# Streamable HTTP 多服务端示例
streamable-http:
connections:
knowledge:
url: http://localhost:9001
crm:
url: http://localhost:9002
# SSE 多服务端示例
sse:
connections:
search:
url: http://localhost:9003
# STDIO 多服务端示例
stdio:
connections:
filesystem:
command: npx
args:
- -y
- '@modelcontextprotocol/server-filesystem'
- D:/data
+1
View File
@@ -37,6 +37,7 @@
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-push</module>
<module>ruoyi-common-mqtt</module>
<module>ruoyi-common-mcp</module>
</modules>
</project>
+7
View File
@@ -159,6 +159,13 @@
<version>${revision}</version>
</dependency>
<!-- mcp模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mcp</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-mcp</artifactId>
<description>
ruoyi-common-mcp mcp模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,29 @@
package org.dromara.common.mcp.config;
import io.modelcontextprotocol.client.McpSyncClient;
import org.dromara.common.mcp.core.McpClientTemplate;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* MCP 公共模块自动配置。
*
* @author Lion Li
*/
@AutoConfiguration
public class McpAutoConfiguration {
/**
* MCP Client 通用操作模板。
*/
@Bean
@ConditionalOnBean(McpSyncClient.class)
@ConditionalOnMissingBean
public McpClientTemplate mcpClientTemplate(List<McpSyncClient> mcpSyncClients) {
return new McpClientTemplate(mcpSyncClients);
}
}
@@ -0,0 +1,125 @@
package org.dromara.common.mcp.core;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.RequiredArgsConstructor;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* MCP Client 通用操作模板。
* <p>
* Spring AI 已经负责 MCP Client 的创建、初始化与连接管理,本模板只做项目内常用调用封装,
* 避免业务模块直接遍历 `McpSyncClient` 或重复处理返回结构。
*
* @author Lion Li
*/
@RequiredArgsConstructor
public class McpClientTemplate {
private final List<McpSyncClient> mcpSyncClients;
/**
* 查询所有已连接 MCP Server 的工具列表。
*
* @return Server 名称与工具名称列表
*/
public Map<String, List<String>> listTools() {
Map<String, List<String>> result = new LinkedHashMap<>();
for (McpSyncClient client : mcpSyncClients) {
List<String> tools = client.listTools().tools().stream()
.map(McpSchema.Tool::name)
.toList();
result.put(getServerName(client), tools);
}
return result;
}
/**
* 调用所有 MCP Server 上的同名工具。
*
* @param toolName 工具名称
* @param arguments 工具参数
* @return 各 Server 的工具调用结果
*/
public Map<String, McpToolCallResult> callTool(String toolName, Map<String, Object> arguments) {
Map<String, McpToolCallResult> result = new LinkedHashMap<>();
for (McpSyncClient client : mcpSyncClients) {
McpSchema.CallToolResult callResult = client.callTool(new McpSchema.CallToolRequest(toolName, arguments));
result.put(getServerName(client), McpToolCallResult.of(getServerName(client), callResult));
}
return result;
}
/**
* 调用指定 MCP Server 上的工具。
*
* @param serverName Server 名称
* @param toolName 工具名称
* @param arguments 工具参数
* @return 工具调用结果
*/
public Optional<McpToolCallResult> callTool(String serverName, String toolName, Map<String, Object> arguments) {
return findClient(serverName)
.map(client -> McpToolCallResult.of(serverName, client.callTool(new McpSchema.CallToolRequest(toolName, arguments))));
}
/**
* 读取所有 MCP Server 上的同名资源。
*
* @param uri 资源地址
* @return 各 Server 的资源内容
*/
public Map<String, McpResourceReadResult> readResource(String uri) {
Map<String, McpResourceReadResult> result = new LinkedHashMap<>();
for (McpSyncClient client : mcpSyncClients) {
McpSchema.ReadResourceResult readResult = client.readResource(new McpSchema.ReadResourceRequest(uri));
result.put(getServerName(client), McpResourceReadResult.of(getServerName(client), readResult));
}
return result;
}
/**
* 读取指定 MCP Server 上的资源。
*
* @param serverName Server 名称
* @param uri 资源地址
* @return 资源内容
*/
public Optional<McpResourceReadResult> readResource(String serverName, String uri) {
return findClient(serverName)
.map(client -> McpResourceReadResult.of(serverName, client.readResource(new McpSchema.ReadResourceRequest(uri))));
}
/**
* 按 Server 名称查找 MCP Client。
*
* @param serverName Server 名称
* @return MCP Client
*/
public Optional<McpSyncClient> findClient(String serverName) {
return mcpSyncClients.stream()
.filter(client -> serverName.equals(getServerName(client)))
.findFirst();
}
/**
* 查询已连接的 MCP Client。
*
* @return MCP Client 列表
*/
public List<McpSyncClient> getClients() {
return mcpSyncClients;
}
private String getServerName(McpSyncClient client) {
McpSchema.Implementation serverInfo = client.getServerInfo();
if (serverInfo == null || serverInfo.name() == null) {
return "unknown";
}
return serverInfo.name();
}
}
@@ -0,0 +1,20 @@
package org.dromara.common.mcp.core;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
/**
* MCP 资源读取结果。
*
* @author Lion Li
*/
public record McpResourceReadResult(
String serverName,
List<McpSchema.ResourceContents> contents
) {
public static McpResourceReadResult of(String serverName, McpSchema.ReadResourceResult result) {
return new McpResourceReadResult(serverName, result.contents());
}
}
@@ -0,0 +1,27 @@
package org.dromara.common.mcp.core;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
/**
* MCP 工具调用结果。
*
* @author Lion Li
*/
public record McpToolCallResult(
String serverName,
boolean error,
List<McpSchema.Content> content,
Object structuredContent
) {
public static McpToolCallResult of(String serverName, McpSchema.CallToolResult result) {
return new McpToolCallResult(
serverName,
Boolean.TRUE.equals(result.isError()),
result.content(),
result.structuredContent()
);
}
}
@@ -0,0 +1 @@
org.dromara.common.mcp.config.McpAutoConfiguration
+5
View File
@@ -98,6 +98,11 @@
<artifactId>ruoyi-common-mqtt</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mcp</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,64 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mcp.core.McpResourceReadResult;
import org.dromara.demo.mcp.McpDemoClientService;
import org.dromara.demo.mcp.McpDemoClientService.McpDemoHandleResult;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* MCP Client 演示案例。
* <p>
* 这些接口不是 MCP 协议接口,只是为了在后台系统中手动触发 MCP Client 调用。
* 真实 MCP Server 入口由 Spring AI 挂载到 `/mcp`。
* <p>
* 调用示例:
* <ul>
* <li>`GET /demo/mcp/tools`:查看已连接 MCP Server 的工具列表</li>
* <li>`GET /demo/mcp/receive?toolName=demo_get_data&amp;id=1`:调用外部工具并模拟业务处理</li>
* <li>`GET /demo/mcp/resource?uri=demo://summary`:读取外部 MCP 资源</li>
* </ul>
* 当 `spring.ai.mcp.client.enabled=false` 时,本 Controller 不会注册。
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mcp")
@ConditionalOnBean(McpDemoClientService.class)
public class McpDemoController {
private final McpDemoClientService mcpDemoClientService;
/**
* 查询外部 MCP Server 工具列表。
*/
@GetMapping("/tools")
public R<Map<String, java.util.List<String>>> tools() {
return R.ok(mcpDemoClientService.listRemoteTools());
}
/**
* 调用外部 MCP 工具并模拟业务处理。
*/
@GetMapping("/receive")
public R<McpDemoHandleResult> receive(@RequestParam(defaultValue = "demo_get_data") String toolName,
@RequestParam(defaultValue = "1") String id) {
return R.ok(mcpDemoClientService.receiveAndHandle(toolName, Map.of("id", id)));
}
/**
* 读取外部 MCP 资源。
*/
@GetMapping("/resource")
public R<Map<String, McpResourceReadResult>> resource(@RequestParam(defaultValue = "demo://summary") String uri) {
return R.ok(mcpDemoClientService.readRemoteResource(uri));
}
}
@@ -0,0 +1,88 @@
package org.dromara.demo.mcp;
import org.dromara.common.mcp.core.McpClientTemplate;
import org.dromara.common.mcp.core.McpResourceReadResult;
import org.dromara.common.mcp.core.McpToolCallResult;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* MCP Client 演示服务,从外部 MCP Server 接收数据后交给业务层处理。
* <p>
* 本服务会注入 Spring AI 自动创建的多个 `McpSyncClient`。当
* `spring.ai.mcp.client.enabled=true` 且配置了多个 connection 时,
* 这里可以统一遍历这些 MCP Server。
* <p>
* 如果要用当前项目自己连自己测试,建议启动两个 `ruoyi-admin` 实例:
* 一个作为 server,一个作为 clientclient 指向 server 的地址。
*
* @author Lion Li
*/
@Service
@RequiredArgsConstructor
@ConditionalOnBean(McpClientTemplate.class)
public class McpDemoClientService {
private final McpClientTemplate mcpClientTemplate;
/**
* 查询已接入的 MCP Server 与工具列表。
*
* @return Server 名称与工具名称列表
*/
public Map<String, List<String>> listRemoteTools() {
return mcpClientTemplate.listTools();
}
/**
* 调用外部 MCP 工具接收数据。
*
* @param toolName 工具名称
* @param arguments 工具参数
* @return 工具返回内容
*/
public Map<String, McpToolCallResult> callRemoteTool(String toolName, Map<String, Object> arguments) {
return mcpClientTemplate.callTool(toolName, arguments);
}
/**
* 读取外部 MCP 资源。
*
* @param uri 资源地址
* @return 资源内容
*/
public Map<String, McpResourceReadResult> readRemoteResource(String uri) {
return mcpClientTemplate.readResource(uri);
}
/**
* 模拟业务处理入口。真实业务可在这里做校验、转换、去重、入库和审计。
* <p>
* MCP 返回数据不建议直接入库,应先转换成业务 BO/DTO,再进入业务 Service。
*
* @param toolName 工具名称
* @param arguments 工具参数
* @return 处理结果
*/
public McpDemoHandleResult receiveAndHandle(String toolName, Map<String, Object> arguments) {
return new McpDemoHandleResult("MCP", true, callRemoteTool(toolName, arguments));
}
/**
* MCP 数据处理结果。
*
* @param sourceType 数据来源类型
* @param handled 是否已处理
* @param data MCP 原始返回数据
*/
public record McpDemoHandleResult(
String sourceType,
boolean handled,
Map<String, McpToolCallResult> data
) {
}
}
@@ -0,0 +1,85 @@
package org.dromara.demo.mcp;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* MCP Server 演示工具,对外提供业务数据。
* <p>
* 启动 `ruoyi-admin` 后,本类中的 `@McpTool` 与 `@McpResource` 会被 Spring AI
* MCP 注解扫描器注册到当前应用的 MCP Server,默认通过同端口 `/mcp` 暴露。
* <p>
* 可被外部 MCP Client 调用的示例能力:
* <ul>
* <li>`demo_get_data`:根据 id 查询一条演示数据</li>
* <li>`demo_list_data`:按 limit 查询演示数据列表</li>
* <li>`demo://summary`:读取演示模块摘要资源</li>
* </ul>
* 真实业务中不要在 MCP 工具里直接访问 Mapper,应继续调用业务 Service
* 并保留权限、租户、脱敏、审计和幂等规则。
*
* @author Lion Li
*/
@Component
public class McpDemoServerTool {
/**
* 查询一条演示数据。
*
* @param id 业务主键
* @return 演示数据
*/
@McpTool(name = "demo_get_data", description = "根据业务主键查询演示数据")
public McpDemoData getData(@McpToolParam(description = "业务主键") String id) {
return new McpDemoData(id, "演示数据-" + id, "ruoyi-demo", 100, LocalDateTime.now());
}
/**
* 查询演示数据列表。
*
* @param limit 返回条数
* @return 演示数据列表
*/
@McpTool(name = "demo_list_data", description = "查询演示数据列表")
public List<McpDemoData> listData(@McpToolParam(description = "返回条数") Integer limit) {
int size = limit == null || limit < 1 ? 3 : Math.min(limit, 20);
return java.util.stream.IntStream.rangeClosed(1, size)
.mapToObj(index -> new McpDemoData(String.valueOf(index), "演示数据-" + index, "ruoyi-demo", index * 10, LocalDateTime.now()))
.toList();
}
/**
* 读取演示资源。
*
* @return 演示资源内容
*/
@McpResource(
name = "demo_summary",
uri = "demo://summary",
description = "演示模块摘要资源",
mimeType = "text/plain"
)
public String summary() {
return "ruoyi-demo MCP resource summary";
}
/**
* MCP 演示数据。
*
* @author Lion Li
*/
public record McpDemoData(
String id,
String name,
String source,
Integer amount,
LocalDateTime syncTime
) {
}
}