为MCP服务器添加文件操作、图片搜索、PlantUML生成、网页搜索、终端命令、文档解析等实用功能

This commit is contained in:
OpenX123
2025-12-11 20:41:04 +08:00
parent f63ccbe7bd
commit 77aeabb4be
5 changed files with 414 additions and 5 deletions

View File

@@ -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:
# 最大连接池数量

View File

@@ -47,6 +47,39 @@
<scope>test</scope>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- OkHttp for HTTP requests -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- PlantUML -->
<dependency>
<groupId>net.sourceforge.plantuml</groupId>
<artifactId>plantuml</artifactId>
<version>1.2024.3</version>
</dependency>
<!-- Spring AI Tika for document parsing -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>

View File

@@ -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";
}
}

View File

@@ -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<String, String> headers = new HashMap<>();
headers.put("Authorization", apiKey);
Map<String, Object> params = new HashMap<>();
params.put("query", query);
String response = HttpUtil.createGet(apiUrl)
.addHeaders(headers)
.form(params)
.execute()
.body();
List<String> 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<Map<String, String>> results = new ArrayList<>();
try {
String apiKey = toolsProperties.getTavily().getApiKey();
String baseUrl = toolsProperties.getTavily().getBaseUrl();
Map<String, Object> 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<String, String> 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<Document> 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("<script[^>]*>[\\s\\S]*?</script>", "")
.replaceAll("<style[^>]*>[\\s\\S]*?</style>", "")
.replaceAll("<[^>]+>", " ")
.replaceAll("\\s+", " ")
.trim();
return text;
}
} catch (Exception e) {
return "Error loading web page: " + e.getMessage();
}
}
}

View File

@@ -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