diff --git a/README.md b/README.md index 7a150e91..b018d5b6 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,11 @@ + + + MCP接入点 + + @@ -170,8 +175,8 @@ #### 🚀 部署方式选择 | 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 | |---------|------|---------|---------|---------|---------| -| **最简化安装** | 智能对话、IOT、MCP、视觉感知,数据存储在配置文件 | 低配置环境,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | -| **全模块安装** | 智能对话、IOT、MCP、视觉感知、OTA、智控台,数据存储在数据库 | 完整功能体验 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | +| **全模块安装** | 智能对话、IOT、MCP接入点、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | > 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据 @@ -226,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 视觉感知系统 | 支持多种VLLM(视觉大模型),实现多模态交互 | | 意图识别系统 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 | | 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 | -| IOT/MCP控制协议 | 支持设备注册管理、智能控制接口,同时支持IOT、MCP控制协议 | +| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 | | 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 | | 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 | | 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 | diff --git a/docs/images/demo13.png b/docs/images/demo13.png new file mode 100644 index 00000000..1ad79fa3 Binary files /dev/null and b/docs/images/demo13.png differ diff --git a/docs/mcp-endpoint-integration.md b/docs/mcp-endpoint-integration.md new file mode 100644 index 00000000..ca72aef1 --- /dev/null +++ b/docs/mcp-endpoint-integration.md @@ -0,0 +1,134 @@ +# MCP 接入点部署使用指南 + +本教程包含2个部分 +- 1、如何开启mcp接入点 +- 2、如何为智能体接入一个简单的mcp功能,如计算器功能 + +部署的前提条件: +- 1、你已经部署了全模块,因为mcp接入点需要全模块中的智控台功能 +- 2、你想在不修改xiaozhi-server项目的前提下,扩展小智的功能 + +# 如何开启mcp接入点 + +## 第一步,下载mcp接入点项目源码 + +浏览器打开[mcp接入点项目地址](https://github.com/xinnan-tech/mcp-endpoint-server) + +打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。 + +点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-endpoint-server-main` +你需要把它重命名成`mcp-endpoint-server`。 + +## 第二步,启动程序 +这个项目是一个很简单的项目,建议使用docker运行。不过如果你不想使用docker运行,你可以参考[这个页面](https://github.com/xinnan-tech/mcp-endpoint-server/blob/main/README_dev.md)使用源码运行。以下是docker运行的方法 + +``` +# 进入本项目源码根目录 +cd mcp-endpoint-server + +# 清除缓存 +docker compose -f docker-compose.yml down +docker stop mcp-endpoint-server +docker rm mcp-endpoint-server +docker rmi ghcr.nju.edu.cn/xinnan-tech/mcp-endpoint-server:latest + +# 启动docker容器 +docker compose -f docker-compose.yml up -d +# 查看日志 +docker logs -f mcp-endpoint-server +``` + +此时,日志里会输出类似以下的日志 +``` +====================================================== +接口地址: http://172.1.1.1:8004/mcp_endpoint/health?key=xxxx +=======上面的地址是MCP接入点地址,请勿泄露给任何人============ +``` + +请你把接口地址复制出来: + +由于你是docker部署,切不可直接使用上面的地址! + +由于你是docker部署,切不可直接使用上面的地址! + +由于你是docker部署,切不可直接使用上面的地址! + +你先把地址复制出来,放在一个草稿里,你要知道你的电脑的局域网ip是什么,例如我的电脑局域网ip是`192.168.1.25`,那么 +原来我的接口地址 +``` +http://172.1.1.1:8004/mcp_endpoint/health?key=xxxx +``` +就要改成 +``` +http://192.168.1.25:8004/mcp_endpoint/health?key=xxxx +``` + +改好后,请使用浏览器直接访问这个接口。当浏览器出现类似这样的代码,说明是成功了。 +``` +{"result":{"status":"success","connections":{"tool_connections":0,"robot_connections":0,"total_connections":0}},"error":null,"id":null,"jsonrpc":"2.0"} +``` + +请你保留好这个`接口地址`,下一步要用到。 + +## 第三步,配置智控台 + +使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。 + +然后搜索参数`server.mcp_endpoint`,此时,它的值应该是`null`值。 +点击修改按钮,把上一步得来的`接口地址`粘贴到`参数值`里。然后保存。 + +如果能保存成功,说明一切顺利,你可以去智能体查看效果了。如果不成功,说明智控台无法访问mcp接入点,很大概率是网络防火墙,或者没有填写正确的局域网ip。 + +# 如何为智能体接入一个简单的mcp功能,如计算器功能 + +如果以上步骤顺利,你可以进入智能体管理,点击`配置角色`,在`意图识别`的右边,有一个`编辑功能`的按钮。 + +点击这个按钮。在弹出的页面里,位于底部,会有`MCP接入点`,正常来说,会显示这个智能体的`MCP接入点地址`,接下来,我们来给这个智能体扩展一个基于MCP技术的计算器的功能。 + +这个`MCP接入点地址`很重要,你等一下会用到。 + +## 第一步 下载虾哥MCP计算器项目代码 + +浏览器打开虾哥写的[计算器项目](https://github.com/78/mcp-calculator), + +打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。 + +点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-calculatorr-main` +你需要把它重命名成`mcp-calculator`。接下来,我们用命令行进入项目目录即安装依赖 + + +```bash +# 进入项目目录 +cd mcp-calculator + +conda remove -n mcp-calculator --all -y +conda create -n mcp-calculator python=3.10 -y +conda activate mcp-calculator + +pip install -r requirements.txt +``` + +## 第二步 启动 + +启动前,先从你的智控台的智能体里,复制到了MCP接入点的地址。 + +例如我的智能体的mcp地址是 +``` +ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc +``` + +开始输入命令 + +```bash +export MCP_ENDPOINT=ws://192.168.4.7:8004/mcp_endpoint/mcp/?token=abc +``` + +输入完后,启动程序 + +```bash +python mcp_pipe.py calculator.py +``` + + +启动完后,你再进入智控台,点击刷新MCP的接入状态,就会看到你扩展的功能列表了。 + diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index b5837eb5..32a121fa 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -111,6 +111,11 @@ public interface Constant { */ String FILE_EXTENSION_SEG = "."; + /** + * mcp接入点路径 + */ + String SERVER_MCP_ENDPOINT = "server.mcp_endpoint"; + /** * 无记忆 */ @@ -227,7 +232,7 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.5.8"; + public static final String VERSION = "0.6.1"; /** * 无效固件URL diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java b/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java new file mode 100644 index 00000000..79b05192 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/HashEncryptionUtil.java @@ -0,0 +1,52 @@ +package xiaozhi.common.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 哈希加密算法的工具类 + * @author zjy + */ +@Slf4j +public class HashEncryptionUtil { + /** + * 使用md5进行加密 + * @param context 被加密的内容 + * @return 哈希值 + */ + public static String Md5hexDigest(String context){ + return hexDigest(context,"MD5"); + } + + /** + * 指定哈希算法进行加密 + * @param context 被加密的内容 + * @param algorithm 哈希算法 + * @return 哈希值 + */ + public static String hexDigest(String context,String algorithm ){ + // 获取MD5算法实例 + MessageDigest md = null; + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + log.error("加密失败的算法:{}",algorithm); + throw new RuntimeException("加密失败,"+ algorithm +"哈希算法系统不支持"); + } + // 计算智能体id的MD5值 + byte[] messageDigest = md.digest(context.getBytes()); + // 将字节数组转换为十六进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + String hex = Integer.toHexString(0xFF & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java new file mode 100644 index 00000000..3f369774 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java @@ -0,0 +1,66 @@ +package xiaozhi.modules.agent.controller; + +import java.util.List; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.security.user.SecurityUser; + +@Tag(name = "智能体Mcp接入点管理") +@RequiredArgsConstructor +@RestController +@RequestMapping("/agent/mcp") +public class AgentMcpAccessPointController { + private final AgentMcpAccessPointService agentMcpAccessPointService; + private final AgentService agentService; + + /** + * 获取智能体的Mcp接入点地址 + * + * @param audioId 智能体id + * @return 返回错误提醒或者Mcp接入点地址 + */ + @Operation(summary = "获取智能体的Mcp接入点地址") + @GetMapping("/address/{agentId}") + @RequiresPermissions("sys:role:normal") + public Result getAgentMcpAccessAddress(@PathVariable("agentId") String agentId) { + // 获取当前用户 + UserDetail user = SecurityUser.getUser(); + + // 检查权限 + if (!agentService.checkAgentPermission(agentId, user.getId())) { + return new Result().error("没有权限查看该智能体的MCP接入点地址"); + } + String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId); + if (agentMcpAccessAddress == null) { + return new Result().ok("请联系管理员进入参数管理配置mcp接入点地址"); + } + return new Result().ok(agentMcpAccessAddress); + } + + @Operation(summary = "获取智能体的Mcp工具列表") + @GetMapping("/tools/{agentId}") + @RequiresPermissions("sys:role:normal") + public Result> getAgentMcpToolsList(@PathVariable("agentId") String agentId) { + // 获取当前用户 + UserDetail user = SecurityUser.getUser(); + + // 检查权限 + if (!agentService.checkAgentPermission(agentId, user.getId())) { + return new Result>().error("没有权限查看该智能体的MCP工具列表"); + } + List agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId); + return new Result>().ok(agentMcpToolsList); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java new file mode 100644 index 00000000..e4fac002 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcRequest.java @@ -0,0 +1,32 @@ +package xiaozhi.modules.agent.dto; + +import lombok.Data; + +/** + * MCP JSON-RPC 请求 DTO + */ +@Data +public class McpJsonRpcRequest { + private String jsonrpc = "2.0"; + private String method; + private Object params; + private Integer id; + + public McpJsonRpcRequest() { + } + + public McpJsonRpcRequest(String method) { + this.method = method; + } + + public McpJsonRpcRequest(String method, Object params, Integer id) { + this.method = method; + this.params = params; + this.id = id; + } + + public McpJsonRpcRequest(String method, Object params) { + this.method = method; + this.params = params; + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java new file mode 100644 index 00000000..1bd667e6 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/McpJsonRpcResponse.java @@ -0,0 +1,48 @@ +package xiaozhi.modules.agent.dto; + +import lombok.Data; + +/** + * MCP JSON-RPC 响应 DTO + */ +@Data +public class McpJsonRpcResponse { + private String jsonrpc = "2.0"; + private Integer id; + private McpResult result; + private McpError error; + + public McpJsonRpcResponse() { + } + + @Data + public static class McpResult { + private String type; + private String message; + private String agent_id; + private McpTool[] tools; + + public McpResult() { + } + } + + @Data + public static class McpTool { + private String name; + private String description; + private Object inputSchema; + + public McpTool() { + } + } + + @Data + public static class McpError { + private Integer code; + private String message; + private Object data; + + public McpError() { + } + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java new file mode 100644 index 00000000..336ade65 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentMcpAccessPointService.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.agent.service; + + +import java.util.List; + +/** + * 智能体Mcp接入点处理service + * + * @author zjy + */ +public interface AgentMcpAccessPointService { + /** + * 获取智能体的mcp接入点地址 + * @param id 智能体id + * @return mcp接入点地址 + */ + String getAgentMcpAccessAddress(String id); + + /** + * 获取智能体的mcp接入点已有的工具列表 + * @param id 智能体id + * @return 工具列表 + */ + List getAgentMcpToolsList(String id); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java new file mode 100644 index 00000000..e45d2201 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java @@ -0,0 +1,180 @@ +package xiaozhi.modules.agent.service.impl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.utils.AESUtils; +import xiaozhi.common.utils.HashEncryptionUtil; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.agent.dto.McpJsonRpcRequest; +import xiaozhi.modules.agent.dto.McpJsonRpcResponse; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.sys.service.SysParamsService; +import xiaozhi.modules.sys.utils.WebSocketClientManager; + +@AllArgsConstructor +@Service +@Slf4j +public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointService { + private SysParamsService sysParamsService; + + @Override + public String getAgentMcpAccessAddress(String id) { + // 获取到mcp的地址 + String url = sysParamsService.getValue(Constant.SERVER_MCP_ENDPOINT, true); + if (StringUtils.isBlank(url) || "null".equals(url)) { + return null; + } + URI uri = getURI(url); + // 获取智能体mcp的url前缀 + String agentMcpUrl = getAgentMcpUrl(uri); + // 获取密钥 + String key = getSecretKey(uri); + // 获取加密的token + String encryptToken = encryptToken(id, key); + // 对token进行URL编码 + String encodedToken = URLEncoder.encode(encryptToken, StandardCharsets.UTF_8); + // 返回智能体Mcp路径的格式 + agentMcpUrl = "%s/mcp/?token=%s".formatted(agentMcpUrl, encodedToken); + return agentMcpUrl; + } + + @Override + public List getAgentMcpToolsList(String id) { + String wsUrl = getAgentMcpAccessAddress(id); + if (StringUtils.isBlank(wsUrl)) { + return List.of(); + } + + // 将 /mcp 替换为 /call + wsUrl = wsUrl.replace("/mcp/", "/call/"); + + try { + // 创建 WebSocket 连接 + try (WebSocketClientManager client = WebSocketClientManager.build( + new WebSocketClientManager.Builder() + .uri(wsUrl) + .connectTimeout(5, TimeUnit.SECONDS) + .maxSessionDuration(20, TimeUnit.SECONDS))) { + + // 发送初始化通知 + McpJsonRpcRequest initRequest = new McpJsonRpcRequest("notifications/initialized"); + client.sendJson(initRequest); + + // 等待 0.2 秒 + Thread.sleep(200); + + // 发送工具列表请求 + McpJsonRpcRequest toolsRequest = new McpJsonRpcRequest("tools/list", null, 1); + client.sendJson(toolsRequest); + + // 监听响应,直到收到包含 id=1 的响应 + List responses = client.listener(response -> { + try { + McpJsonRpcResponse jsonResponse = JsonUtils.parseObject(response, McpJsonRpcResponse.class); + return jsonResponse != null && Integer.valueOf(1).equals(jsonResponse.getId()); + } catch (Exception e) { + log.warn("解析响应失败: {}", response, e); + return false; + } + }); + + // 处理响应 + for (String response : responses) { + try { + McpJsonRpcResponse jsonResponse = JsonUtils.parseObject(response, McpJsonRpcResponse.class); + if (jsonResponse != null && Integer.valueOf(1).equals(jsonResponse.getId()) + && jsonResponse.getResult() != null && jsonResponse.getResult().getTools() != null) { + + // 提取工具名称列表 + return java.util.Arrays.stream(jsonResponse.getResult().getTools()) + .map(McpJsonRpcResponse.McpTool::getName) + .collect(Collectors.toList()); + } + } catch (Exception e) { + log.warn("处理工具列表响应失败: {}", response, e); + } + } + + log.warn("未找到有效的工具列表响应"); + return List.of(); + + } + } catch (Exception e) { + log.error("获取智能体 MCP 工具列表失败,智能体ID: {}", id, e); + return List.of(); + } + } + + /** + * 获取URI对象 + * + * @param url 路径 + * @return URI对象 + */ + private static URI getURI(String url) { + try { + return new URI(url); + } catch (URISyntaxException e) { + log.error("路径格式不正确路径:{},\n错误信息:{}", url, e.getMessage()); + throw new RuntimeException("mcp的地址存在错误,请进入参数管理修改mcp接入点地址"); + } + } + + /** + * 获取密钥 + * + * @param uri mcp地址 + * @return 密钥 + */ + private static String getSecretKey(URI uri) { + // 获取参数 + String query = uri.getQuery(); + // 获取aes加密密钥 + String str = "key="; + return query.substring(query.indexOf(str) + str.length()); + } + + /** + * 获取智能体mcp接入点url + * + * @param uri mcp地址 + * @return 智能体mcp接入点url + */ + private String getAgentMcpUrl(URI uri) { + // 获取协议 + String wsScheme = (uri.getScheme().equals("https")) ? "wss" : "ws"; + // 获取主机,端口,路径 + String path = uri.getSchemeSpecificPart(); + // 获取到最后一个/前的path + path = path.substring(0, path.lastIndexOf("/")); + return wsScheme + ":" + path; + } + + /** + * 获取对智能体id加密的token + * + * @param agentId 智能体id + * @param key 加密密钥 + * @return 加密后token + */ + private static String encryptToken(String agentId, String key) { + // 使用md5对智能体id进行加密 + String md5 = HashEncryptionUtil.Md5hexDigest(agentId); + // aes需要加密文本 + String json = "{\"agentId\": \"%s\"}".formatted(md5); + // 加密后成token值 + return AESUtils.encrypt(key, json); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index e56cb381..34192dd8 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -1,6 +1,10 @@ package xiaozhi.modules.config.service.impl; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -15,6 +19,7 @@ import xiaozhi.common.utils.JsonUtils; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentTemplateEntity; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; @@ -39,6 +44,7 @@ public class ConfigServiceImpl implements ConfigService { private final RedisUtils redisUtils; private final TimbreService timbreService; private final AgentPluginMappingService agentPluginMappingService; + private final AgentMcpAccessPointService agentMcpAccessPointService; @Override public Object getConfig(Boolean isCache) { @@ -144,6 +150,12 @@ public class ConfigServiceImpl implements ConfigService { result.put("plugins", pluginParams); } } + // 获取mcp接入点地址 + String mcpEndpoint = agentMcpAccessPointService.getAgentMcpAccessAddress(agent.getId()); + if (StringUtils.isNotBlank(mcpEndpoint) && mcpEndpoint.startsWith("ws")) { + mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/"); + result.put("mcp_endpoint", mcpEndpoint); + } // 构建模块配置 buildModuleConfig( diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index 4860cf1c..edcc9328 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -104,6 +104,9 @@ public class SysParamsController { // 验证OTA地址 validateOtaUrl(dto.getParamCode(), dto.getParamValue()); + // 验证MCP地址 + validateMcpUrl(dto.getParamCode(), dto.getParamValue()); + sysParamsService.update(dto); configService.getConfig(false); return new Result(); @@ -195,4 +198,33 @@ public class SysParamsController { throw new RenException("OTA接口验证失败:" + e.getMessage()); } } + + private void validateMcpUrl(String paramCode, String url) { + if (!paramCode.equals(Constant.SERVER_MCP_ENDPOINT)) { + return; + } + if (StringUtils.isBlank(url) || url.equals("null")) { + throw new RenException("MCP地址不能为空"); + } + if (url.contains("localhost") || url.contains("127.0.0.1")) { + throw new RenException("MCP地址不能使用localhost或127.0.0.1"); + } + if (!url.toLowerCase().contains("key")) { + throw new RenException("不是正确的MCP地址"); + } + try { + // 发送GET请求 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + if (response.getStatusCode() != HttpStatus.OK) { + throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode()); + } + // 检查响应内容是否包含OTA相关信息 + String body = response.getBody(); + if (body == null || !body.contains("success")) { + throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口"); + } + } catch (Exception e) { + throw new RenException("MCP接口验证失败:" + e.getMessage()); + } + } } diff --git a/main/manager-api/src/main/resources/db/changelog/202506261637.sql b/main/manager-api/src/main/resources/db/changelog/202506261637.sql new file mode 100644 index 00000000..792e00c2 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202506261637.sql @@ -0,0 +1,2 @@ +delete from `sys_params` where id = 113; +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (113, 'server.mcp_endpoint', 'null', 'string', 1, 'mcp接入点地址'); \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index c527398a..63a08e11 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -225,4 +225,11 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202506251619.sql \ No newline at end of file + path: classpath:db/changelog/202506251619.sql + - changeSet: + id: 202506261637 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202506261637.sql diff --git a/main/manager-api/src/main/resources/lua/emptyAll.lua b/main/manager-api/src/main/resources/lua/emptyAll.lua index 1d984cb7..2626a894 100644 --- a/main/manager-api/src/main/resources/lua/emptyAll.lua +++ b/main/manager-api/src/main/resources/lua/emptyAll.lua @@ -1 +1 @@ -redis.call('FLUSHALL') \ No newline at end of file +redis.call('FLUSHDB') \ No newline at end of file diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 7f33d320..f6c37d7e 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -143,4 +143,34 @@ export default { }); }).send(); }, + // 获取智能体的MCP接入点地址 + getAgentMcpAccessAddress(agentId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/mcp/address/${agentId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentMcpAccessAddress(agentId, callback); + }); + }).send(); + }, + // 获取智能体的MCP工具列表 + getAgentMcpToolsList(agentId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/mcp/tools/${agentId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentMcpToolsList(agentId, callback); + }); + }).send(); + }, } diff --git a/main/manager-web/src/components/FunctionDialog.vue b/main/manager-web/src/components/FunctionDialog.vue index f001ad11..76d47005 100644 --- a/main/manager-web/src/components/FunctionDialog.vue +++ b/main/manager-web/src/components/FunctionDialog.vue @@ -101,39 +101,54 @@
-
-

MCP接入点

-
-
-
-

接入点地址

-
- 以下是智能体的MCP接入点地址。 - 查看接入点使用文档 +
+
+

MCP接入点

+
+
+ 以下是智能体的MCP接入点地址。 + 查看接入点使用文档 +
+
+ + +
- - - -
-

接入点状态

+
+

接入点状态

+
- {{ mcpStatus === 'connected' ? '已连接' : '未连接' }} + {{ + mcpStatus === 'connected' ? '已连接' : + mcpStatus === 'loading' ? '加载中...' : '未连接' + }}
+
+
+ + {{ tool }} + +
+
+ 暂无可用工具 +
+
@@ -146,6 +161,8 @@