mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-15 21:03:40 +00:00
Merge pull request #165 from Code-Mr-Jiu/main
MCP相关功能----进程管理及mcp_info CRUD
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
package org.ruoyi.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.ruoyi.annotation.DataColumn;
|
||||||
|
import org.ruoyi.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP对象 mcp_info
|
||||||
|
*
|
||||||
|
* @author ageerle
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("mcp_info")
|
||||||
|
public class McpInfo extends BaseEntity {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
@TableId(value = "mcp_id", type = IdType.AUTO)
|
||||||
|
private Integer mcpId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器名称
|
||||||
|
*/
|
||||||
|
private String serverName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接方式
|
||||||
|
*/
|
||||||
|
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command
|
||||||
|
*/
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Args
|
||||||
|
*/
|
||||||
|
private String arguments;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Env
|
||||||
|
*/
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private Boolean status;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.ruoyi.domain.bo;
|
||||||
|
|
||||||
|
import io.github.linpeilie.annotations.AutoMapper;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.ruoyi.domain.McpInfo;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP业务对象 mcp_info
|
||||||
|
*
|
||||||
|
* @author ageerle
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
|
||||||
|
@AutoMapper(target = McpInfo.class, reverseConvertGenerate = false)
|
||||||
|
public class McpInfoBo implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
@NotNull(message = "id不能为空" )
|
||||||
|
private Integer mcpId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器名称
|
||||||
|
*/
|
||||||
|
private String serverName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接方式
|
||||||
|
*/
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command
|
||||||
|
*/
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Args
|
||||||
|
*/
|
||||||
|
private String arguments;
|
||||||
|
private String description;
|
||||||
|
/**
|
||||||
|
* Env
|
||||||
|
*/
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private Boolean status;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.ruoyi.domain.vo;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import io.github.linpeilie.annotations.AutoMapper;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
|
||||||
|
import org.ruoyi.common.excel.convert.ExcelDictConvert;
|
||||||
|
import org.ruoyi.domain.McpInfo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP视图对象 mcp_info
|
||||||
|
*
|
||||||
|
* @author jiyi
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ExcelIgnoreUnannotated
|
||||||
|
@AutoMapper(target = McpInfo.class)
|
||||||
|
public class McpInfoVo implements Serializable {
|
||||||
|
private Integer mcpId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器名称
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "服务器名称")
|
||||||
|
private String serverName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接方式
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "链接方式", converter = ExcelDictConvert.class)
|
||||||
|
@ExcelDictFormat(dictType = "mcp_transport_type")
|
||||||
|
private String transportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "Command")
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Args
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "Args")
|
||||||
|
private String arguments;
|
||||||
|
@ExcelProperty(value = "Description")
|
||||||
|
private String description;
|
||||||
|
/**
|
||||||
|
* Env
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "Env")
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "是否启用")
|
||||||
|
private Boolean status;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.ruoyi.mapper;
|
||||||
|
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
import org.ruoyi.core.mapper.BaseMapperPlus;
|
||||||
|
import org.ruoyi.domain.McpInfo;
|
||||||
|
import org.ruoyi.domain.vo.McpInfoVo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCPMapper接口
|
||||||
|
*
|
||||||
|
* @author jiuyi
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface McpInfoMapper extends BaseMapperPlus<McpInfo, McpInfoVo> {
|
||||||
|
@Select("SELECT * FROM mcp_info WHERE server_name = #{serverName}")
|
||||||
|
McpInfo selectByServerName(@Param("serverName") String serverName);
|
||||||
|
|
||||||
|
@Select("SELECT * FROM mcp_info WHERE status = 1")
|
||||||
|
List<McpInfo> selectActiveServers();
|
||||||
|
|
||||||
|
@Select("SELECT server_name FROM mcp_info WHERE status = 1")
|
||||||
|
List<String> 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="org.ruoyi.mapper.McpInfoMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FunctionCallback> callbacks = new ArrayList<>();
|
||||||
|
List<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, McpServerConfig> mcpServers;
|
||||||
|
|
||||||
|
// getters and setters
|
||||||
|
public Map<String, McpServerConfig> getMcpServers() {
|
||||||
|
return mcpServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMcpServers(Map<String, McpServerConfig> mcpServers) {
|
||||||
|
this.mcpServers = mcpServers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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<String, Process> runningProcesses = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, McpServerProcess> mcpServerProcesses = new ConcurrentHashMap<>();
|
||||||
|
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final Map<String, BufferedWriter> processWriters = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, BufferedReader> 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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> buildCommandListWithFullPaths(String command, List<String> args) {
|
||||||
|
List<String> 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<String> args, Map<String, String> 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<String> getRunningMcpServers() {
|
||||||
|
Set<String> running = new HashSet<>();
|
||||||
|
for (Map.Entry<String, Process> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package org.ruoyi.mcp.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.info.ProcessInfo;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.Disposable;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class McpProcessSSEManager {
|
||||||
|
|
||||||
|
private final Map<String, Process> runningProcesses = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, ProcessInfo> processInfos = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, WebClient> sseClients = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Disposable> sseSubscriptions = new ConcurrentHashMap<>();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private McpSSEToolInvoker mcpToolInvoker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 MCP 服务器进程(SSE 模式)
|
||||||
|
*/
|
||||||
|
public boolean startMcpServer(String serverName, String command, List<String> args, Map<String, String> env) {
|
||||||
|
try {
|
||||||
|
System.out.println("准备启动 MCP 服务器 (SSE 模式): " + serverName);
|
||||||
|
|
||||||
|
// 如果已经运行,先停止
|
||||||
|
if (isMcpServerRunning(serverName)) {
|
||||||
|
stopMcpServer(serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建命令
|
||||||
|
List<String> commandList = buildCommandList(command, args);
|
||||||
|
|
||||||
|
// 创建 ProcessBuilder
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(commandList);
|
||||||
|
processBuilder.redirectErrorStream(true);
|
||||||
|
|
||||||
|
// 设置工作目录
|
||||||
|
String workingDir = System.getProperty("user.dir");
|
||||||
|
processBuilder.directory(new File(workingDir));
|
||||||
|
|
||||||
|
// 打印调试信息
|
||||||
|
System.out.println("=== ProcessBuilder 调试信息 ===");
|
||||||
|
System.out.println("完整命令列表: " + commandList);
|
||||||
|
System.out.println("================================");
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
runningProcesses.put(serverName, process);
|
||||||
|
|
||||||
|
ProcessInfo processInfo = new ProcessInfo();
|
||||||
|
processInfo.setStartTime(System.currentTimeMillis());
|
||||||
|
processInfo.setPid(getProcessId(process));
|
||||||
|
processInfos.put(serverName, processInfo);
|
||||||
|
|
||||||
|
// 启动日志读取线程
|
||||||
|
ExecutorService executorService = Executors.newCachedThreadPool();
|
||||||
|
executorService.submit(() -> readProcessOutput(serverName, process));
|
||||||
|
|
||||||
|
// 等待进程启动
|
||||||
|
Thread.sleep(3000);
|
||||||
|
boolean isAlive = process.isAlive();
|
||||||
|
|
||||||
|
if (isAlive) {
|
||||||
|
System.out.println("✓ MCP 服务器 [" + serverName + "] 启动成功");
|
||||||
|
// 初始化 SSE 连接
|
||||||
|
initializeSseConnection(serverName);
|
||||||
|
} else {
|
||||||
|
System.err.println("✗ MCP 服务器 [" + serverName + "] 启动失败");
|
||||||
|
readErrorOutput(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAlive;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 启动 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private String getProcessId(Process process) {
|
||||||
|
try {
|
||||||
|
return String.valueOf(process.pid());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 初始化 SSE 连接
|
||||||
|
*/
|
||||||
|
private void initializeSseConnection(String serverName) {
|
||||||
|
try {
|
||||||
|
// 创建 WebClient 用于 SSE 连接
|
||||||
|
WebClient webClient = WebClient.builder()
|
||||||
|
.baseUrl("http://localhost:3000") // 假设默认端口 3000
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sseClients.put(serverName, webClient);
|
||||||
|
|
||||||
|
// 建立 SSE 连接
|
||||||
|
String sseUrl = "/sse/" + serverName; // SSE 端点
|
||||||
|
|
||||||
|
Disposable subscription = webClient.get()
|
||||||
|
.uri(sseUrl)
|
||||||
|
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToFlux(String.class)
|
||||||
|
.subscribe(
|
||||||
|
event -> handleSseEvent(serverName, event),
|
||||||
|
error -> System.err.println("SSE 连接错误 [" + serverName + "]: " + error.getMessage()),
|
||||||
|
() -> System.out.println("SSE 连接完成 [" + serverName + "]")
|
||||||
|
);
|
||||||
|
|
||||||
|
sseSubscriptions.put(serverName, subscription);
|
||||||
|
System.out.println("✓ SSE 连接建立成功 [" + serverName + "]");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 建立 SSE 连接失败 [" + serverName + "]: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SSE 事件
|
||||||
|
*/
|
||||||
|
private void handleSseEvent(String serverName, String event) {
|
||||||
|
try {
|
||||||
|
System.out.println("收到来自 [" + serverName + "] 的 SSE 事件: " + event);
|
||||||
|
|
||||||
|
// 解析 SSE 事件
|
||||||
|
if (event.startsWith("data: ")) {
|
||||||
|
String jsonData = event.substring(6); // 移除 "data: " 前缀
|
||||||
|
Map<String, Object> message = objectMapper.readValue(jsonData, Map.class);
|
||||||
|
|
||||||
|
// 处理不同类型的事件
|
||||||
|
String type = (String) message.get("type");
|
||||||
|
if ("tool_response".equals(type)) {
|
||||||
|
mcpToolInvoker.handleSseResponse(serverName, message);
|
||||||
|
} else if ("tool_error".equals(type)) {
|
||||||
|
mcpToolInvoker.handleSseError(serverName, message);
|
||||||
|
} else if ("progress".equals(type)) {
|
||||||
|
handleProgressEvent(serverName, message);
|
||||||
|
} else {
|
||||||
|
System.out.println("[" + serverName + "] 未知事件类型: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("处理 SSE 事件失败 [" + serverName + "]: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理进度事件
|
||||||
|
*/
|
||||||
|
private void handleProgressEvent(String serverName, Map<String, Object> message) {
|
||||||
|
Object progress = message.get("progress");
|
||||||
|
Object messageText = message.get("message");
|
||||||
|
System.out.println("[" + serverName + "] 进度: " + progress + " - " + messageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建命令列表
|
||||||
|
*/
|
||||||
|
private List<String> buildCommandList(String command, List<String> args) {
|
||||||
|
List<String> commandList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (isWindows() && "npx".equalsIgnoreCase(command)) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止 MCP 服务器进程
|
||||||
|
*/
|
||||||
|
public boolean stopMcpServer(String serverName) {
|
||||||
|
// 停止 SSE 连接
|
||||||
|
Disposable subscription = sseSubscriptions.remove(serverName);
|
||||||
|
if (subscription != null && !subscription.isDisposed()) {
|
||||||
|
subscription.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
sseClients.remove(serverName);
|
||||||
|
|
||||||
|
// 停止进程
|
||||||
|
Process process = runningProcesses.remove(serverName);
|
||||||
|
ProcessInfo processInfo = processInfos.remove(serverName);
|
||||||
|
|
||||||
|
if (process != null && process.isAlive()) {
|
||||||
|
process.destroy();
|
||||||
|
try {
|
||||||
|
if (!process.waitFor(10, TimeUnit.SECONDS)) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
process.waitFor(2, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
System.out.println("MCP 服务器 [" + serverName + "] 已停止");
|
||||||
|
return true;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查 MCP 服务器是否运行
|
||||||
|
*/
|
||||||
|
public boolean isMcpServerRunning(String serverName) {
|
||||||
|
Process process = runningProcesses.get(serverName);
|
||||||
|
return process != null && process.isAlive();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 进程信息类
|
||||||
|
*/
|
||||||
|
public static class ProcessInfo {
|
||||||
|
private String pid;
|
||||||
|
private long startTime;
|
||||||
|
|
||||||
|
public String getPid() { return pid; }
|
||||||
|
public void setPid(String pid) { this.pid = pid; }
|
||||||
|
|
||||||
|
public long getStartTime() { return startTime; }
|
||||||
|
public void setStartTime(long startTime) { this.startTime = startTime; }
|
||||||
|
|
||||||
|
public long getUptime() {
|
||||||
|
return System.currentTimeMillis() - startTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package org.ruoyi.mcp.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.FluxSink;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
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 McpSSEToolInvoker {
|
||||||
|
|
||||||
|
|
||||||
|
private final Map<String, CompletableFuture<Object>> pendingRequests = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicLong requestIdCounter = new AtomicLong(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 MCP 工具(SSE 模式)
|
||||||
|
*/
|
||||||
|
public Object invokeTool(String serverName, Object parameters) {
|
||||||
|
try {
|
||||||
|
// 生成请求ID
|
||||||
|
String requestId = "req_" + requestIdCounter.incrementAndGet();
|
||||||
|
|
||||||
|
// 创建 CompletableFuture 等待响应
|
||||||
|
CompletableFuture<Object> future = new CompletableFuture<>();
|
||||||
|
pendingRequests.put(requestId, future);
|
||||||
|
|
||||||
|
// 构造 MCP 调用请求
|
||||||
|
Map<String, Object> callRequest = new HashMap<>();
|
||||||
|
callRequest.put("requestId", requestId);
|
||||||
|
callRequest.put("serverName", serverName);
|
||||||
|
callRequest.put("parameters", convertToMap(parameters));
|
||||||
|
callRequest.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
System.out.println("通过 SSE 调用 MCP 工具 [" + serverName + "] 参数: " + parameters);
|
||||||
|
|
||||||
|
// 发送请求到 MCP 服务器(通过 HTTP POST)
|
||||||
|
sendSseToolCall(serverName, callRequest);
|
||||||
|
|
||||||
|
// 等待响应(超时 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 SSE 工具调用请求
|
||||||
|
*/
|
||||||
|
private void sendSseToolCall(String serverName, Map<String, Object> callRequest) {
|
||||||
|
try {
|
||||||
|
// 通过 HTTP POST 发送工具调用请求
|
||||||
|
WebClient webClient = WebClient.builder()
|
||||||
|
.baseUrl("http://localhost:3000")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String toolCallUrl = "/tool/" + serverName;
|
||||||
|
|
||||||
|
webClient.post()
|
||||||
|
.uri(toolCallUrl)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(callRequest)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.timeout(Duration.ofSeconds(5))
|
||||||
|
.subscribe(
|
||||||
|
response -> System.out.println("工具调用请求发送成功: " + response),
|
||||||
|
error -> System.err.println("工具调用请求发送失败: " + error.getMessage())
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("发送 SSE 工具调用请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SSE 响应
|
||||||
|
*/
|
||||||
|
public void handleSseResponse(String serverName, Map<String, Object> message) {
|
||||||
|
String requestId = (String) message.get("requestId");
|
||||||
|
if (requestId != null) {
|
||||||
|
CompletableFuture<Object> future = pendingRequests.remove(requestId);
|
||||||
|
if (future != null) {
|
||||||
|
Object data = message.get("data");
|
||||||
|
future.complete(data != null ? data : message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SSE 错误
|
||||||
|
*/
|
||||||
|
public void handleSseError(String serverName, Map<String, Object> message) {
|
||||||
|
String requestId = (String) message.get("requestId");
|
||||||
|
if (requestId != null) {
|
||||||
|
CompletableFuture<Object> future = pendingRequests.remove(requestId);
|
||||||
|
if (future != null) {
|
||||||
|
String errorMessage = (String) message.get("message");
|
||||||
|
future.completeExceptionally(new RuntimeException(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式调用 MCP 工具(支持实时进度)
|
||||||
|
*/
|
||||||
|
public Flux<Object> invokeToolStream(String serverName, Object parameters) {
|
||||||
|
return Flux.create(emitter -> {
|
||||||
|
try {
|
||||||
|
// 生成请求ID
|
||||||
|
String requestId = "req_" + requestIdCounter.incrementAndGet();
|
||||||
|
|
||||||
|
// 构造 MCP 调用请求
|
||||||
|
Map<String, Object> callRequest = new HashMap<>();
|
||||||
|
callRequest.put("requestId", requestId);
|
||||||
|
callRequest.put("serverName", serverName);
|
||||||
|
callRequest.put("parameters", convertToMap(parameters));
|
||||||
|
callRequest.put("stream", true); // 标记为流式调用
|
||||||
|
callRequest.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 创建流式处理器
|
||||||
|
StreamHandler streamHandler = new StreamHandler(emitter);
|
||||||
|
pendingRequests.put(requestId + "_stream", null); // 占位符
|
||||||
|
|
||||||
|
// 发送流式调用请求
|
||||||
|
sendSseToolCall(serverName, callRequest);
|
||||||
|
|
||||||
|
// 注册流式处理器
|
||||||
|
registerStreamHandler(requestId, streamHandler);
|
||||||
|
|
||||||
|
emitter.onDispose(() -> {
|
||||||
|
// 清理资源
|
||||||
|
pendingRequests.remove(requestId + "_stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
emitter.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式处理器
|
||||||
|
*/
|
||||||
|
private static class StreamHandler {
|
||||||
|
private final FluxSink<Object> emitter;
|
||||||
|
|
||||||
|
public StreamHandler(FluxSink<Object> emitter) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNext(Object data) {
|
||||||
|
emitter.next(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onComplete() {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onError(Throwable error) {
|
||||||
|
emitter.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> convertToMap(Object parameters) {
|
||||||
|
if (parameters instanceof Map) {
|
||||||
|
Map<String, Object> 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<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerStreamHandler(String requestId, StreamHandler streamHandler) {
|
||||||
|
// 实现流式处理器注册逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> args;
|
||||||
|
private Map<String, String> 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<String> getArgs() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArgs(List<String> args) {
|
||||||
|
this.args = args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getEnv() {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnv(Map<String, String> 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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, CompletableFuture<Object>> 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<Object> future = new CompletableFuture<>();
|
||||||
|
pendingRequests.put(requestId, future);
|
||||||
|
|
||||||
|
// 构造 MCP 调用消息
|
||||||
|
Map<String, Object> 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<String, Object> message) {
|
||||||
|
String type = (String) message.get("type");
|
||||||
|
if ("tool_response".equals(type)) {
|
||||||
|
String requestId = (String) message.get("requestId");
|
||||||
|
if (requestId != null) {
|
||||||
|
CompletableFuture<Object> 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<Object> future = pendingRequests.remove(requestId);
|
||||||
|
if (future != null) {
|
||||||
|
String errorMessage = (String) message.get("message");
|
||||||
|
future.completeExceptionally(new RuntimeException(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> convertToMap(Object parameters) {
|
||||||
|
if (parameters instanceof Map) {
|
||||||
|
Map<String, Object> 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<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.ruoyi.mcp.controller;
|
||||||
|
|
||||||
|
import org.ruoyi.mcp.config.McpSSEToolInvoker;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/sse")
|
||||||
|
public class MCPSseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private McpSSEToolInvoker mcpToolInvoker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 流式响应端点
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/{serverName}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter streamMcpResponse(@PathVariable String serverName) {
|
||||||
|
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发送连接建立消息
|
||||||
|
emitter.send(SseEmitter.event()
|
||||||
|
.name("connected")
|
||||||
|
.data(Map.of("serverName", serverName, "status", "connected")));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 MCP 工具(流式)
|
||||||
|
*/
|
||||||
|
@PostMapping("/tool/{serverName}")
|
||||||
|
public ResponseEntity<?> callMcpTool(
|
||||||
|
@PathVariable String serverName,
|
||||||
|
@RequestBody Map<String, Object> request) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean isStream = (Boolean) request.getOrDefault("stream", false);
|
||||||
|
Object parameters = request.get("parameters");
|
||||||
|
|
||||||
|
if (isStream) {
|
||||||
|
// 流式调用
|
||||||
|
return ResponseEntity.ok(Map.of("status", "streaming_started"));
|
||||||
|
} else {
|
||||||
|
// 普通调用
|
||||||
|
Object result = mcpToolInvoker.invokeTool(serverName, parameters);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
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;
|
||||||
|
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||||
|
import org.ruoyi.common.log.annotation.Log;
|
||||||
|
import org.ruoyi.common.web.core.BaseController;
|
||||||
|
import org.ruoyi.core.page.PageQuery;
|
||||||
|
import org.ruoyi.common.core.domain.R;
|
||||||
|
import org.ruoyi.common.core.validate.AddGroup;
|
||||||
|
import org.ruoyi.common.core.validate.EditGroup;
|
||||||
|
import org.ruoyi.common.log.enums.BusinessType;
|
||||||
|
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||||
|
|
||||||
|
import org.ruoyi.core.page.TableDataInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP
|
||||||
|
*
|
||||||
|
* @author ageerle
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/operator/mcpInfo")
|
||||||
|
public class McpInfoController extends BaseController {
|
||||||
|
|
||||||
|
private final McpInfoService mcpInfoService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP列表
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:list")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo<McpInfoVo> list(McpInfoBo bo, PageQuery pageQuery) {
|
||||||
|
return mcpInfoService.queryPageList(bo, pageQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出MCP列表
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:export")
|
||||||
|
@Log(title = "MCP", businessType = BusinessType.EXPORT)
|
||||||
|
@PostMapping("/export")
|
||||||
|
public void export(McpInfoBo bo, HttpServletResponse response) {
|
||||||
|
List<McpInfoVo> list = mcpInfoService.queryList(bo);
|
||||||
|
ExcelUtil.exportExcel(list, "MCP", McpInfoVo.class, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取MCP详细信息
|
||||||
|
*
|
||||||
|
* @param mcpId 主键
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:query")
|
||||||
|
@GetMapping("/{mcpId}")
|
||||||
|
public R<McpInfoVo> getInfo(@NotNull(message = "主键不能为空")
|
||||||
|
@PathVariable Integer mcpId) {
|
||||||
|
return R.ok(mcpInfoService.queryById(mcpId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增MCP
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:add")
|
||||||
|
@Log(title = "MCP", businessType = BusinessType.INSERT)
|
||||||
|
@RepeatSubmit()
|
||||||
|
@PostMapping()
|
||||||
|
public R<Void> add(@Validated(AddGroup.class) @RequestBody McpInfoBo bo) {
|
||||||
|
return toAjax(mcpInfoService.insertByBo(bo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改MCP
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:edit")
|
||||||
|
@Log(title = "MCP", businessType = BusinessType.UPDATE)
|
||||||
|
@RepeatSubmit()
|
||||||
|
@PutMapping()
|
||||||
|
public R<Void> edit(@Validated(EditGroup.class) @RequestBody McpInfoBo bo) {
|
||||||
|
return toAjax(mcpInfoService.updateByBo(bo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除MCP
|
||||||
|
*
|
||||||
|
* @param mcpIds 主键串
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("operator:mcpInfo:remove")
|
||||||
|
@Log(title = "MCP", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{mcpIds}")
|
||||||
|
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||||
|
@PathVariable Integer[] mcpIds) {
|
||||||
|
return toAjax(mcpInfoService.deleteWithValidByIds(List.of(mcpIds), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加或更新 MCP 工具
|
||||||
|
*/
|
||||||
|
@PostMapping("/tools")
|
||||||
|
public R<McpInfo> saveToolConfig(@RequestBody McpInfoRequest request) {
|
||||||
|
return R.ok(mcpInfoService.saveToolConfig(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有活跃服务器名称
|
||||||
|
*/
|
||||||
|
@GetMapping("/tools/names")
|
||||||
|
public R<List<String>> getActiveServerNames() {
|
||||||
|
return R.ok(mcpInfoService.getActiveServerNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取工具配置
|
||||||
|
*/
|
||||||
|
@GetMapping("/tools/{serverName}")
|
||||||
|
public R<McpServerConfig> getToolConfig(@PathVariable String serverName) {
|
||||||
|
return R.ok(mcpInfoService.getToolConfigByName(serverName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用工具
|
||||||
|
*/
|
||||||
|
@PostMapping("/tools/{serverName}/enable")
|
||||||
|
public Map<String, Object> enableTool(@PathVariable String serverName) {
|
||||||
|
boolean success = mcpInfoService.enableTool(serverName);
|
||||||
|
return Map.of("success", success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用工具
|
||||||
|
*/
|
||||||
|
@PostMapping("/tools/{serverName}/disable")
|
||||||
|
public Map<String, Object> disableTool(@PathVariable String serverName) {
|
||||||
|
boolean success = mcpInfoService.disableTool(serverName);
|
||||||
|
return Map.of("success", success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除工具
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/tools/{serverName}")
|
||||||
|
public Map<String, Object> deleteTool(@PathVariable String serverName) {
|
||||||
|
boolean success = mcpInfoService.deleteToolConfig(serverName);
|
||||||
|
return Map.of("success", success);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> args;
|
||||||
|
private Map<String, String> 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<String> getArgs() { return args; }
|
||||||
|
public void setArgs(List<String> args) { this.args = args; }
|
||||||
|
|
||||||
|
public Map<String, String> getEnv() { return env; }
|
||||||
|
public void setEnv(Map<String, String> env) { this.env = env; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCPService接口
|
||||||
|
*
|
||||||
|
* @author ageerle
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
public interface McpInfoService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP
|
||||||
|
*/
|
||||||
|
McpInfoVo queryById(Integer mcpId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP列表
|
||||||
|
*/
|
||||||
|
TableDataInfo<McpInfoVo> queryPageList(McpInfoBo bo, PageQuery pageQuery);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP列表
|
||||||
|
*/
|
||||||
|
List<McpInfoVo> queryList(McpInfoBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增MCP
|
||||||
|
*/
|
||||||
|
Boolean insertByBo(McpInfoBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改MCP
|
||||||
|
*/
|
||||||
|
Boolean updateByBo(McpInfoBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验并批量删除MCP信息
|
||||||
|
*/
|
||||||
|
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
|
||||||
|
|
||||||
|
McpServerConfig getToolConfigByName(String serverName);
|
||||||
|
|
||||||
|
McpConfig getAllActiveMcpConfig();
|
||||||
|
|
||||||
|
List<String> getActiveServerNames();
|
||||||
|
|
||||||
|
McpInfo saveToolConfig(McpInfoRequest request);
|
||||||
|
|
||||||
|
boolean deleteToolConfig(String serverName);
|
||||||
|
|
||||||
|
boolean updateToolStatus(String serverName, Boolean status);
|
||||||
|
|
||||||
|
boolean enableTool(String serverName);
|
||||||
|
|
||||||
|
boolean disableTool(String serverName);
|
||||||
|
}
|
||||||
@@ -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<String, McpServerConfig> 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<String, Object> getMcpToolStatus() {
|
||||||
|
List<String> activeTools = mcpInfoService.getActiveServerNames();
|
||||||
|
Map<String, Object> status = new HashMap<>();
|
||||||
|
|
||||||
|
for (String serverName : activeTools) {
|
||||||
|
boolean isRunning = mcpProcessManager.isMcpServerRunning(serverName);
|
||||||
|
McpProcessManager.McpServerProcess processInfo = mcpProcessManager.getProcessInfo(serverName);
|
||||||
|
|
||||||
|
Map<String, Object> 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<String> getRunningTools() {
|
||||||
|
return mcpProcessManager.getRunningMcpServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
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;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCPService业务层处理
|
||||||
|
*
|
||||||
|
* @author ageerle
|
||||||
|
* @date Sat Aug 09 16:50:58 CST 2025
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class McpInfoServiceImpl implements McpInfoService {
|
||||||
|
|
||||||
|
private final McpInfoMapper baseMapper;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
/**
|
||||||
|
* 查询MCP
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public McpInfoVo queryById(Integer mcpId) {
|
||||||
|
return baseMapper.selectVoById(mcpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TableDataInfo<McpInfoVo> queryPageList(McpInfoBo bo, PageQuery pageQuery) {
|
||||||
|
LambdaQueryWrapper<McpInfo> lqw = buildQueryWrapper(bo);
|
||||||
|
Page<McpInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||||
|
return TableDataInfo.build(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询MCP列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<McpInfoVo> queryList(McpInfoBo bo) {
|
||||||
|
LambdaQueryWrapper<McpInfo> lqw = buildQueryWrapper(bo);
|
||||||
|
return baseMapper.selectVoList(lqw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LambdaQueryWrapper<McpInfo> buildQueryWrapper(McpInfoBo bo) {
|
||||||
|
LambdaQueryWrapper<McpInfo> lqw = Wrappers.lambdaQuery();
|
||||||
|
lqw.like(StringUtils.isNotBlank(bo.getServerName()), McpInfo::getServerName, bo.getServerName());
|
||||||
|
lqw.eq(StringUtils.isNotBlank(bo.getTransportType()), McpInfo::getTransportType, bo.getTransportType());
|
||||||
|
lqw.eq(StringUtils.isNotBlank(bo.getCommand()), McpInfo::getCommand, bo.getCommand());
|
||||||
|
lqw.eq(bo.getStatus() != null, McpInfo::getStatus, bo.getStatus());
|
||||||
|
return lqw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增MCP
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean insertByBo(McpInfoBo bo) {
|
||||||
|
McpInfo add = MapstructUtils.convert(bo, McpInfo. class);
|
||||||
|
validEntityBeforeSave(add);
|
||||||
|
boolean flag = baseMapper.insert(add) > 0;
|
||||||
|
if (flag) {
|
||||||
|
bo.setMcpId(add.getMcpId());
|
||||||
|
}
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改MCP
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean updateByBo(McpInfoBo bo) {
|
||||||
|
McpInfo update = MapstructUtils.convert(bo, McpInfo. class);
|
||||||
|
validEntityBeforeSave(update);
|
||||||
|
return baseMapper.updateById(update) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存前的数据校验
|
||||||
|
*/
|
||||||
|
private void validEntityBeforeSave(McpInfo entity) {
|
||||||
|
//TODO 做一些数据校验,如唯一约束
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除MCP
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid) {
|
||||||
|
if (isValid) {
|
||||||
|
//TODO 做一些业务上的校验,判断是否需要校验
|
||||||
|
}
|
||||||
|
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<McpInfo> activeTools = baseMapper.selectActiveServers();
|
||||||
|
Map<String, McpServerConfig> 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<String> 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<String> args = objectMapper.readValue(tool.getArguments(), new TypeReference<List<String>>() {});
|
||||||
|
config.setArgs(args);
|
||||||
|
} else {
|
||||||
|
config.setArgs(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 env
|
||||||
|
if (tool.getEnv() != null && !tool.getEnv().isEmpty()) {
|
||||||
|
Map<String, String> env = objectMapper.readValue(tool.getEnv(), new TypeReference<Map<String, String>>() {});
|
||||||
|
config.setEnv(env);
|
||||||
|
} else {
|
||||||
|
config.setEnv(new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
config.setArgs(new ArrayList<>());
|
||||||
|
config.setEnv(new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
script/sql/update/mcp_info_menu.sql
Normal file
46
script/sql/update/mcp_info_menu.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- 菜单 SQL
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309056, 'MCP', '2000', '1', 'mcpInfo', 'operator/mcpInfo/index', 1, 0, 'C', '0', '0', 'operator:mcpInfo:list', '#', 103, 1, sysdate(), null, null, 'MCP菜单');
|
||||||
|
|
||||||
|
-- 按钮 SQL
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309057, 'MCP查询', 1954103099019309056, '1', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:query', '#', 103, 1, sysdate(), null, null, '');
|
||||||
|
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309058, 'MCP新增', 1954103099019309056, '2', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:add', '#', 103, 1, sysdate(), null, null, '');
|
||||||
|
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309059, 'MCP修改', 1954103099019309056, '3', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:edit', '#', 103, 1, sysdate(), null, null, '');
|
||||||
|
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309060, 'MCP删除', 1954103099019309056, '4', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:remove', '#', 103, 1, sysdate(), null, null, '');
|
||||||
|
|
||||||
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
|
values(1954103099019309061, 'MCP导出', 1954103099019309056, '5', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:export', '#', 103, 1, sysdate(), null, null, '');
|
||||||
|
|
||||||
|
|
||||||
|
-- mcp_info ddl
|
||||||
|
CREATE TABLE `mcp_info` (
|
||||||
|
`mcp_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||||
|
`server_name` varchar(50) DEFAULT NULL COMMENT '服务器名称',
|
||||||
|
`transport_type` varchar(255) DEFAULT NULL COMMENT '链接方式',
|
||||||
|
`command` varchar(255) DEFAULT NULL COMMENT 'Command',
|
||||||
|
`arguments` varchar(255) DEFAULT NULL COMMENT 'Args',
|
||||||
|
`env` varchar(255) DEFAULT NULL COMMENT 'Env',
|
||||||
|
`status` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
|
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
|
||||||
|
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
|
||||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`mcp_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
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 (1954098808913211393, '000000', 0, 'STDIO', 'STDIO', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:33:56', 1, '2025-08-09 16:34:19', 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 (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);
|
||||||
Reference in New Issue
Block a user