mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
Merge pull request #249 from OpenX123/feature/添加多个实用MCP工具
Feature/添加多个实用mcp工具
This commit is contained in:
@@ -47,6 +47,39 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package org.ruoyi.mcpserve;
|
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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* MCP Server 应用启动类
|
||||||
|
* 工具通过 DynamicToolCallbackProvider 动态加载
|
||||||
|
*
|
||||||
* @author ageer
|
* @author ageer
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@@ -17,9 +16,4 @@ public class RuoyiMcpServeApplication {
|
|||||||
SpringApplication.run(RuoyiMcpServeApplication.class, args);
|
SpringApplication.run(RuoyiMcpServeApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ToolCallbackProvider systemTools(ToolService toolService) {
|
|
||||||
return MethodToolCallbackProvider.builder().toolObjects(toolService).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<McpTool> allTools;
|
||||||
|
private volatile ToolCallback[] cachedCallbacks;
|
||||||
|
|
||||||
|
public DynamicToolCallbackProvider(McpToolsConfig mcpToolsConfig, List<McpTool> 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<Object> 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<String> getRegisteredToolNames() {
|
||||||
|
return allTools.stream()
|
||||||
|
.map(McpTool::getToolName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已启用的工具名称
|
||||||
|
*/
|
||||||
|
public List<String> getEnabledToolNames() {
|
||||||
|
return allTools.stream()
|
||||||
|
.filter(tool -> mcpToolsConfig.isToolEnabled(tool.getToolName()))
|
||||||
|
.map(McpTool::getToolName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Boolean> 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<String, Boolean> toolStates) {
|
||||||
|
enabled.putAll(toolStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> getToolsInfo() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
// 所有已注册的工具
|
||||||
|
result.put("registered", toolCallbackProvider.getRegisteredToolNames());
|
||||||
|
|
||||||
|
// 已加载的工具回调详情
|
||||||
|
List<Map<String, String>> callbacks = Stream.of(toolCallbackProvider.getToolCallbacks())
|
||||||
|
.map(callback -> {
|
||||||
|
Map<String, String> 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<String, String> refreshTools() {
|
||||||
|
toolCallbackProvider.refreshTools();
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("message", "工具缓存已刷新");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用指定工具
|
||||||
|
*/
|
||||||
|
@PostMapping("/enable/{toolName}")
|
||||||
|
public Map<String, Object> enableTool(@PathVariable String toolName) {
|
||||||
|
mcpToolsConfig.enableTool(toolName);
|
||||||
|
toolCallbackProvider.refreshTools();
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("toolName", toolName);
|
||||||
|
result.put("enabled", true);
|
||||||
|
result.put("message", "工具已启用");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用指定工具
|
||||||
|
*/
|
||||||
|
@PostMapping("/disable/{toolName}")
|
||||||
|
public Map<String, Object> disableTool(@PathVariable String toolName) {
|
||||||
|
mcpToolsConfig.disableTool(toolName);
|
||||||
|
toolCallbackProvider.refreshTools();
|
||||||
|
|
||||||
|
Map<String, Object> 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<String, Object> batchSetTools(@RequestBody Map<String, Boolean> toolStates) {
|
||||||
|
mcpToolsConfig.setToolsEnabled(toolStates);
|
||||||
|
toolCallbackProvider.refreshTools();
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("updated", toolStates);
|
||||||
|
result.put("enabled", toolCallbackProvider.getEnabledToolNames());
|
||||||
|
result.put("message", "工具状态已更新");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有工具的启用状态
|
||||||
|
*/
|
||||||
|
@GetMapping("/status")
|
||||||
|
public Map<String, Object> getToolsStatus() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<String> registered = toolCallbackProvider.getRegisteredToolNames();
|
||||||
|
|
||||||
|
Map<String, Boolean> status = new HashMap<>();
|
||||||
|
for (String toolName : registered) {
|
||||||
|
status.put(toolName, mcpToolsConfig.isToolEnabled(toolName));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("status", status);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Tool;
|
||||||
import org.springframework.ai.tool.annotation.ToolParam;
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author ageer
|
* 基础工具类
|
||||||
|
*
|
||||||
|
* @author OpenX
|
||||||
*/
|
*/
|
||||||
@Service
|
@Component
|
||||||
public class ToolService {
|
public class BasicTools implements McpTool {
|
||||||
|
|
||||||
|
public static final String TOOL_NAME = "basic";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToolName() {
|
||||||
|
return TOOL_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
@Tool(description = "获取一个指定前缀的随机数")
|
@Tool(description = "获取一个指定前缀的随机数")
|
||||||
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
||||||
// 定义日期格式
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
|
||||||
//根据当前时间获取yyMMdd格式的时间字符串
|
|
||||||
String format = LocalDate.now().format(formatter);
|
String format = LocalDate.now().format(formatter);
|
||||||
//生成随机数
|
|
||||||
String replace = prefix + UUID.randomUUID().toString().replace("-", "");
|
String replace = prefix + UUID.randomUUID().toString().replace("-", "");
|
||||||
return format + replace;
|
return format + replace;
|
||||||
}
|
}
|
||||||
@@ -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<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.ruoyi.mcpserve.tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP工具标记接口
|
||||||
|
* 所有MCP工具类都需要实现此接口,以便动态加载器识别
|
||||||
|
*
|
||||||
|
* @author OpenX
|
||||||
|
*/
|
||||||
|
public interface McpTool {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具名称,用于配置文件中的启用/禁用控制
|
||||||
|
*
|
||||||
|
* @return 工具名称
|
||||||
|
*/
|
||||||
|
String getToolName();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,38 @@ spring:
|
|||||||
name: ruoyi-mcp-serve
|
name: ruoyi-mcp-serve
|
||||||
version: 1.0.0
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user