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