feat:基于stdio模式 启动mcp服务器

This commit is contained in:
酒亦
2025-08-11 21:22:12 +08:00
parent 9891259452
commit bc2eb8fdb9
17 changed files with 1088 additions and 10 deletions

View File

@@ -49,6 +49,8 @@ public class McpInfo extends BaseEntity {
*/
private String arguments;
private String description;
/**
* Env
*/

View File

@@ -44,8 +44,8 @@ public class McpInfoBo implements Serializable {
* Args
*/
private String arguments;
/**
private String description;
/**
* Env
*/
private String env;

View File

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

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<>();
}
}

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

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

View File

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