From 77aeabb4be3193d8671e17a3c09e4a1665e315d4 Mon Sep 17 00:00:00 2001 From: OpenX123 <2113239898goole@gmail.com> Date: Thu, 11 Dec 2025 20:41:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BAMCP=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E6=93=8D=E4=BD=9C=E3=80=81?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=90=9C=E7=B4=A2=E3=80=81PlantUML=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=80=81=E7=BD=91=E9=A1=B5=E6=90=9C=E7=B4=A2=E3=80=81?= =?UTF-8?q?=E7=BB=88=E7=AB=AF=E5=91=BD=E4=BB=A4=E3=80=81=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E7=AD=89=E5=AE=9E=E7=94=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 4 +- ruoyi-extend/ruoyi-mcp-server/pom.xml | 33 ++ .../mcpserve/config/ToolsProperties.java | 65 ++++ .../ruoyi/mcpserve/service/ToolService.java | 306 +++++++++++++++++- .../src/main/resources/application.yml | 11 + 5 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index e952157f..da406558 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -15,9 +15,9 @@ spring: master: type: ${spring.datasource.type} driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true username: root - password: root + password: 1234 hikari: # 最大连接池数量 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/config/ToolsProperties.java b/ruoyi-extend/ruoyi-mcp-server/src/main/java/org/ruoyi/mcpserve/config/ToolsProperties.java new file mode 100644 index 00000000..18cf16ff --- /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 = "https://api.pexels.com/v1/search"; + } + + @Data + public static class Tavily { + /** + * Tavily API密钥 + */ + private String apiKey; + + /** + * API地址 + */ + private String baseUrl = "https://api.tavily.com/search"; + } + + @Data + public static class FileConfig { + /** + * 文件保存目录 + */ + private String saveDir = "./tmp"; + } +} 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/service/ToolService.java index a12b1d20..3bf07a65 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/service/ToolService.java @@ -1,21 +1,57 @@ package org.ruoyi.mcpserve.service; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; +import okhttp3.*; +import org.ruoyi.mcpserve.config.ToolsProperties; +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.Service; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.UUID; - +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** - * @author ageer + * MCP工具服务类 + * 整合了文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等功能 + * + * @author ageer,OpenX */ @Service public class ToolService { + private final ToolsProperties toolsProperties; + private final OkHttpClient httpClient; + private final ObjectMapper objectMapper; + + public ToolService(ToolsProperties toolsProperties) { + this.toolsProperties = toolsProperties; + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.objectMapper = new ObjectMapper(); + } + + // ==================== 基础工具 ==================== + @Tool(description = "获取一个指定前缀的随机数") public String add(@ToolParam(description = "字符前缀") String prefix) { // 定义日期格式 @@ -31,4 +67,268 @@ public class ToolService { public LocalDateTime getCurrentTime() { return LocalDateTime.now(); } + + // ==================== 文件操作工具 ==================== + + @Tool(description = "Read content from a file") + public String readFile(@ToolParam(description = "Name of the file to read") 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 = "Write content to a file") + public String writeFile( + @ToolParam(description = "Name of the file to write") String fileName, + @ToolParam(description = "Content to write to the file") 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(); + } + } + + // ==================== 图片搜索工具 ==================== + + @Tool(description = "Search for images from Pexels") + public String searchImage(@ToolParam(description = "Image search keywords") 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(); + } + } + + // ==================== PlantUML工具 ==================== + + @Tool(description = "Generate a PlantUML diagram and return SVG code") + public String generatePlantUmlSvg( + @ToolParam(description = "UML diagram source code") 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(); + } + + // ==================== 网页搜索工具 ==================== + + @Tool(description = "Search for information from web search engines") + public String webSearch( + @ToolParam(description = "Search query text") String query, + @ToolParam(description = "Max results count") 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); + } + + // ==================== 终端命令工具 ==================== + + @Tool(description = "Execute a command in the terminal") + public String executeTerminalCommand( + @ToolParam(description = "Command to execute in the terminal") 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(); + } + + // ==================== 文档解析工具 ==================== + + @Tool(description = "Parse the content of a document from URL") + public String parseDocumentFromUrl( + @ToolParam(description = "URL of the document to parse") 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(); + } + } + + // ==================== 网页内容加载工具 ==================== + + @Tool(description = "Load and extract text content from a web page URL") + public String loadWebPage(@ToolParam(description = "The URL of the web page to load") 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/resources/application.yml b/ruoyi-extend/ruoyi-mcp-server/src/main/resources/application.yml index f5ced666..fd1e1f46 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,15 @@ 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 +