diff --git a/pom.xml b/pom.xml index 19b221a43..9b72103fc 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,8 @@ 1.8.7 2.5.12 + + 2.0.0-M6 3.5.0 @@ -113,6 +115,15 @@ import + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + cn.hutool diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 80ef4b3d5..974128778 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -60,6 +60,11 @@ ruoyi-common-mail + + org.dromara + ruoyi-common-mcp + + org.dromara ruoyi-api diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 015f6a284..beab6585f 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -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 diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index f6a30c47f..fb14db339 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -37,6 +37,7 @@ ruoyi-common-encrypt ruoyi-common-push ruoyi-common-mqtt + ruoyi-common-mcp diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index b60c35f40..809474e19 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -159,6 +159,13 @@ ${revision} + + + org.dromara + ruoyi-common-mcp + ${revision} + + diff --git a/ruoyi-common/ruoyi-common-mcp/pom.xml b/ruoyi-common/ruoyi-common-mcp/pom.xml new file mode 100644 index 000000000..9c3ba8ab5 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/pom.xml @@ -0,0 +1,34 @@ + + + + org.dromara + ruoyi-common + ${revision} + + 4.0.0 + + ruoyi-common-mcp + + + ruoyi-common-mcp mcp模块 + + + + + org.dromara + ruoyi-common-core + + + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + diff --git a/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/config/McpAutoConfiguration.java b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/config/McpAutoConfiguration.java new file mode 100644 index 000000000..7802a8c4a --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/config/McpAutoConfiguration.java @@ -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 mcpSyncClients) { + return new McpClientTemplate(mcpSyncClients); + } +} diff --git a/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpClientTemplate.java b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpClientTemplate.java new file mode 100644 index 000000000..962665cdd --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpClientTemplate.java @@ -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 通用操作模板。 + *

+ * Spring AI 已经负责 MCP Client 的创建、初始化与连接管理,本模板只做项目内常用调用封装, + * 避免业务模块直接遍历 `McpSyncClient` 或重复处理返回结构。 + * + * @author Lion Li + */ +@RequiredArgsConstructor +public class McpClientTemplate { + + private final List mcpSyncClients; + + /** + * 查询所有已连接 MCP Server 的工具列表。 + * + * @return Server 名称与工具名称列表 + */ + public Map> listTools() { + Map> result = new LinkedHashMap<>(); + for (McpSyncClient client : mcpSyncClients) { + List 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 callTool(String toolName, Map arguments) { + Map 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 callTool(String serverName, String toolName, Map arguments) { + return findClient(serverName) + .map(client -> McpToolCallResult.of(serverName, client.callTool(new McpSchema.CallToolRequest(toolName, arguments)))); + } + + /** + * 读取所有 MCP Server 上的同名资源。 + * + * @param uri 资源地址 + * @return 各 Server 的资源内容 + */ + public Map readResource(String uri) { + Map 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 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 findClient(String serverName) { + return mcpSyncClients.stream() + .filter(client -> serverName.equals(getServerName(client))) + .findFirst(); + } + + /** + * 查询已连接的 MCP Client。 + * + * @return MCP Client 列表 + */ + public List getClients() { + return mcpSyncClients; + } + + private String getServerName(McpSyncClient client) { + McpSchema.Implementation serverInfo = client.getServerInfo(); + if (serverInfo == null || serverInfo.name() == null) { + return "unknown"; + } + return serverInfo.name(); + } +} diff --git a/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpResourceReadResult.java b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpResourceReadResult.java new file mode 100644 index 000000000..afdaab154 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpResourceReadResult.java @@ -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 contents +) { + + public static McpResourceReadResult of(String serverName, McpSchema.ReadResourceResult result) { + return new McpResourceReadResult(serverName, result.contents()); + } +} diff --git a/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpToolCallResult.java b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpToolCallResult.java new file mode 100644 index 000000000..80bd2d4cb --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/src/main/java/org/dromara/common/mcp/core/McpToolCallResult.java @@ -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 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() + ); + } +} diff --git a/ruoyi-common/ruoyi-common-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..cd71c1bbd --- /dev/null +++ b/ruoyi-common/ruoyi-common-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.dromara.common.mcp.config.McpAutoConfiguration diff --git a/ruoyi-modules/ruoyi-demo/pom.xml b/ruoyi-modules/ruoyi-demo/pom.xml index da6105015..847c7d6bf 100644 --- a/ruoyi-modules/ruoyi-demo/pom.xml +++ b/ruoyi-modules/ruoyi-demo/pom.xml @@ -98,6 +98,11 @@ ruoyi-common-mqtt + + org.dromara + ruoyi-common-mcp + + diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java new file mode 100644 index 000000000..163fe7eb9 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/McpDemoController.java @@ -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 演示案例。 + *

+ * 这些接口不是 MCP 协议接口,只是为了在后台系统中手动触发 MCP Client 调用。 + * 真实 MCP Server 入口由 Spring AI 挂载到 `/mcp`。 + *

+ * 调用示例: + *

+ * 当 `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>> tools() { + return R.ok(mcpDemoClientService.listRemoteTools()); + } + + /** + * 调用外部 MCP 工具并模拟业务处理。 + */ + @GetMapping("/receive") + public R 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> resource(@RequestParam(defaultValue = "demo://summary") String uri) { + return R.ok(mcpDemoClientService.readRemoteResource(uri)); + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java new file mode 100644 index 000000000..da4e3701b --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoClientService.java @@ -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 接收数据后交给业务层处理。 + *

+ * 本服务会注入 Spring AI 自动创建的多个 `McpSyncClient`。当 + * `spring.ai.mcp.client.enabled=true` 且配置了多个 connection 时, + * 这里可以统一遍历这些 MCP Server。 + *

+ * 如果要用当前项目自己连自己测试,建议启动两个 `ruoyi-admin` 实例: + * 一个作为 server,一个作为 client,client 指向 server 的地址。 + * + * @author Lion Li + */ +@Service +@RequiredArgsConstructor +@ConditionalOnBean(McpClientTemplate.class) +public class McpDemoClientService { + + private final McpClientTemplate mcpClientTemplate; + + /** + * 查询已接入的 MCP Server 与工具列表。 + * + * @return Server 名称与工具名称列表 + */ + public Map> listRemoteTools() { + return mcpClientTemplate.listTools(); + } + + /** + * 调用外部 MCP 工具接收数据。 + * + * @param toolName 工具名称 + * @param arguments 工具参数 + * @return 工具返回内容 + */ + public Map callRemoteTool(String toolName, Map arguments) { + return mcpClientTemplate.callTool(toolName, arguments); + } + + /** + * 读取外部 MCP 资源。 + * + * @param uri 资源地址 + * @return 资源内容 + */ + public Map readRemoteResource(String uri) { + return mcpClientTemplate.readResource(uri); + } + + /** + * 模拟业务处理入口。真实业务可在这里做校验、转换、去重、入库和审计。 + *

+ * MCP 返回数据不建议直接入库,应先转换成业务 BO/DTO,再进入业务 Service。 + * + * @param toolName 工具名称 + * @param arguments 工具参数 + * @return 处理结果 + */ + public McpDemoHandleResult receiveAndHandle(String toolName, Map 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 data + ) { + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoServerTool.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoServerTool.java new file mode 100644 index 000000000..2988936f9 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/mcp/McpDemoServerTool.java @@ -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 演示工具,对外提供业务数据。 + *

+ * 启动 `ruoyi-admin` 后,本类中的 `@McpTool` 与 `@McpResource` 会被 Spring AI + * MCP 注解扫描器注册到当前应用的 MCP Server,默认通过同端口 `/mcp` 暴露。 + *

+ * 可被外部 MCP Client 调用的示例能力: + *

    + *
  • `demo_get_data`:根据 id 查询一条演示数据
  • + *
  • `demo_list_data`:按 limit 查询演示数据列表
  • + *
  • `demo://summary`:读取演示模块摘要资源
  • + *
+ * 真实业务中不要在 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 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 + ) { + } + +}