diff --git a/README.md b/README.md
index 7a150e91..b018d5b6 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,11 @@
+
+
+
+
+
|
|
@@ -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 @@