From deea428bfa2bc00be672337b0f939f34b0c54493 Mon Sep 17 00:00:00 2001 From: OpenX123 <2113239898goole@gmail.com> Date: Fri, 12 Dec 2025 13:09:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=8A=A0=E8=BD=BD=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=93=8D=E4=BD=9C=E3=80=81=E5=9B=BE=E7=89=87=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E3=80=81PlantUML=E7=94=9F=E6=88=90=E3=80=81=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E6=90=9C=E7=B4=A2=E3=80=81=E7=BB=88=E7=AB=AF=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E3=80=81=E6=96=87=E6=A1=A3=E8=A7=A3=E6=9E=90=E7=AD=89?= =?UTF-8?q?=E5=AE=9E=E7=94=A8MCP=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-extend/ruoyi-mcp-server/pom.xml | 33 +++++ .../mcpserve/RuoyiMcpServeApplication.java | 12 +- .../config/DynamicToolCallbackProvider.java | 104 ++++++++++++++ .../ruoyi/mcpserve/config/McpToolsConfig.java | 75 ++++++++++ .../mcpserve/config/ToolsProperties.java | 65 +++++++++ .../mcpserve/controller/ToolsController.java | 128 ++++++++++++++++++ .../BasicTools.java} | 23 ++-- .../ruoyi/mcpserve/tools/DocumentTools.java | 41 ++++++ .../org/ruoyi/mcpserve/tools/FileTools.java | 55 ++++++++ .../mcpserve/tools/ImageSearchTools.java | 70 ++++++++++ .../org/ruoyi/mcpserve/tools/McpTool.java | 17 +++ .../ruoyi/mcpserve/tools/PlantUmlTools.java | 87 ++++++++++++ .../ruoyi/mcpserve/tools/TerminalTools.java | 75 ++++++++++ .../ruoyi/mcpserve/tools/WebPageTools.java | 67 +++++++++ .../ruoyi/mcpserve/tools/WebSearchTools.java | 88 ++++++++++++ .../src/main/resources/application.yml | 34 +++++ 16 files changed, 956 insertions(+), 18 deletions(-) create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/DynamicToolCallbackProvider.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/McpToolsConfig.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/controller/ToolsController.java rename ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/{service/ToolService.java => tools/BasicTools.java} (71%) create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/DocumentTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/FileTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/ImageSearchTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/McpTool.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/PlantUmlTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/TerminalTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebPageTools.java create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebSearchTools.java diff --git a/ruoyi-extend/ruoyi-mcp-server/pom.xml b/ruoyi-extend/ruoyi-mcp-server/pom.xml index a35f0140..6dfb5d17 100644 --- a/ruoyi-extend/ruoyi-mcp-server/pom.xml +++ b/ruoyi-extend/ruoyi-mcp-server/pom.xml @@ -47,6 +47,39 @@ test + + + cn.hutool + hutool-all + 5.8.25 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + net.sourceforge.plantuml + plantuml + 1.2024.3 + + + + + org.springframework.ai + spring-ai-tika-document-reader + + + + + org.projectlombok + lombok + true + diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/RuoyiMcpServeApplication.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/RuoyiMcpServeApplication.java index 54a01deb..9abee854 100644 --- a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/RuoyiMcpServeApplication.java +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/RuoyiMcpServeApplication.java @@ -1,13 +1,12 @@ package org.ruoyi.mcpserve; -import org.ruoyi.mcpserve.service.ToolService; -import org.springframework.ai.tool.ToolCallbackProvider; -import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; /** + * MCP Server 应用启动类 + * 工具通过 DynamicToolCallbackProvider 动态加载 + * * @author ageer */ @SpringBootApplication @@ -17,9 +16,4 @@ public class RuoyiMcpServeApplication { SpringApplication.run(RuoyiMcpServeApplication.class, args); } - @Bean - public ToolCallbackProvider systemTools(ToolService toolService) { - return MethodToolCallbackProvider.builder().toolObjects(toolService).build(); - } - } diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/DynamicToolCallbackProvider.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/DynamicToolCallbackProvider.java new file mode 100644 index 00000000..e4c0f6f3 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/DynamicToolCallbackProvider.java @@ -0,0 +1,104 @@ +package org.ruoyi.mcpserve.config; + +import org.ruoyi.mcpserve.tools.McpTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 动态工具回调提供者 + * 根据配置动态加载启用的MCP工具 + * + * @author OpenX + */ +@Component +public class DynamicToolCallbackProvider implements ToolCallbackProvider { + + private static final Logger log = LoggerFactory.getLogger(DynamicToolCallbackProvider.class); + + private final McpToolsConfig mcpToolsConfig; + private final List allTools; + private volatile ToolCallback[] cachedCallbacks; + + public DynamicToolCallbackProvider(McpToolsConfig mcpToolsConfig, List allTools) { + this.mcpToolsConfig = mcpToolsConfig; + this.allTools = allTools; + log.info("发现 {} 个MCP工具", allTools.size()); + } + + @Override + public ToolCallback[] getToolCallbacks() { + if (cachedCallbacks == null) { + synchronized (this) { + if (cachedCallbacks == null) { + cachedCallbacks = buildToolCallbacks(); + } + } + } + return cachedCallbacks; + } + + /** + * 构建工具回调数组 + */ + private ToolCallback[] buildToolCallbacks() { + List enabledTools = allTools.stream() + .filter(tool -> { + boolean enabled = mcpToolsConfig.isToolEnabled(tool.getToolName()); + if (enabled) { + log.info("启用工具: {}", tool.getToolName()); + } else { + log.info("禁用工具: {}", tool.getToolName()); + } + return enabled; + }) + .collect(Collectors.toList()); + + if (enabledTools.isEmpty()) { + log.warn("没有启用任何MCP工具"); + return new ToolCallback[0]; + } + + // 使用 MethodToolCallbackProvider 构建工具回调 + MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder() + .toolObjects(enabledTools.toArray()) + .build(); + + return provider.getToolCallbacks(); + } + + /** + * 刷新工具缓存,用于配置变更后重新加载 + */ + public void refreshTools() { + synchronized (this) { + cachedCallbacks = null; + log.info("工具缓存已清除,将在下次调用时重新加载"); + } + } + + /** + * 获取所有已注册的工具名称 + */ + public List getRegisteredToolNames() { + return allTools.stream() + .map(McpTool::getToolName) + .collect(Collectors.toList()); + } + + /** + * 获取已启用的工具名称 + */ + public List getEnabledToolNames() { + return allTools.stream() + .filter(tool -> mcpToolsConfig.isToolEnabled(tool.getToolName())) + .map(McpTool::getToolName) + .collect(Collectors.toList()); + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/McpToolsConfig.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/McpToolsConfig.java new file mode 100644 index 00000000..98c5fa18 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/McpToolsConfig.java @@ -0,0 +1,75 @@ +package org.ruoyi.mcpserve.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * MCP工具动态配置类 + * 支持通过配置文件启用/禁用各个工具 + * + * @author OpenX + */ +@Data +@Component +@ConfigurationProperties(prefix = "mcp.tools") +public class McpToolsConfig { + + /** + * 工具启用配置 + * key: 工具名称 + * value: 是否启用 + */ + private Map enabled = new HashMap<>(); + + /** + * 检查工具是否启用 + * 默认情况下,如果未配置则启用 + * + * @param toolName 工具名称 + * @return 是否启用 + */ + public boolean isToolEnabled(String toolName) { + return enabled.getOrDefault(toolName, true); + } + + /** + * 动态启用工具 + * + * @param toolName 工具名称 + */ + public void enableTool(String toolName) { + enabled.put(toolName, true); + } + + /** + * 动态禁用工具 + * + * @param toolName 工具名称 + */ + public void disableTool(String toolName) { + enabled.put(toolName, false); + } + + /** + * 动态设置工具启用状态 + * + * @param toolName 工具名称 + * @param enable 是否启用 + */ + public void setToolEnabled(String toolName, boolean enable) { + enabled.put(toolName, enable); + } + + /** + * 批量设置工具启用状态 + * + * @param toolStates 工具状态映射 + */ + public void setToolsEnabled(Map toolStates) { + enabled.putAll(toolStates); + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java new file mode 100644 index 00000000..69f44657 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java @@ -0,0 +1,65 @@ +package org.ruoyi.mcpserve.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 工具配置属性类 + * + * @author OpenX + */ +@Data +@Component +@ConfigurationProperties(prefix = "tools") +public class ToolsProperties { + + /** + * Pexels图片搜索配置 + */ + private Pexels pexels = new Pexels(); + + /** + * Tavily搜索配置 + */ + private Tavily tavily = new Tavily(); + + /** + * 文件操作配置 + */ + private FileConfig file = new FileConfig(); + + @Data + public static class Pexels { + /** + * Pexels API密钥 + */ + private String apiKey; + + /** + * API地址 + */ + private String apiUrl; + } + + @Data + public static class Tavily { + /** + * Tavily API密钥 + */ + private String apiKey; + + /** + * API地址 + */ + private String baseUrl; + } + + @Data + public static class FileConfig { + /** + * 文件保存目录 + */ + private String saveDir; + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/controller/ToolsController.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/controller/ToolsController.java new file mode 100644 index 00000000..a09b3ac4 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/controller/ToolsController.java @@ -0,0 +1,128 @@ +package org.ruoyi.mcpserve.controller; +import org.ruoyi.mcpserve.config.DynamicToolCallbackProvider; +import org.ruoyi.mcpserve.config.McpToolsConfig; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * MCP工具测试Controller + * 用于查看已加载的工具信息 + * + * @author OpenX + */ +@RestController +@RequestMapping("/tools") +public class ToolsController { + + private final DynamicToolCallbackProvider toolCallbackProvider; + private final McpToolsConfig mcpToolsConfig; + + public ToolsController(DynamicToolCallbackProvider toolCallbackProvider, McpToolsConfig mcpToolsConfig) { + this.toolCallbackProvider = toolCallbackProvider; + this.mcpToolsConfig = mcpToolsConfig; + } + + /** + * 获取所有工具信息 + */ + @GetMapping + public Map getToolsInfo() { + Map result = new HashMap<>(); + + // 所有已注册的工具 + result.put("registered", toolCallbackProvider.getRegisteredToolNames()); + + // 已加载的工具回调详情 + List> callbacks = Stream.of(toolCallbackProvider.getToolCallbacks()) + .map(callback -> { + Map info = new HashMap<>(); + info.put("name", callback.getToolDefinition().name()); + info.put("description", callback.getToolDefinition().description()); + return info; + }) + .collect(Collectors.toList()); + result.put("callbacks", callbacks); + + return result; + } + + /** + * 刷新工具缓存 + */ + @PostMapping("/refresh") + public Map refreshTools() { + toolCallbackProvider.refreshTools(); + Map result = new HashMap<>(); + result.put("message", "工具缓存已刷新"); + return result; + } + + /** + * 启用指定工具 + */ + @PostMapping("/enable/{toolName}") + public Map enableTool(@PathVariable String toolName) { + mcpToolsConfig.enableTool(toolName); + toolCallbackProvider.refreshTools(); + + Map result = new HashMap<>(); + result.put("toolName", toolName); + result.put("enabled", true); + result.put("message", "工具已启用"); + return result; + } + + /** + * 禁用指定工具 + */ + @PostMapping("/disable/{toolName}") + public Map disableTool(@PathVariable String toolName) { + mcpToolsConfig.disableTool(toolName); + toolCallbackProvider.refreshTools(); + + Map result = new HashMap<>(); + result.put("toolName", toolName); + result.put("enabled", false); + result.put("message", "工具已禁用"); + return result; + } + + /** + * 批量设置工具状态 + * 请求体示例: {"basic": true, "terminal": false, "plantuml": true} + */ + @PostMapping("/batch") + public Map batchSetTools(@RequestBody Map toolStates) { + mcpToolsConfig.setToolsEnabled(toolStates); + toolCallbackProvider.refreshTools(); + + Map result = new HashMap<>(); + result.put("updated", toolStates); + result.put("enabled", toolCallbackProvider.getEnabledToolNames()); + result.put("message", "工具状态已更新"); + return result; + } + + /** + * 获取所有工具的启用状态 + */ + @GetMapping("/status") + public Map getToolsStatus() { + Map result = new HashMap<>(); + List registered = toolCallbackProvider.getRegisteredToolNames(); + + Map status = new HashMap<>(); + for (String toolName : registered) { + status.put(toolName, mcpToolsConfig.isToolEnabled(toolName)); + } + + result.put("status", status); + return result; + } +} \ No newline at end of file diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/service/ToolService.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/BasicTools.java similarity index 71% rename from ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/service/ToolService.java rename to ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/BasicTools.java index a12b1d20..95592fb8 100644 --- a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/service/ToolService.java +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/BasicTools.java @@ -1,28 +1,33 @@ -package org.ruoyi.mcpserve.service; +package org.ruoyi.mcpserve.tools; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; - /** - * @author ageer + * 基础工具类 + * + * @author OpenX */ -@Service -public class ToolService { +@Component +public class BasicTools implements McpTool { + + public static final String TOOL_NAME = "basic"; + + @Override + public String getToolName() { + return TOOL_NAME; + } @Tool(description = "获取一个指定前缀的随机数") public String add(@ToolParam(description = "字符前缀") String prefix) { - // 定义日期格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd"); - //根据当前时间获取yyMMdd格式的时间字符串 String format = LocalDate.now().format(formatter); - //生成随机数 String replace = prefix + UUID.randomUUID().toString().replace("-", ""); return format + replace; } diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/DocumentTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/DocumentTools.java new file mode 100644 index 00000000..a5fdc81e --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/DocumentTools.java @@ -0,0 +1,41 @@ +package org.ruoyi.mcpserve.tools; + +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.tika.TikaDocumentReader; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 文档解析工具类 + * + * @author OpenX + */ +@Component +public class DocumentTools implements McpTool { + + public static final String TOOL_NAME = "document"; + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "从URL解析文档内容,支持PDF、Word、Excel等格式") + public String parseDocumentFromUrl( + @ToolParam(description = "要解析的文档URL地址") String fileUrl) { + try { + TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(new UrlResource(fileUrl)); + List documents = tikaDocumentReader.read(); + if (documents.isEmpty()) { + return "No content found in the document."; + } + return documents.get(0).getText(); + } catch (Exception e) { + return "Error parsing document: " + e.getMessage(); + } + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/FileTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/FileTools.java new file mode 100644 index 00000000..495ebd13 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/FileTools.java @@ -0,0 +1,55 @@ +package org.ruoyi.mcpserve.tools; + +import cn.hutool.core.io.FileUtil; +import org.ruoyi.mcpserve.config.ToolsProperties; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +/** + * 文件操作工具类 + * + * @author OpenX + */ +@Component +public class FileTools implements McpTool { + + public static final String TOOL_NAME = "file"; + + private final ToolsProperties toolsProperties; + + public FileTools(ToolsProperties toolsProperties) { + this.toolsProperties = toolsProperties; + } + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "读取文件内容") + public String readFile(@ToolParam(description = "要读取的文件名") String fileName) { + String fileDir = toolsProperties.getFile().getSaveDir() + "/file"; + String filePath = fileDir + "/" + fileName; + try { + return FileUtil.readUtf8String(filePath); + } catch (Exception e) { + return "Error reading file: " + e.getMessage(); + } + } + + @Tool(description = "写入内容到文件") + public String writeFile( + @ToolParam(description = "要写入的文件名") String fileName, + @ToolParam(description = "要写入的内容") String content) { + String fileDir = toolsProperties.getFile().getSaveDir() + "/file"; + String filePath = fileDir + "/" + fileName; + try { + FileUtil.mkdir(fileDir); + FileUtil.writeUtf8String(content, filePath); + return "File written successfully to: " + filePath; + } catch (Exception e) { + return "Error writing to file: " + e.getMessage(); + } + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/ImageSearchTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/ImageSearchTools.java new file mode 100644 index 00000000..64aa3b98 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/ImageSearchTools.java @@ -0,0 +1,70 @@ +package org.ruoyi.mcpserve.tools; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import org.ruoyi.mcpserve.config.ToolsProperties; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 图片搜索工具类 + * + * @author OpenX + */ +@Component +public class ImageSearchTools implements McpTool { + + public static final String TOOL_NAME = "image-search"; + + private final ToolsProperties toolsProperties; + + public ImageSearchTools(ToolsProperties toolsProperties) { + this.toolsProperties = toolsProperties; + } + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "从Pexels搜索图片") + public String searchImage(@ToolParam(description = "图片搜索关键词") String query) { + try { + String apiKey = toolsProperties.getPexels().getApiKey(); + String apiUrl = toolsProperties.getPexels().getApiUrl(); + + Map headers = new HashMap<>(); + headers.put("Authorization", apiKey); + + Map params = new HashMap<>(); + params.put("query", query); + + String response = HttpUtil.createGet(apiUrl) + .addHeaders(headers) + .form(params) + .execute() + .body(); + + List images = JSONUtil.parseObj(response) + .getJSONArray("photos") + .stream() + .map(photoObj -> (JSONObject) photoObj) + .map(photoObj -> photoObj.getJSONObject("src")) + .map(photo -> photo.getStr("medium")) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + + return String.join(",", images); + } catch (Exception e) { + return "Error search image: " + e.getMessage(); + } + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/McpTool.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/McpTool.java new file mode 100644 index 00000000..c77237c0 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/McpTool.java @@ -0,0 +1,17 @@ +package org.ruoyi.mcpserve.tools; + +/** + * MCP工具标记接口 + * 所有MCP工具类都需要实现此接口,以便动态加载器识别 + * + * @author OpenX + */ +public interface McpTool { + + /** + * 获取工具名称,用于配置文件中的启用/禁用控制 + * + * @return 工具名称 + */ + String getToolName(); +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/PlantUmlTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/PlantUmlTools.java new file mode 100644 index 00000000..f91900fb --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/PlantUmlTools.java @@ -0,0 +1,87 @@ +package org.ruoyi.mcpserve.tools; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +/** + * PlantUML工具类 + * + * @author OpenX + */ +@Component +public class PlantUmlTools implements McpTool { + + public static final String TOOL_NAME = "plantuml"; + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "生成PlantUML图表并返回SVG代码") + public String generatePlantUmlSvg( + @ToolParam(description = "UML图表源代码") String umlCode) { + try { + if (umlCode == null || umlCode.trim().isEmpty()) { + return "Error: UML代码不能为空"; + } + + System.setProperty("PLANTUML_LIMIT_SIZE", "32768"); + System.setProperty("java.awt.headless", "true"); + + String normalizedUmlCode = normalizeUmlCode(umlCode); + + SourceStringReader reader = new SourceStringReader(normalizedUmlCode); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + reader.generateImage(outputStream, new FileFormatOption(FileFormat.SVG)); + + byte[] svgBytes = outputStream.toByteArray(); + if (svgBytes.length == 0) { + return "Error: 生成的SVG内容为空,请检查UML语法是否正确"; + } + + return new String(svgBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + return "Error generating PlantUML: " + e.getMessage(); + } + } + + private String normalizeUmlCode(String umlCode) { + umlCode = umlCode.trim(); + if (umlCode.contains("@startuml")) { + int startIndex = umlCode.indexOf("@startuml"); + int endIndex = umlCode.lastIndexOf("@enduml"); + if (endIndex > startIndex) { + String startPart = umlCode.substring(startIndex); + int firstNewLine = startPart.indexOf('\n'); + String content = firstNewLine > 0 ? startPart.substring(firstNewLine + 1) : ""; + if (content.contains("@enduml")) { + content = content.substring(0, content.lastIndexOf("@enduml")).trim(); + } + umlCode = content; + } + } + + StringBuilder normalizedCode = new StringBuilder(); + normalizedCode.append("@startuml\n"); + normalizedCode.append("!pragma layout smetana\n"); + normalizedCode.append("skinparam charset UTF-8\n"); + normalizedCode.append("skinparam defaultFontName SimHei\n"); + normalizedCode.append("skinparam defaultFontSize 12\n"); + normalizedCode.append("skinparam dpi 150\n"); + normalizedCode.append("\n"); + normalizedCode.append(umlCode); + if (!umlCode.endsWith("\n")) { + normalizedCode.append("\n"); + } + normalizedCode.append("@enduml"); + return normalizedCode.toString(); + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/TerminalTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/TerminalTools.java new file mode 100644 index 00000000..1947463b --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/TerminalTools.java @@ -0,0 +1,75 @@ +package org.ruoyi.mcpserve.tools; + +import org.ruoyi.mcpserve.config.ToolsProperties; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 终端命令工具类 + * + * @author OpenX + */ +@Component +public class TerminalTools implements McpTool { + + public static final String TOOL_NAME = "terminal"; + + private final ToolsProperties toolsProperties; + + public TerminalTools(ToolsProperties toolsProperties) { + this.toolsProperties = toolsProperties; + } + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "在终端中执行命令") + public String executeTerminalCommand( + @ToolParam(description = "要执行的终端命令") String command) { + StringBuilder output = new StringBuilder(); + try { + String projectRoot = System.getProperty("user.dir"); + String fileDir = toolsProperties.getFile().getSaveDir() + "/file"; + File workingDir = new File(projectRoot, fileDir); + + if (!workingDir.exists()) { + workingDir.mkdirs(); + } + + ProcessBuilder processBuilder; + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + processBuilder = new ProcessBuilder("cmd.exe", "/c", command); + } else { + processBuilder = new ProcessBuilder("/bin/sh", "-c", command); + } + processBuilder.directory(workingDir); + Process process = processBuilder.start(); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + output.append("Command execution failed with exit code: ").append(exitCode); + } + } catch (IOException | InterruptedException e) { + output.append("Error executing command: ").append(e.getMessage()); + } + return output.toString(); + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebPageTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebPageTools.java new file mode 100644 index 00000000..c1727327 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebPageTools.java @@ -0,0 +1,67 @@ +package org.ruoyi.mcpserve.tools; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 网页内容加载工具类 + * + * @author OpenX + */ +@Component +public class WebPageTools implements McpTool { + + public static final String TOOL_NAME = "web-page"; + + private final OkHttpClient httpClient; + + public WebPageTools() { + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + } + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "加载网页并提取文本内容") + public String loadWebPage(@ToolParam(description = "要加载的网页URL地址") String url) { + if (url == null || url.trim().isEmpty()) { + return "Error: URL is empty. Please provide a valid URL."; + } + + try { + Request request = new Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + return "Error: Failed to load web page, status: " + response.code(); + } + + String html = response.body().string(); + // 简单的HTML文本提取 + String text = html.replaceAll("]*>[\\s\\S]*?", "") + .replaceAll("]*>[\\s\\S]*?", "") + .replaceAll("<[^>]+>", " ") + .replaceAll("\\s+", " ") + .trim(); + + return text; + } + } catch (Exception e) { + return "Error loading web page: " + e.getMessage(); + } + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebSearchTools.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebSearchTools.java new file mode 100644 index 00000000..a3b81fb7 --- /dev/null +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/tools/WebSearchTools.java @@ -0,0 +1,88 @@ +package org.ruoyi.mcpserve.tools; + +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; +import org.ruoyi.mcpserve.config.ToolsProperties; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 网页搜索工具类 + * + * @author OpenX + */ +@Component +public class WebSearchTools implements McpTool { + + public static final String TOOL_NAME = "web-search"; + + private final ToolsProperties toolsProperties; + private final OkHttpClient httpClient; + private final ObjectMapper objectMapper; + + public WebSearchTools(ToolsProperties toolsProperties) { + this.toolsProperties = toolsProperties; + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.objectMapper = new ObjectMapper(); + } + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Tool(description = "从网络搜索引擎搜索信息") + public String webSearch( + @ToolParam(description = "搜索查询文本") String query, + @ToolParam(description = "最大返回结果数量") int maxResults) { + List> results = new ArrayList<>(); + try { + String apiKey = toolsProperties.getTavily().getApiKey(); + String baseUrl = toolsProperties.getTavily().getBaseUrl(); + + Map requestBody = new HashMap<>(); + requestBody.put("query", query); + requestBody.put("max_results", maxResults); + + Request request = new Request.Builder() + .url(baseUrl) + .post(RequestBody.create(MediaType.parse("application/json"), + objectMapper.writeValueAsString(requestBody))) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + return "搜索请求失败: " + response; + } + + JsonNode jsonNode = objectMapper.readTree(response.body().string()).get("results"); + if (jsonNode != null && !jsonNode.isEmpty()) { + jsonNode.forEach(data -> { + Map processedResult = new HashMap<>(); + processedResult.put("title", data.get("title").asText()); + processedResult.put("url", data.get("url").asText()); + processedResult.put("content", data.get("content").asText()); + results.add(processedResult); + }); + } + } + } catch (Exception e) { + return "搜索时发生错误: " + e.getMessage(); + } + return JSONUtil.toJsonStr(results); + } +} diff --git a/ruoyi-extend/ruoyi-mcp-server/src/main/resources/application.yml b/ruoyi-extend/ruoyi-mcp-server/src/main/resources/application.yml index f5ced666..4cbdae4a 100644 --- a/ruoyi-extend/ruoyi-mcp-server/src/main/resources/application.yml +++ b/ruoyi-extend/ruoyi-mcp-server/src/main/resources/application.yml @@ -7,4 +7,38 @@ spring: name: ruoyi-mcp-serve version: 1.0.0 +# 工具配置 +tools: + pexels: + api-key: your-pexels-api-key #key获取地址: https://www.pexels.com/zh-cn/api/key + api-url: https://api.pexels.com/v1/search + tavily: + api-key: your-tavily-api-key #key获取地址: https://app.tavily.com/home + base-url: https://api.tavily.com/search + file: + save-dir: ./tmp + +# MCP工具初始化配置 +mcp: + tools: + enabled: + # 基础工具(随机数、当前时间) + basic: true + # 文件操作工具(读写文件) + file: true + # 图片搜索工具(Pexels) + image-search: true + # PlantUML图表生成工具 + plantuml: true + # 网页搜索工具(Tavily) + web-search: true + # 终端命令执行工具 + terminal: true + # 文档解析工具 + document: true + # 网页内容加载工具 + web-page: true + + +