From bc2eb8fdb9a6e3a7a8b6ad2bf4ac4e067802a797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=85=92=E4=BA=A6?= Date: Mon, 11 Aug 2025 21:22:12 +0800 Subject: [PATCH] =?UTF-8?q?=20feat:=E5=9F=BA=E4=BA=8Estdio=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20=E5=90=AF=E5=8A=A8mcp=E6=9C=8D=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/domain/McpInfo.java | 2 + .../java/org/ruoyi/domain/bo/McpInfoBo.java | 4 +- .../java/org/ruoyi/domain/vo/McpInfoVo.java | 5 +- .../java/org/ruoyi/mapper/McpInfoMapper.java | 19 +- .../ruoyi/mcp/config/ChatClientConfig.java | 20 + .../DynamicMcpToolCallbackProvider.java | 97 +++++ .../java/org/ruoyi/mcp/config/McpConfig.java | 20 + .../ruoyi/mcp/config/McpProcessManager.java | 341 ++++++++++++++++++ .../org/ruoyi/mcp/config/McpServerConfig.java | 61 ++++ .../ruoyi/mcp/config/McpStartupConfig.java | 27 ++ .../org/ruoyi/mcp/config/McpToolInvoker.java | 113 ++++++ .../mcp/controller/McpInfoController.java | 56 +++ .../org/ruoyi/mcp/domain/McpInfoRequest.java | 28 ++ .../org/ruoyi/mcp/service/McpInfoService.java | 20 + .../mcp/service/McpToolManagementService.java | 134 +++++++ .../mcp/service/impl/McpInfoServiceImpl.java | 148 +++++++- script/sql/update/mcp_info_menu.sql | 3 + 17 files changed, 1088 insertions(+), 10 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java index 11b3a096..f8392ad6 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java @@ -49,6 +49,8 @@ public class McpInfo extends BaseEntity { */ private String arguments; + private String description; + /** * Env */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java index 9aba1209..2228fb99 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java @@ -44,8 +44,8 @@ public class McpInfoBo implements Serializable { * Args */ private String arguments; - - /** + private String description; + /** * Env */ private String env; diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java index 610b0a8e..b5cc27c1 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java @@ -14,7 +14,7 @@ import java.io.Serializable; /** * MCP视图对象 mcp_info * - * @author ageerle + * @author jiyi * @date Sat Aug 09 16:50:58 CST 2025 */ @Data @@ -47,7 +47,8 @@ public class McpInfoVo implements Serializable { */ @ExcelProperty(value = "Args") private String arguments; - + @ExcelProperty(value = "Description") + private String description; /** * Env */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java index 0ff4ad69..fb26aaac 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java @@ -1,18 +1,33 @@ package org.ruoyi.mapper; +import org.apache.ibatis.annotations.*; import org.ruoyi.core.mapper.BaseMapperPlus; -import org.apache.ibatis.annotations.Mapper; import org.ruoyi.domain.McpInfo; import org.ruoyi.domain.vo.McpInfoVo; +import java.util.List; + /** * MCPMapper接口 * - * @author ageerle + * @author jiuyi * @date Sat Aug 09 16:50:58 CST 2025 */ @Mapper public interface McpInfoMapper extends BaseMapperPlus { + @Select("SELECT * FROM mcp_info WHERE server_name = #{serverName}") + McpInfo selectByServerName(@Param("serverName") String serverName); + @Select("SELECT * FROM mcp_info WHERE status = 1") + List selectActiveServers(); + + @Select("SELECT server_name FROM mcp_info WHERE status = 1") + List selectActiveServerNames(); + + @Update("UPDATE mcp_info SET status = #{status} WHERE server_name = #{serverName}") + int updateActiveStatus(@Param("serverName") String serverName, @Param("status") Boolean status); + + @Delete("DELETE FROM mcp_info WHERE server_name = #{serverName}") + int deleteByServerName(@Param("serverName") String serverName); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java new file mode 100644 index 00000000..3138f381 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java @@ -0,0 +1,20 @@ +package org.ruoyi.mcp.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ChatClientConfig { + + @Autowired + private DynamicMcpToolCallbackProvider dynamicMcpToolCallbackProvider; + + @Bean + public ChatClient chatClient(ChatClient.Builder builder) { + return builder + .defaultTools(java.util.List.of(dynamicMcpToolCallbackProvider.createToolCallbackProvider().getToolCallbacks())) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java new file mode 100644 index 00000000..32cb1c49 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java @@ -0,0 +1,97 @@ +package org.ruoyi.mcp.config; + +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.definition.ToolDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.*; +/** + * 动态MCP工具回调提供者 + * + * 这个类有大问题 ,没有测试!!!!!!! + */ +@Component +public class DynamicMcpToolCallbackProvider { + + @Autowired + private McpInfoService mcpInfoService; + + @Autowired + private McpProcessManager mcpProcessManager; + + @Autowired + private McpToolInvoker mcpToolInvoker; + + /** + * 创建工具回调提供者 + */ + public ToolCallbackProvider createToolCallbackProvider() { + List callbacks = new ArrayList<>(); + List activeServerNames = mcpInfoService.getActiveServerNames(); + + for (String serverName : activeServerNames) { + FunctionCallback callback = createMcpToolCallback(serverName); + callbacks.add(callback); + } + + return ToolCallbackProvider.from(callbacks); + } + + private FunctionCallback createMcpToolCallback(String serverName) { + return new ToolCallback() { + @Override + public ToolDefinition getToolDefinition() { + // 获取工具配置 + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + // 返回一个默认的ToolDefinition + return ToolDefinition.builder() + .name(serverName) + .description("MCP tool for " + serverName) + .build(); + } + // 根据config创建ToolDefinition + return ToolDefinition.builder() + .name(serverName) + .description(config.getDescription()) // 假设McpServerConfig有getDescription方法 + .build(); + } + + @Override + public String call(String toolInput) { + try { + // 获取工具配置 + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + return "{\"error\": \"MCP tool not found: " + serverName + "\", \"serverName\": \"" + serverName + "\"}"; + } + + // 确保 MCP 服务器正在运行 + ensureMcpServerRunning(serverName, config); + + // 调用 MCP 工具 + Object result = mcpToolInvoker.invokeTool(serverName, toolInput); + + return "{\"result\": \"" + result.toString() + "\", \"serverName\": \"" + serverName + "\"}"; + } catch (Exception e) { + return "{\"error\": \"MCP tool execution failed: " + e.getMessage() + "\", \"serverName\": \"" + serverName + "\"}"; + } + } + }; + } + + private void ensureMcpServerRunning(String serverName, McpServerConfig config) { + if (!mcpProcessManager.isMcpServerRunning(serverName)) { + boolean started = mcpProcessManager.startMcpServer( + serverName, + config + ); + if (!started) { + throw new RuntimeException("Failed to start MCP server: " + serverName); + } + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java new file mode 100644 index 00000000..7f186388 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java @@ -0,0 +1,20 @@ +package org.ruoyi.mcp.config; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import java.util.List; + +public class McpConfig { + @JsonProperty("mcpServers") + private Map mcpServers; + + // getters and setters + public Map getMcpServers() { + return mcpServers; + } + + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java new file mode 100644 index 00000000..3c6ebee8 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java @@ -0,0 +1,341 @@ +package org.ruoyi.mcp.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.io.*; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.*; +@Slf4j +@Component +public class McpProcessManager { + + private final Map runningProcesses = new ConcurrentHashMap<>(); + private final Map mcpServerProcesses = new ConcurrentHashMap<>(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final Map processWriters = new ConcurrentHashMap<>(); + private final Map processReaders = new ConcurrentHashMap<>(); + + @Autowired + private McpInfoService mcpInfoService; + /** + * 启动 MCP 服务器进程(支持环境变量) + */ + public boolean startMcpServer(String serverName, McpServerConfig serverConfig) { + try { + log.info("启动MCP服务器进程: {}", serverName); + + ProcessBuilder processBuilder = new ProcessBuilder(); + + // 构建命令 + List commandList = buildCommandListWithFullPaths(serverConfig.getCommand(), serverConfig.getArgs()); + + + processBuilder.command(commandList); + + // 设置工作目录 + if (serverConfig.getWorkingDirectory() != null) { + processBuilder.directory(new File(serverConfig.getWorkingDirectory())); + } else { + processBuilder.directory(new File(System.getProperty("user.dir"))); + } + + // 设置环境变量 + if (serverConfig.getEnv() != null) { + processBuilder.environment().putAll(serverConfig.getEnv()); + } + // ===== 关键:在 start 之前打印完整的调试信息 ===== + System.out.println("=== ProcessBuilder 调试信息 ==="); + System.out.println("完整命令列表: " + commandList); + System.out.println("命令字符串: " + String.join(" ", commandList)); + System.out.println("工作目录: " + processBuilder.directory()); + System.out.println("================================"); + //https://www.modelscope.cn/mcp/servers/@worryzyy/howtocook-mcp + + // 启动进程 + Process process = processBuilder.start(); + // 获取输入输出流 + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + processWriters.put(serverName, writer); + processReaders.put(serverName, reader); + + // 存储进程引用 + McpServerProcess serverProcess = new McpServerProcess(serverName, process, serverConfig); + mcpServerProcesses.put(serverName, serverProcess); + // 启动日志读取线程 + executorService.submit(() -> readProcessOutput(serverName, process)); + // 启动 MCP 通信监听线程 + executorService.submit(() -> listenMcpMessages(serverName, reader)); + + // 更新服务器状态 + mcpInfoService.enableTool(serverName); + boolean isAlive = process.isAlive(); + + if (isAlive) { + log.info("成功启动MCP服务器: {} 命令: {}", serverName, commandList); + } else { + System.err.println("✗ MCP server [" + serverName + "] failed to start"); + // 读取错误输出 + readErrorOutput(process); + } + return true; + + } catch (IOException e) { + log.error("启动MCP服务器进程失败: " + serverName, e); + + // 更新服务器状态为禁用 + //mcpInfoService.disableTool(serverName); + + throw new RuntimeException("Failed to start MCP server process: " + e.getMessage(), e); + + } + + } + /** + * 发送 MCP 消息 + */ + public boolean sendMcpMessage(String serverName, Map message) { + try { + BufferedWriter writer = processWriters.get(serverName); + if (writer == null) { + System.err.println("未找到服务器 [" + serverName + "] 的输出流"); + return false; + } + + String jsonMessage = objectMapper.writeValueAsString(message); + System.out.println("发送消息到 [" + serverName + "]: " + jsonMessage); + + writer.write(jsonMessage); + writer.newLine(); + writer.flush(); + + return true; + } catch (Exception e) { + System.err.println("发送消息到 [" + serverName + "] 失败: " + e.getMessage()); + return false; + } + } + + /** + * 监听 MCP 消息 + */ + private void listenMcpMessages(String serverName, BufferedReader reader) { + try { + String line; + while ((line = reader.readLine()) != null) { + try { + // 解析收到的 JSON 消息 + Map message = objectMapper.readValue(line, Map.class); + System.out.println("收到来自 [" + serverName + "] 的消息: " + message); + + // 处理不同类型的 MCP 消息 + handleMessage(serverName, message); + + } catch (Exception e) { + System.err.println("解析消息失败: " + line + ", 错误: " + e.getMessage()); + // 如果不是 JSON,当作普通日志输出 + System.out.println("[" + serverName + "] 日志: " + line); + } + } + } catch (IOException e) { + if (isMcpServerRunning(serverName)) { + System.err.println("监听 [" + serverName + "] 消息时出错: " + e.getMessage()); + } + } + } + + + /** + * 处理 MCP 消息(更新版本) + */ + private void handleMessage(String serverName, Map message) { + String type = (String) message.get("type"); + if (type == null) return; + + switch (type) { + case "ready": + System.out.println("MCP 服务器 [" + serverName + "] 准备就绪"); + break; + case "response": + System.out.println("MCP 服务器 [" + serverName + "] 响应: " + message.get("data")); + break; + case "error": + System.err.println("MCP 服务器 [" + serverName + "] 错误: " + message.get("message")); + break; + default: + System.out.println("MCP 服务器 [" + serverName + "] 未知消息类型: " + type); + break; + } + } + + /** + * 构建命令列表 + */ + private List buildCommandListWithFullPaths(String command, List args) { + List commandList = new ArrayList<>(); + + if (isWindows() && "npx".equalsIgnoreCase(command)) { + // 在 Windows 上使用 cmd.exe 包装以确保兼容性 + commandList.add("cmd.exe"); + commandList.add("/c"); + commandList.add("npx"); + commandList.addAll(args); + } else { + commandList.add(command); + commandList.addAll(args); + } + + return commandList; + } + /** + * 检查是否为 Windows 系统 + */ + private boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("windows"); + } + + + /** + * 读取错误输出 + */ + private void readErrorOutput(Process process) { + try { + InputStream errorStream = process.getErrorStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream)); + String line; + while ((line = reader.readLine()) != null) { + System.err.println("ERROR: " + line); + } + } catch (Exception e) { + System.err.println("Failed to read error output: " + e.getMessage()); + } + } + /** + * 停止 MCP 服务器进程 + */ + public boolean stopMcpServer(String serverName) { + Process process = runningProcesses.remove(serverName); + BufferedWriter writer = processWriters.remove(serverName); + BufferedReader reader = processReaders.remove(serverName); + try { + if (writer != null) { + writer.close(); + } + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + System.err.println("关闭流时出错: " + e.getMessage()); + } + // 更新服务器状态为禁用 + mcpInfoService.disableTool(serverName); + + if (process != null && process.isAlive()) { + process.destroy(); + try { + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly(); + process.waitFor(1, TimeUnit.SECONDS); + } + System.out.println("MCP server [" + serverName + "] stopped"); + return true; + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + /** + * 重启 MCP 服务器进程 + */ + public boolean restartMcpServer(String serverName, String command, List args, Map env) { + stopMcpServer(serverName); + McpServerConfig mcpServerConfig = new McpServerConfig(); + mcpServerConfig.setCommand(command); + mcpServerConfig.setArgs(args); + mcpServerConfig.setEnv(env); + return startMcpServer(serverName, mcpServerConfig); + } + + /** + * 检查 MCP 服务器是否运行 + */ + public boolean isMcpServerRunning(String serverName) { + Process process = runningProcesses.get(serverName); + return process != null && process.isAlive(); + } + + /** + * 获取所有运行中的 MCP 服务器 + */ + public Set getRunningMcpServers() { + Set running = new HashSet<>(); + for (Map.Entry entry : runningProcesses.entrySet()) { + if (entry.getValue().isAlive()) { + running.add(entry.getKey()); + } + } + return running; + } + + /** + * 获取进程信息 + */ + public McpServerProcess getProcessInfo(String serverName) { + return mcpServerProcesses.get(serverName); + } + + private void readProcessOutput(String serverName, Process process) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null && process.isAlive()) { + System.out.println("[" + serverName + "] " + line); + } + } catch (IOException e) { + System.err.println("Error reading output from " + serverName + ": " + e.getMessage()); + } + } + + private String getProcessId(Process process) { + try { + // Java 9+ 可以直接获取 PID + return String.valueOf(process.pid()); + } catch (Exception e) { + // Java 8 兼容处理 + return "unknown"; + } + } + + /** + * MCP服务器进程信息 + */ + public static class McpServerProcess { + private final String name; + private final Process process; + private final McpServerConfig config; + private final LocalDateTime startTime; + + public McpServerProcess(String name, Process process, McpServerConfig config) { + this.name = name; + this.process = process; + this.config = config; + this.startTime = LocalDateTime.now(); + } + + // Getters + public String getName() { return name; } + public Process getProcess() { return process; } + public McpServerConfig getConfig() { return config; } + public LocalDateTime getStartTime() { return startTime; } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java new file mode 100644 index 00000000..123d3181 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java @@ -0,0 +1,61 @@ +package org.ruoyi.mcp.config; + +import java.util.List; +import java.util.Map; + +public class McpServerConfig { + private String command; + private List args; + private Map env; + private String Description; + private String workingDirectory; + // getters and setters + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + + public Map getEnv() { + return env; + } + + public void setEnv(Map env) { + this.env = env; + } + + public String getDescription() { + return Description; + } + + public void setDescription(String description) { + Description = description; + } + public String getWorkingDirectory() { + return workingDirectory; + } + + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + @Override + public String toString() { + return "McpServerConfig{" + + "command='" + command + '\'' + + ", args=" + args + + ", env=" + env + + ", Description='" + Description + '\'' + + ", workingDirectory='" + workingDirectory + '\'' + + '}'; + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java new file mode 100644 index 00000000..66e6f468 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java @@ -0,0 +1,27 @@ +package org.ruoyi.mcp.config; + +import org.ruoyi.mcp.service.McpToolManagementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class McpStartupConfig { + + @Autowired + private McpToolManagementService mcpToolManagementService; + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + // 应用启动时自动初始化 MCP 工具 + try { + System.out.println("Starting MCP tools initialization..."); + mcpToolManagementService.initializeMcpTools(); + System.out.println("MCP tools initialization completed successfully"); + } catch (Exception e) { + System.err.println("Failed to initialize MCP tools: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java new file mode 100644 index 00000000..ee75bc88 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java @@ -0,0 +1,113 @@ +package org.ruoyi.mcp.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class McpToolInvoker { + + private final Map> pendingRequests = new ConcurrentHashMap<>(); + private final AtomicLong requestIdCounter = new AtomicLong(0); + @Autowired + private McpProcessManager mcpProcessManager; + + + /** + * 调用 MCP 工具(Studio 模式) + */ + public Object invokeTool(String serverName, Object parameters) { + try { + // 生成请求ID + String requestId = "req_" + requestIdCounter.incrementAndGet(); + + // 创建 CompletableFuture 等待响应 + CompletableFuture future = new CompletableFuture<>(); + pendingRequests.put(requestId, future); + + // 构造 MCP 调用消息 + Map callMessage = new HashMap<>(); + callMessage.put("type", "tool_call"); + callMessage.put("requestId", requestId); + callMessage.put("serverName", serverName); + callMessage.put("parameters", convertToMap(parameters)); + callMessage.put("timestamp", System.currentTimeMillis()); + + System.out.println("调用 MCP 工具 [" + serverName + "] 参数: " + parameters); + + // 发送消息到 MCP 服务器 + boolean sent = mcpProcessManager.sendMcpMessage(serverName, callMessage); + if (!sent) { + pendingRequests.remove(requestId); + throw new RuntimeException("无法发送消息到 MCP 服务器: " + serverName); + } + + // 等待响应(超时 30 秒) + Object result = future.get(30, TimeUnit.SECONDS); + + System.out.println("MCP 工具 [" + serverName + "] 调用成功,响应: " + result); + + return result; + + } catch (Exception e) { + System.err.println("调用 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage()); + e.printStackTrace(); + + return Map.of( + "serverName", serverName, + "status", "failed", + "message", "Tool invocation failed: " + e.getMessage(), + "parameters", parameters + ); + } + } + /** + * 处理 MCP 服务器的响应消息 + */ + public void handleMcpResponse(String serverName, Map message) { + String type = (String) message.get("type"); + if ("tool_response".equals(type)) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + Object data = message.get("data"); + future.complete(data != null ? data : message); + } + } + } else if ("tool_error".equals(type)) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + String errorMessage = (String) message.get("message"); + future.completeExceptionally(new RuntimeException(errorMessage)); + } + } + } + } + + + @SuppressWarnings("unchecked") + private Map convertToMap(Object parameters) { + if (parameters instanceof Map) { + Map result = new HashMap<>(); + Map paramMap = (Map) parameters; + for (Map.Entry entry : paramMap.entrySet()) { + if (entry.getKey() instanceof String) { + result.put((String) entry.getKey(), entry.getValue()); + } + } + return result; + } + return new HashMap<>(); + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java index 524c6f51..62978741 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java @@ -1,13 +1,18 @@ package org.ruoyi.mcp.controller; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import org.ruoyi.domain.McpInfo; import org.ruoyi.domain.bo.McpInfoBo; import org.ruoyi.domain.vo.McpInfoVo; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; import org.ruoyi.mcp.service.McpInfoService; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; @@ -103,4 +108,55 @@ public class McpInfoController extends BaseController { @PathVariable Integer[] mcpIds) { return toAjax(mcpInfoService.deleteWithValidByIds(List.of(mcpIds), true)); } + + /** + * 添加或更新 MCP 工具 + */ + @PostMapping("/tools") + public R saveToolConfig(@RequestBody McpInfoRequest request) { + return R.ok(mcpInfoService.saveToolConfig(request)); + } + + /** + * 获取所有活跃服务器名称 + */ + @GetMapping("/tools/names") + public R> getActiveServerNames() { + return R.ok(mcpInfoService.getActiveServerNames()); + } + + /** + * 根据名称获取工具配置 + */ + @GetMapping("/tools/{serverName}") + public R getToolConfig(@PathVariable String serverName) { + return R.ok(mcpInfoService.getToolConfigByName(serverName)); + } + + /** + * 启用工具 + */ + @PostMapping("/tools/{serverName}/enable") + public Map enableTool(@PathVariable String serverName) { + boolean success = mcpInfoService.enableTool(serverName); + return Map.of("success", success); + } + + /** + * 禁用工具 + */ + @PostMapping("/tools/{serverName}/disable") + public Map disableTool(@PathVariable String serverName) { + boolean success = mcpInfoService.disableTool(serverName); + return Map.of("success", success); + } + + /** + * 删除工具 + */ + @DeleteMapping("/tools/{serverName}") + public Map deleteTool(@PathVariable String serverName) { + boolean success = mcpInfoService.deleteToolConfig(serverName); + return Map.of("success", success); + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java new file mode 100644 index 00000000..d5525ed6 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java @@ -0,0 +1,28 @@ +package org.ruoyi.mcp.domain; + +import java.util.List; +import java.util.Map; + +public class McpInfoRequest { + private String serverName; + private String command; + private List args; + private Map env; + private String description; + + // getters and setters + public String getServerName() { return serverName; } + public void setServerName(String serverName) { this.serverName = serverName; } + + public String getCommand() { return command; } + public void setCommand(String command) { this.command = command; } + + public List getArgs() { return args; } + public void setArgs(List args) { this.args = args; } + + public Map getEnv() { return env; } + public void setEnv(Map env) { this.env = env; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java index b2c2ad0d..aa926d79 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java @@ -2,8 +2,12 @@ package org.ruoyi.mcp.service; import org.ruoyi.core.page.TableDataInfo; import org.ruoyi.core.page.PageQuery; + import org.ruoyi.domain.McpInfo; import org.ruoyi.domain.bo.McpInfoBo; import org.ruoyi.domain.vo.McpInfoVo; + import org.ruoyi.mcp.config.McpConfig; + import org.ruoyi.mcp.config.McpServerConfig; + import org.ruoyi.mcp.domain.McpInfoRequest; import java.util.Collection; import java.util.List; @@ -45,4 +49,20 @@ public interface McpInfoService { * 校验并批量删除MCP信息 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + McpServerConfig getToolConfigByName(String serverName); + + McpConfig getAllActiveMcpConfig(); + + List getActiveServerNames(); + + McpInfo saveToolConfig(McpInfoRequest request); + + boolean deleteToolConfig(String serverName); + + boolean updateToolStatus(String serverName, Boolean status); + + boolean enableTool(String serverName); + + boolean disableTool(String serverName); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java new file mode 100644 index 00000000..4b7ff701 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java @@ -0,0 +1,134 @@ +package org.ruoyi.mcp.service; + +import org.ruoyi.domain.McpInfo; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpProcessManager; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; + +@Service +public class McpToolManagementService { + + @Autowired + private McpInfoService mcpInfoService; + + @Autowired + private McpProcessManager mcpProcessManager; + + /** + * 初始化所有 MCP 工具(应用启动时调用) + */ + public void initializeMcpTools() { + System.out.println("Initializing MCP tools..."); + + McpConfig config = mcpInfoService.getAllActiveMcpConfig(); + if (config.getMcpServers() != null) { + int successCount = 0; + int totalCount = config.getMcpServers().size(); + + for (Map.Entry entry : config.getMcpServers().entrySet()) { + String serverName = entry.getKey(); + McpServerConfig serverConfig = entry.getValue(); + + System.out.println("Starting MCP server: " + serverName); + System.out.println("Starting MCP serverConfig: " + serverConfig); + // 启动 MCP 服务器进程 + boolean started = mcpProcessManager.startMcpServer(serverName,serverConfig); + + if (started) { + successCount++; + System.out.println("✓ MCP server [" + serverName + "] started successfully"); + } else { + System.err.println("✗ Failed to start MCP server [" + serverName + "]"); + } + } + + System.out.println("MCP tools initialization completed. " + + successCount + "/" + totalCount + " tools started."); + } + } + + /** + * 添加新的 MCP 工具并启动 + */ + public boolean addMcpTool(McpInfoRequest request) { + try { + McpInfo tool = mcpInfoService.saveToolConfig(request); + + // 启动新添加的工具 + McpServerConfig config = new McpServerConfig(); + config.setCommand(request.getCommand()); + config.setArgs(request.getArgs()); + config.setEnv(request.getEnv()); + + boolean started = mcpProcessManager.startMcpServer( + request.getServerName(), + config + ); + + return started; + } catch (Exception e) { + System.err.println("Failed to add MCP tool: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * 获取 MCP 工具状态 + */ + public Map getMcpToolStatus() { + List activeTools = mcpInfoService.getActiveServerNames(); + Map status = new HashMap<>(); + + for (String serverName : activeTools) { + boolean isRunning = mcpProcessManager.isMcpServerRunning(serverName); + McpProcessManager.McpServerProcess processInfo = mcpProcessManager.getProcessInfo(serverName); + + Map toolStatus = new HashMap<>(); + toolStatus.put("running", isRunning); + toolStatus.put("processInfo", processInfo); + + status.put(serverName, toolStatus); + } + + return status; + } + + /** + * 重启指定的 MCP 工具 + */ + public boolean restartMcpTool(String serverName) { + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + return false; + } + + return mcpProcessManager.restartMcpServer( + serverName, + config.getCommand(), + config.getArgs(), + config.getEnv() + ); + } + + /** + * 停止指定的 MCP 工具 + */ + public boolean stopMcpTool(String serverName) { + return mcpProcessManager.stopMcpServer(serverName); + } + + /** + * 获取所有运行中的工具 + */ + public Set getRunningTools() { + return mcpProcessManager.getRunningMcpServers(); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java index 627b83ff..95704717 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java @@ -1,5 +1,7 @@ package org.ruoyi.mcp.service.impl; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.core.page.TableDataInfo; import org.ruoyi.core.page.PageQuery; @@ -11,14 +13,15 @@ import org.ruoyi.domain.McpInfo; import org.ruoyi.domain.bo.McpInfoBo; import org.ruoyi.domain.vo.McpInfoVo; import org.ruoyi.mapper.McpInfoMapper; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; import org.ruoyi.mcp.service.McpInfoService; import org.springframework.stereotype.Service; import org.ruoyi.common.core.utils.StringUtils; -import java.util.List; -import java.util.Map; -import java.util.Collection; +import java.util.*; /** * MCPService业务层处理 @@ -31,7 +34,7 @@ import java.util.Collection; public class McpInfoServiceImpl implements McpInfoService { private final McpInfoMapper baseMapper; - + private final ObjectMapper objectMapper = new ObjectMapper(); /** * 查询MCP */ @@ -109,4 +112,141 @@ public class McpInfoServiceImpl implements McpInfoService { } return baseMapper.deleteBatchIds(ids) > 0; } + + /** + * 根据服务器名称获取工具配置 + */ + @Override + public McpServerConfig getToolConfigByName(String serverName) { + McpInfo tool = baseMapper.selectByServerName(serverName); + if (tool != null) { + return convertToMcpServerConfig(tool); + } + return null; + } + + /** + * 获取所有活跃的 MCP 工具配置 + */ + @Override + public McpConfig getAllActiveMcpConfig() { + List activeTools = baseMapper.selectActiveServers(); + Map servers = new HashMap<>(); + + for (McpInfo tool : activeTools) { + McpServerConfig serverConfig = convertToMcpServerConfig(tool); + servers.put(tool.getServerName(), serverConfig); + } + + McpConfig config = new McpConfig(); + config.setMcpServers(servers); + return config; + } + + /** + * 获取所有活跃服务器名称 + */ + @Override + public List getActiveServerNames() { + return baseMapper.selectActiveServerNames(); + } + + /** + * 保存或更新 MCP 工具配置 + */ + @Override + public McpInfo saveToolConfig(McpInfoRequest request) { + McpInfo existingTool = baseMapper.selectByServerName(request.getServerName()); + + McpInfo tool; + if (existingTool != null) { + tool = existingTool; + } else { + tool = new McpInfo(); + } + + tool.setServerName(request.getServerName()); + tool.setCommand(request.getCommand()); + + try { + tool.setArguments(objectMapper.writeValueAsString(request.getArgs())); + if (request.getEnv() != null) { + tool.setEnv(objectMapper.writeValueAsString(request.getEnv())); + } + } catch (Exception e) { + throw new RuntimeException("Failed to serialize JSON data", e); + } + + tool.setDescription(request.getDescription()); + tool.setStatus(true); // 默认启用 + + if (existingTool != null) { + baseMapper.updateById(tool); + } else { + baseMapper.insert(tool); + } + + return tool; + } + + /** + * 删除工具配置 + */ + @Override + public boolean deleteToolConfig(String serverName) { + return baseMapper.deleteByServerName(serverName) > 0; + } + + /** + * 更新工具状态 + */ + @Override + public boolean updateToolStatus(String serverName, Boolean status) { + return baseMapper.updateActiveStatus(serverName, status) > 0; + } + + /** + * 启用工具 + */ + @Override + public boolean enableTool(String serverName) { + return updateToolStatus(serverName, true); + } + + /** + * 禁用工具 + */ + @Override + public boolean disableTool(String serverName) { + return updateToolStatus(serverName, false); + } + + private McpServerConfig convertToMcpServerConfig(McpInfo tool) { + McpServerConfig config = new McpServerConfig(); + config.setCommand(tool.getCommand()); + + try { + // 解析 args + if (tool.getArguments() != null && !tool.getArguments().isEmpty()) { + List args = objectMapper.readValue(tool.getArguments(), new TypeReference>() {}); + config.setArgs(args); + } else { + config.setArgs(new ArrayList<>()); + } + + // 解析 env + if (tool.getEnv() != null && !tool.getEnv().isEmpty()) { + Map env = objectMapper.readValue(tool.getEnv(), new TypeReference>() {}); + config.setEnv(env); + } else { + config.setEnv(new HashMap<>()); + } + + } catch (Exception e) { + config.setArgs(new ArrayList<>()); + config.setEnv(new HashMap<>()); + } + + return config; + } } diff --git a/script/sql/update/mcp_info_menu.sql b/script/sql/update/mcp_info_menu.sql index b26de439..3117429c 100644 --- a/script/sql/update/mcp_info_menu.sql +++ b/script/sql/update/mcp_info_menu.sql @@ -41,3 +41,6 @@ INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, ` INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098960432443394, '000000', 1, 'SSE', 'SSE', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:34:32', NULL, '2025-08-09 16:34:32', NULL); INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954099421436784642, '000000', 2, 'HTTP', 'HTTP', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:36:22', NULL, '2025-08-09 16:36:22', NULL); INSERT INTO `ruoyi-ai`.`sys_dict_type` (`dict_id`, `tenant_id`, `dict_name`, `dict_type`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098639622713345, '000000', 'mcp链接方式', 'mcp_transport_type', '0', NULL, NULL, '2025-08-09 16:33:16', NULL, '2025-08-09 16:33:16', NULL); + + +INSERT INTO `ruoyi-ai`.`mcp_info` (`mcp_id`, `server_name`, `transport_type`, `command`, `arguments`, `env`, `status`, `description`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, 'howtocook-mcp', 'STDIO', 'npx', '["-y", "howtocook-mcp"]', NULL, 1, NULL, NULL, NULL, '2025-08-11 17:19:25', 1, '2025-08-11 18:24:22', NULL);