爱折腾-创客智造实验室 - openclaw-ai助手入门教程
说明： 本文介绍了在独立服务器上通过docker方式部署OpenClaw智能助手并接入国产大模型和飞书机器人的完整流程。 主要内容包括： 1）通过docker命令行快速安装OpenClaw； 2）配置国际/国产大模型（如DeepSeek）； 3）安装飞书插件并配置应用凭证； 4）设置飞书机器人权限与事件。 5）设置nginx反向代理访问 部署完成后，用户可通过飞书与AI助手交互，支持多轮对话和上下文记忆功能。 1️⃣ 环境准备：Docker 及 Docker Compose - 安装 Docker 官方文档：[https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) - Docker Compose 一般随 Docker Engine 一同安装，验证：`docker compose version` 2️⃣ 获取 OpenClaw 源码并部署（Docker 方式） git clone https://github.com/openclaw/openclaw.git cd openclaw ./docker-setup.bash # 交互式部署脚本 2.1 交互式配置要点 步骤 问题 选择/输入 说明 2.1 理解风险，安全问题 Yes 接受安全须知 2.2 Onboarding mode QuickStart 快速安装模式 2.3 Model / auth provider skip for now 稍后手动配置 DeepSeek 2.4 Filter models by provider all provider 不影响，后期跳过 2.5 Select Channel skip for now 或 feishu 飞书稍后配置 2.6 Configure skills now No 暂不需要技能包 2.7 Hook: session-Memory 选中（空格） 启用多轮对话记忆 2.8 等待部署完成 - 记录输出的 onboarding link（内含访问令牌） 2.2 部署完成后的操作 # 停止默认启动的容器，并以守护模式重新启动 docker compose down docker compose up -d # 查看运行日志 docker compose logs -f #OpenClaw 配置文件位置（宿主机）： cat ~/.openclaw/openclaw.json # 所有配置持久化在此 3️⃣ DeepSeek 模型接入 3.1 获取 DeepSeek API 密钥 注册/登录 DeepSeek 开放平台 创建 API Key，格式：sk-xxxxxxxxxxxx ⚠️ 重要：此密钥不可公开，一旦泄露请立即删除并重新生成 3.2 编辑配置文件，添加 DeepSeek 提供商 vim ~/.openclaw/openclaw.json #在 models 节点下添加 providers 定义，并设置默认模型： json5 { "models": { "mode": "merge", "providers": { "custom-api-deepseek-com": { "baseUrl": "https://api.deepseek.com", "apiKey": "sk-你的全新密钥", // ⚠️ 替换为最新密钥 "api": "openai-completions", "models": [ { "id": "deepseek-reasoner", // 推理模型 "name": "DeepSeek Reasoner", "reasoning": true, "contextWindow": 64000, "maxTokens": 8192, "input": ["text"] }, { "id": "deepseek-chat", // 快速对话模型 "name": "DeepSeek Chat", "contextWindow": 64000, "maxTokens": 8192, "input": ["text"] } ] } } }, "agents": { "defaults": { "model": { "primary": "custom-api-deepseek-com/deepseek-reasoner" // 默认使用模型 } } } } #保存文件。 3.3 重启网关使配置生效 docker compose restart openclaw-gateway 3.4 测试模型是否接入成功 # 方法一：通过主代理测试（推荐） docker compose exec openclaw-gateway \ node dist/index.js agent --agent main --message "你好，请用一句话介绍自己" #成功标志：回复内容包含“DeepSeek”、“深度求索”等字样。 3.5 可选：更换默认模型以提升速度 docker compose exec openclaw-gateway \ node dist/index.js config set agents.defaults.model.primary custom-api-deepseek-com/deepseek-chat docker compose restart openclaw-gateway 3.6 常用诊断命令 # 整体健康检查 docker compose exec openclaw-gateway node dist/index.js doctor # 查看当前模型配置 docker compose exec openclaw-gateway node dist/index.js config get agents.defaults.model 4️⃣ 飞书机器人集成 4.1 安装飞书插件并补充依赖 # 安装插件（推荐 @xzq-xu/feishu） docker compose exec openclaw-gateway \ node dist/index.js plugins install @xzq-xu/feishu # 进入插件目录，安装生产依赖（跳过 postinstall 避免 SSH 问题） docker compose exec openclaw-gateway bash -c \ "cd /home/node/.openclaw/extensions/feishu && npm install --production --ignore-scripts" # 重启网关 docker compose restart openclaw-gateway 4.2 飞书开发者后台准备工作 登录 飞书开放平台 创建企业自建应用 → 填写名称，创建 添加能力 → 机器人（点击“添加”） 记录 App ID 和 App Secret（在“凭证与基础信息”中） 4.3 配置飞书通道凭证与策略 # 启用飞书通道 docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.enabled true # 填入凭证 docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.appId 'cli_你的AppID' docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.appSecret '你的AppSecret' docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.domain 'feishu' # 国内飞书 # 免配对策略（解决 pairing required） docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.dmPolicy 'open' docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.groupPolicy 'open' docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.requireMention true # 群聊需 @ # 性能优化：限制上下文长度 & 关闭工具调用（可选） docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.contextLimit 8000 docker compose exec openclaw-gateway \ node dist/index.js config set channels.feishu.toolsEnabled false # 重启网关 docker compose restart openclaw-gateway 4.4 飞书后台最终配置（长连接模式） 事件订阅 → 订阅方式选择 「使用长连接接收事件」 → 保存 添加事件 → 搜索 im.message.receive_v1 → 确认添加 权限管理 → 批量导入以下权限： json { "scopes": { "tenant": [ "contact:contact.base:readonly", // 获取用户姓名（解决 99991672 错误） "im:message", "im:message.group_at_msg:readonly", "im:message.p2p_msg:readonly", "im:message:send_as_bot", "im:resource" ] } } 版本管理与发布 → 创建版本 → 填写版本号、可见范围 → 提交发布 （若为企业内部应用，可直接审批） 4.5 验证飞书长连接及消息收发 # 实时查看飞书相关日志 docker compose logs -f openclaw-gateway | grep feishu 成功标志： text [feishu] connected to Feishu WebSocket [feishu] Bot identity: openclaw (ou_xxxx) 打开飞书客户端，搜索你的机器人名称 发送任意消息，应收到 DeepSeek 的智能回复 完整openclaw.json参考 $ cat ~/.openclaw/openclaw.json { "meta": { "lastTouchedVersion": "2026.2.9", "lastTouchedAt": "2026-02-11T17:06:29.081Z" }, "wizard": { "lastRunAt": "2026-02-11T14:12:20.673Z", "lastRunVersion": "2026.2.9", "lastRunCommand": "onboard", "lastRunMode": "local" }, "models": { "mode": "merge", "providers": { "custom-api-deepseek-com": { "baseUrl": "https://api.deepseek.com", "apiKey": "sk-b5687763851c4cxxxxxxxxxx", "api": "openai-completions", "models": [ { "id": "deepseek-reasoner", "name": "deepseek-reasoner (Custom Provider)", "reasoning": true, "input": [ "text" ], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 64000, "maxTokens": 8192 } ] } } }, "agents": { "defaults": { "model": { "primary": "custom-api-deepseek-com/deepseek-chat" }, "models": { "custom-api-deepseek-com/deepseek-reasoner": {} }, "workspace": "/home/node/.openclaw/workspace", "compaction": { "mode": "safeguard" }, "maxConcurrent": 4, "subagents": { "maxConcurrent": 8 } } }, "messages": { "ackReactionScope": "group-mentions" }, "commands": { "native": "auto", "nativeSkills": "auto" }, "hooks": { "internal": { "enabled": true, "entries": { "session-memory": { "enabled": true } } } }, "channels": { "feishu": { "enabled": true, "appId": "cli_a9018xxxxxxxxxx", "appSecret": "TTudUxxxxxxxxxxxxxxxxxxxxx", "domain": "feishu", "dmPolicy": "open", "groupPolicy": "open" } }, "gateway": { "port": 18789, "mode": "local", "bind": "loopback", "auth": { "mode": "token", "token": "6746d7bb7fd9xxxxxxxxxxxxxxxxxxxxxxxxxx" }, "trustedProxies": [ "172.21.0.x/32", "183.2.xxx.xxx" ], "tailscale": { "mode": "off", "resetOnExit": false }, "nodes": { "denyCommands": [ "camera.snap", "camera.clip", "screen.record", "calendar.add", "contacts.add", "reminders.add" ] } }, "plugins": { "entries": { "feishu": { "enabled": true } } } }
5️⃣ 配置反向代理（使 Web 管理端可通过 HTTPS 访问） 5.1 准备域名及 SSL 证书 申请域名（如 openclaw.yourdomain.com），解析到服务器公网 IP 获取 SSL 证书（可自签或使用 Let's Encrypt） 5.2 安装 Nginx 并配置反向代理 nginx配置 server { listen 443 ssl; server_name openclaw.yourdomain.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://127.0.0.1:18789; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 5.3 配置 OpenClaw 信任代理 IP 编辑 ~/.openclaw/openclaw.json，在 gateway 节点添加： json5 "gateway": { "trustedProxies": ["172.21.0.1/32", "你的代理服务器公网IP"], // ... 其他配置 } #重启网关： docker compose restart openclaw-gateway 5.4 配置自动设备配对策略 #1.访问网站： https://openclaw.yourdomain.com 在Overview->Gateway Token输入框填写生成时候的Token -> 点击connect #2.手动批准： # 查看待批准设备，查看request的ID docker compose exec openclaw-gateway node dist/index.js devices list # 批准指定请求 docker compose exec openclaw-gateway node dist/index.js devices approve <请求ID> #3.刷新网页，就可以看到已经批准了 6️⃣ 性能优化与常见问题 ⚡ 响应速度优化（针对 DeepSeek 回复慢） 问题 解决方案 命令/配置 模型推理慢 换用 deepseek-chat config set agents.defaults.model.primary custom-api-deepseek-com/deepseek-chat 工具调用超时 关闭飞书通道工具 config set channels.feishu.toolsEnabled false 上下文过大 限制飞书上下文 config set channels.feishu.contextLimit 8000 网络延迟 使用国内 DNS 容器内 echo "nameserver 114.114.114.114" > /etc/resolv.conf ❗ 安全红线 DeepSeek API Key 一旦泄露，立即删除并重新生成 配置文件权限收紧： chmod 700 ~/.openclaw chmod 600 ~/.openclaw/openclaw.json #不要在公共文档、日志中明文暴露任何密钥 常用诊断命令汇总 # 整体健康检查 docker compose exec openclaw-gateway node dist/index.js doctor # 查看当前模型配置 docker compose exec openclaw-gateway node dist/index.js config get agents.defaults.model # 查看飞书通道配置 docker compose exec openclaw-gateway node dist/index.js config get channels.feishu # 实时跟踪飞书消息 docker compose logs -f openclaw-gateway | grep -E "feishu|agent" 总结 通过以上步骤，你已成功完成： ✅ Docker 部署 OpenClaw ✅ 接入 DeepSeek（支持 reasoner 和 chat 双模型） ✅ 集成飞书机器人（长连接，免配对） ✅ 配置 HTTPS 反向代理与设备自动批准 现在，你的企业飞书智能助手已正式上线！如有其他问题，欢迎查阅 OpenClaw 官方文档 或社区讨论。
说明： 当任务完成使用指定的邮箱通知 实现方式：使用openclaw的开发能力来编程生成一个自己使用的python脚本即可 步骤1-获取qq邮箱授权码： 登录邮箱并进入设置：在电脑浏览器里打开QQ邮箱官网 (mail.qq.com)，登录后，点击页面顶部的 “设置” 链接，然后选择 “帐户” 选项卡。 找到并开启服务：在“帐户”页面往下拉，找到 “POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务” 这一栏。点击任一服务（比如 “IMAP/SMTP服务”）右侧的 “开启” 按钮。 完成安全验证：这时会弹出一个窗口，要求你验证身份。根据提示，用你绑定该邮箱的手机号发送一条指定短信到指定号码。发送完成后，在电脑上点击 “我已发送” 或类似的确认按钮。 获取并保存授权码：验证通过后，系统就会弹出一串16位的授权码。请务必马上复制并把它保存好（比如记在备忘录里或截图保存），因为这个弹窗关掉后就再也看不到了。如果在第三方客户端设置时还需要，只能重新生成 步骤2-生成发送邮件脚本和配置： 确认邮箱和授权码后，使用openclaw的开发能力来编程生成一个自己使用的python脚本： 使用web-ui的chat窗口，输入如下的参考python脚本，邮箱和授权码： #!/usr/bin/env python3 """ OpenClaw邮件发送系统 支持环境变量配置，敏感信息存储在.env文件中 """ import smtplib import ssl from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import argparse import os import sys from pathlib import Path def load_env(): """加载环境变量""" env_path = Path(__file__).parent / '.env' # 如果.env文件存在，读取它 if env_path.exists(): with open(env_path, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): key, value = line.split('=', 1) os.environ[key.strip()] = value.strip() # 返回配置字典 config = { 'qq_email': os.getenv('QQ_EMAIL', ''), 'qq_password': os.getenv('QQ_EMAIL_PASSWORD', ''), 'smtp_server': os.getenv('SMTP_SERVER', 'smtp.qq.com'), 'smtp_port': int(os.getenv('SMTP_PORT', '587')), 'default_recipient': os.getenv('DEFAULT_RECIPIENT', ''), 'use_tls': os.getenv('USE_TLS', 'true').lower() == 'true', 'use_ssl': os.getenv('USE_SSL', 'false').lower() == 'true', } return config def send_email(to_email, subject, body, from_email=None, password=None, config=None): """ 通过QQ邮箱SMTP发送邮件 Args: to_email: 收件人邮箱 subject: 邮件主题 body: 邮件内容 from_email: 发件人邮箱（可选） password: QQ邮箱授权码（可选） config: 配置字典（可选） Returns: bool: 发送是否成功 """ if config is None: config = load_env() # 使用配置中的值 smtp_server = config['smtp_server'] smtp_port = config['smtp_port'] use_tls = config['use_tls'] use_ssl = config['use_ssl'] # 如果没有提供发件人邮箱，使用配置中的值 if not from_email: from_email = config['qq_email'] # 如果没有提供密码，使用配置中的值 if not password: password = config['qq_password'] # 验证配置 if not from_email: print("错误：未设置发件人邮箱") print(" 请在.env文件中设置QQ_EMAIL环境变量") return False if not password: print("错误：未设置QQ邮箱授权码") print(" 请在.env文件中设置QQ_EMAIL_PASSWORD环境变量") return False # 创建邮件 message = MIMEMultipart("alternative") message["Subject"] = subject message["From"] = from_email message["To"] = to_email # 添加纯文本和HTML版本 text_part = MIMEText(body, "plain", "utf-8") html_part = MIMEText(f""" {subject} {body} 此邮件由OpenClaw邮件系统自动发送 发送时间: {os.environ.get('SEND_TIME', '')} """, "html", "utf-8") message.attach(text_part) message.attach(html_part) try: # 设置发送时间环境变量（用于HTML模板） os.environ['SEND_TIME'] = os.popen('date').read().strip() print(f"正在发送邮件...") print(f" SMTP服务器: {smtp_server}:{smtp_port}") print(f" 发件人: {from_email}") print(f" 收件人: {to_email}") print(f" 主题: {subject}") # 创建安全连接 context = ssl.create_default_context() if use_ssl: # SSL连接 with smtplib.SMTP_SSL(smtp_server, smtp_port, context=context) as server: server.login(from_email, password) server.sendmail(from_email, to_email, message.as_string()) elif use_tls: # TLS连接 with smtplib.SMTP(smtp_server, smtp_port) as server: server.starttls(context=context) # 启用TLS加密 server.login(from_email, password) server.sendmail(from_email, to_email, message.as_string()) else: # 非加密连接（不推荐） with smtplib.SMTP(smtp_server, smtp_port) as server: server.login(from_email, password) server.sendmail(from_email, to_email, message.as_string()) print(f"邮件发送成功！") return True except smtplib.SMTPAuthenticationError as e: print(f"认证失败: {e}") print(" 请检查QQ邮箱和授权码是否正确") print(" 注意：需要使用授权码而非登录密码") return False except Exception as e: print(f"邮件发送失败: {e}") return False def main(): parser = argparse.ArgumentParser( description="OpenClaw邮件发送系统", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: %(prog)s --to recipient@example.com --subject "测试" --body "内容" %(prog)s --to default --subject "提醒" --body "记得开会" # 使用默认收件人 环境变量配置: 请创建.env文件并设置以下变量: QQ_EMAIL=your_email@qq.com QQ_EMAIL_PASSWORD=your_authorization_code DEFAULT_RECIPIENT=default_recipient@qq.com """ ) parser.add_argument("--to", required=True, help="收件人邮箱（或使用'default'使用默认收件人）") parser.add_argument("--subject", required=True, help="邮件主题") parser.add_argument("--body", required=True, help="邮件内容") parser.add_argument("--from", dest="from_email", help="发件人邮箱（默认使用QQ_EMAIL环境变量）") parser.add_argument("--password", help="QQ邮箱授权码（默认使用QQ_EMAIL_PASSWORD环境变量）") parser.add_argument("--config", help="配置文件路径（默认使用当前目录下的.env文件）") args = parser.parse_args() # 加载配置 config = load_env() # 处理收件人 to_email = args.to if to_email.lower() == 'default': to_email = config['default_recipient'] if not to_email: print("错误：未设置默认收件人") print("请在.env文件中设置DEFAULT_RECIPIENT环境变量") sys.exit(1) print(f"使用默认收件人: {to_email}") success = send_email( to_email=to_email, subject=args.subject, body=args.body, from_email=args.from_email, password=args.password, config=config ) sys.exit(0 if success else 1) if __name__ == "__main__": main() openclaw应该会自动根据这个参考脚本，邮箱和授权码，自动帮你配置好这个邮箱并给出测试报告 还可以配置一个定时提醒，比如在web-ui的chat窗口，发指令：增加每日9点的起床提醒推送的邮箱 总结： openclaw的自动化的确是目前看到最好的，能自动完成很多任务，后面再继续探索。
说明 什么是openclaw-mcp-adapter？ 在深入配置之前，我们先理解一下openclaw-mcp-adapter是什么。 它是OpenClaw的一个插件，作用是把MCP服务器的工具转换为OpenClaw原生Agent工具。 简单来说，以前你要通过命令行调用MCP服务器，现在只要配置好mcp-adapter，OpenClaw启动时就会自动连接MCP服务器、发现所有工具，并注册成AI可以直接调用的“一等公民”工具。比如你配置了一个计算器MCP，OpenClaw里就会出现calculator工具，AI可以直接使用。 githu地址 https://github.com/androidStern-personal/openclaw-mcp-adapter mcp-adapter的核心优势： 自动发现工具：连接服务器后自动调用listTools()获取所有工具 原生集成：工具名称带服务器前缀（如myserver_toolname），避免命名冲突 自动重连：连接断开后下次调用自动重连 目前这个插件适合http和stdio的mcp的插件 方法1-让openclaw自动安装: 测试web-ui的chat发指令： 安装openclaw-mcp-adapter插件，https://github.com/androidStern-personal/openclaw-mcp-adapter。 让openclaw自己想办法实现安装 方法2-命令安装 我的openclaw系统基于docker部署 cd openclaw docker compose exec openclaw-gateway node dist/index.js plugins install mcp-adapter 方法3-源码安装 cd openclaw docker compose exec openclaw-gateway /bin/bash git clone https://github.com/androidStern/openclaw-mcp-adapter.git node dist/index.js plugins install ./openclaw-mcp-adapter/ 配置openclaw-mcp-adapter 选择一个mcp服务器，比如http://mcp-porter:8000/sse 打开openclaw.json，配置如下： vim ~/.openclaw/openclaw.json "plugins": { "entries": { "openclaw-mcp-adapter": { "enabled": true, "config": { "servers": [ { "name": "mcp-porter-calculator", "transport": "http", "url": "http://mcp-porter:8000/sse" } ], "toolPrefix": true } } } } 测试： web-ui的chat窗口，发送调用相应mcp的工具即可。
说明: mcp-endpoint-server是什么？ 在开始部署之前，我们先理解一下mcp-endpoint-server在整个架构中的位置。 mcp-endpoint-server是一个基于WebSocket的MCP注册中心，由xinnan-tech团队开发，主要作用是作为小智AI设备与MCP工具之间的桥梁。它的核心设计理念是：让小智设备端能够通过语音调用各种MCP服务。 为什么需要它？ 如果你接触过小智AI，你可能知道小智设备端需要通过WebSocket连接到小智官方服务器。而mcp-endpoint-server提供了一个统一的接入点，设备端可以连接到这里，然后通过它调用各种注册的MCP工具（如计算器、高德地图、Home Assistant等）。 它的优势在于： 高性能：基于FastAPI和WebSocket的异步架构，支持高并发连接 连接管理：智能管理WebSocket连接，支持连接清理 监控统计：提供连接统计和健康检查接口 消息转发：自动转发工具端和小智端之间的消息 配置灵活：支持配置文件管理，易于部署和维护 简单来说，你部署好mcp-endpoint-server后，小智设备端就可以连接上来，然后通过它调用你本地的MCP服务（比如我们之前配置的计算器）。而且你的MCP服务无需部署在公网环境，这极大方便了开发者在各种网络环境下的使用。 工作流程： 设备端：小智设备（或运行mcp_pipe.py的机器）作为WebSocket客户端，主动连接到mcp-endpoint-server 注册工具：设备端将自己本地的MCP服务（如计算器）通过stdio管道与mcp_pipe.py连接，mcp_pipe.py负责将WebSocket消息转换为stdio通信 服务端管理：mcp-endpoint-server管理所有设备的WebSocket连接，并转发来自小智官方服务器的请求 调用链路：小智官方服务器 → mcp-endpoint-server → 设备端 → 本地MCP服务 这种设计的巧妙之处在于：设备端的MCP服务可以运行在内网，不需要公网IP，只需要设备端能主动连接到公网的mcp-endpoint-server即可 docker方式安装mcp-endpoint-server： 部署教程1, 部署教程2 采用单模块部署的安装方法： git clone https://github.com/xinnan-tech/mcp-endpoint-server.git # 进入本项目源码根目录 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 250705 INFO-=====下面的地址分别是智控台/单模块MCP接入点地址==== 250705 INFO-智控台MCP参数配置: http://172.22.0.2:8004/mcp_endpoint/health?key=abc 250705 INFO-单模块部署MCP接入点: ws://172.22.0.2:8004/mcp_endpoint/mcp/?token=def 250705 INFO-=====请根据具体部署选择使用，请勿泄露给任何人====== 假设你的本机地址是127.0.0.1 如上，输出mcp接入点是中ws://127.0.0.1:8004/mcp_endpoint/mcp/?token=def就是你的MCP接入点地址。 这个MCP接入点地址很重要，你等一下会用到 简单验证 curl http://127.0.0.1:8004/health 部署一个计算器验证完整功能： 部署虾哥写的计算器项目,安装 https://github.com/78/mcp-calculator # 进入项目目录 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 建立执行脚本： 使用自己正确的token测试 export MCP_ENDPOINT=ws://127.0.0.1:8004/mcp_endpoint/mcp/?token=abc python mcp_pipe.py calculator.py 启动后mcp-endpoint-server日志会出现类似这样的内容， 250705 -INFO-正在初始化MCP接入点: wss://2662r3426b.vicp.fun/mcp_e 250705 -INFO-发送MCP接入点初始化消息 250705 -INFO-MCP接入点连接成功 250705 -INFO-MCP接入点初始化成功 250705 -INFO-统一工具处理器初始化完成 250705 -INFO-MCP接入点服务器信息: name=Calculator, version=1.9.4 250705 -INFO-MCP接入点支持的工具数量: 1 250705 -INFO-所有MCP接入点工具已获取，客户端准备就绪 250705 -INFO-工具缓存已刷新 250705 -INFO-当前支持的函数列表: [ 'get_time', 'get_lunar', 'play_music', 'get_weather', 'handle_exit_intent', 'calculator'] 建立测试脚本进行计算 建立脚本：vim robot_example_cal.py 脚本内容： #!/usr/bin/env python3 """ MCP Robot Client 示例 适配 xinnan-tech/mcp-endpoint-server + mcp-calculator 支持 JSON-RPC initialize / tools/list / tools/call """ import asyncio import json import base64 import argparse import urllib.parse from Crypto.Cipher import AES from Crypto.Util.Padding import pad import websockets # ==================== Token 生成 ==================== def pad_key(key: str) -> bytes: key_bytes = key.encode("utf-8") if len(key_bytes) in (16, 24, 32): return key_bytes padded = bytearray(32) padded[:min(len(key_bytes), 32)] = key_bytes[:32] return bytes(padded) def encrypt_token(key: str, agent_id: str) -> str: plain_text = json.dumps({"agentId": agent_id}, ensure_ascii=False) key_bytes = pad_key(key) cipher = AES.new(key_bytes, AES.MODE_ECB) padded_data = pad(plain_text.encode("utf-8"), AES.block_size) encrypted_bytes = cipher.encrypt(padded_data) return base64.b64encode(encrypted_bytes).decode("ascii") # ==================== MCP Client ==================== class MCPRobotClient: def __init__(self, uri): self.uri = uri self.request_id = 0 def next_id(self): self.request_id += 1 return self.request_id async def send_request(self, websocket, method, params=None): req_id = self.next_id() request = { "jsonrpc": "2.0", "id": req_id, "method": method, "params": params or {} } await websocket.send(json.dumps(request, ensure_ascii=False)) print(f"发送: {request}") return req_id async def receive_loop(self, websocket): try: async for message in websocket: data = json.loads(message) print(f"收到: {data}") except websockets.ConnectionClosed: print("WebSocket 连接关闭") async def run(self): async with websockets.connect(self.uri) as websocket: print(f"已连接 {self.uri}") # 启动接收任务 receive_task = asyncio.create_task(self.receive_loop(websocket)) # initialize await self.send_request(websocket, "initialize", { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "robot-example", "version": "1.0" } }) await asyncio.sleep(0.5) # tools/list await self.send_request(websocket, "tools/list", {}) await asyncio.sleep(0.5) # 调用 mcp-calculator await self.send_request(websocket, "tools/call", { "name": "calculator", "arguments": { "python_expression": "10 + 5 * 2" } }) # 等待一些响应 await asyncio.sleep(5) receive_task.cancel() # ==================== CLI ==================== if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--host", default="127.0.0.1", help="服务器地址") parser.add_argument("--port", type=int, default=8004, help="服务器端口") parser.add_argument("--agent-id", default="robot-client", help="Agent ID") parser.add_argument("--key", help="AES 密钥（用于生成 token）") parser.add_argument("--token", help="直接指定完整 token（优先级高于 --key）") args = parser.parse_args() # ==================== # Token 解析逻辑 # ==================== if args.token: token = args.token print("使用直接提供的 token") uri = f"ws://{args.host}:{args.port}/mcp_endpoint/call/?token={token}" elif args.key: token = encrypt_token(args.key, args.agent_id) print(f"使用 key 生成 token: {token}") encoded_token = urllib.parse.quote(token) uri = f"ws://{args.host}:{args.port}/mcp_endpoint/call/?token={encoded_token}" else: print("必须提供 --token 或 --key") exit(1) print(f"连接地址: {uri}") client = MCPRobotClient(uri) asyncio.run(client.run()) 运行脚本 python robot_example_cal.py --token "def=" 使用自己正确的token测试，效果如下 使用直接提供的 token 连接地址: ws://127.0.0.1:8004/mcp_endpoint/call/?token=def= 已连接 ws://127.0.0.1:8004/mcp_endpoint/call/?token=def= 发送: {'jsonrpc': '2.0', 'id': 1, 'method': 'initialize', 'params': {'protocolVersion': '2024-11-05', 'capabilities': {}, 'clientInfo': {'name': 'robot-example', 'version': '1.0'}}} 收到: {'jsonrpc': '2.0', 'id': 1, 'result': {'protocolVersion': '2024-11-05', 'capabilities': {'experimental': {}, 'prompts': {'listChanged': False}, 'resources': {'subscribe': False, 'listChanged': False}, 'tools': {'listChanged': True}, 'tasks': {'list': {}, 'cancel': {}, 'requests': {'tools': {'call': {}}, 'prompts': {'get': {}}, 'resources': {'read': {}}}}}, 'serverInfo': {'name': 'Calculator', 'version': '2.14.5'}}} 发送: {'jsonrpc': '2.0', 'id': 2, 'method': 'tools/list', 'params': {}} 收到: {'jsonrpc': '2.0', 'id': 2, 'result': {'tools': [{'name': 'calculator', 'description': "For mathamatical calculation, always use this tool to calculate the result of a python expression. You can use 'math' or 'random' directly, without 'import'.", 'inputSchema': {'properties': {'python_expression': {'type': 'string'}}, 'required': ['python_expression'], 'type': 'object'}, 'outputSchema': {'additionalProperties': True, 'type': 'object'}, '_meta': {'_fastmcp': {'tags': []}}}]}} 发送: {'jsonrpc': '2.0', 'id': 3, 'method': 'tools/call', 'params': {'name': 'calculator', 'arguments': {'python_expression': '10 + 5 * 2'}}} 收到: {'jsonrpc': '2.0', 'id': 3, 'result': {'content': [{'type': 'text', 'text': '{"success":true,"result":20}'}], 'structuredContent': {'success': True, 'result': 20}, 'isError': False}} 看到这样的信息，说明部署正确
说明： 在前面的教程中，我们分别学习了如何配置MCP-adapter来直接调用MCP服务，以及如何部署小智语音的mcp-endpoint-server。现在，我们将两者结合，实现一个更强大的架构：通过OpenClaw的MCP-adapter，经由mcp-endpoint-server调用内网或设备端的MCP工具。 这个架构解决了什么问题？当你的MCP服务运行在内网或设备端（如树莓派、NAS）时，OpenClaw无法直接连接。而mcp-endpoint-server作为公网WebSocket服务器，可以让内网设备主动连接并注册工具。通过一个协议转换代理，OpenClaw就能通过标准的SSE/HTTP协议调用这些工具。整个过程无需公网IP，无需复杂的端口映射。 架构： subgraph 公网云服务器 A[mcp-endpoint-server WebSocket服务端] B[ws2sse代理 WebSocket → SSE] end subgraph 内网/设备端 C[mcp_pipe.py + 本地MCP服务] end subgraph OpenClaw环境 D[openclaw-mcp-adapter] end C -- 主动WebSocket连接 --> A B -- 连接 --> A D -- SSE --> B 工作流程： 内网设备通过mcp_pipe.py将本地MCP服务（如计算器）注册到公网的mcp-endpoint-server。 我们在公网部署一个ws2sse代理，它作为WebSocket客户端连接mcp-endpoint-server，同时对外提供SSE端点。 OpenClaw的mcp-adapter插件通过SSE连接ws2sse代理，发现并注册所有可用的MCP工具。 用户在OpenClaw中通过语音或文字调用工具，请求经mcp-adapter → ws2sse代理 → mcp-endpoint-server → 内网设备，最终执行并返回结果。 openclaw的插件mcp-adapter不能直接通过websocket访问mcp-endpoint-server，所以要通过一个ws2sse代理来做中转。 在这里我们建立一个mcp-porter-server的ws2sse代理，使用docker部署在openclaw同一个服务器 1.建立Dockerfile FROM python:3.12-slim WORKDIR /app # 复制依赖文件并安装（仅当 requirements.txt 变化时才会重新安装） COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制当前目录所有文件到镜像（会被卷挂载覆盖，但用于构建时保证镜像完整性） COPY . . # 容器启动命令 CMD ["python", "ws2sse_proxy.py"] 2.建立docker-compose.yml networks: mcp-shared-network: external: true name: mcp-shared-network services: mcp-porter: build: . # 使用当前目录的 Dockerfile 构建 container_name: mcp-porter restart: always networks: - mcp-shared-network ports: - "8000:8000" volumes: - ./:/app # 将宿主机当前目录挂载到容器 /app，覆盖代码 environment: - TZ=Asia/Shanghai - REMOTE_WS_URL=ws://mcp-endpoint-server:8004/mcp_endpoint/call/?token=${MCP_TOKEN} # 不再直接写 token，使用 .env 文件 建立requirements.txt fastapi==0.104.1 uvicorn[standard]==0.24.0 websockets==12.0 python-multipart==0.0.6 pydantic==2.5.0 python-dotenv==1.0.0 configparser==6.0.0 asyncio-mqtt==0.16.1 pycryptodome==3.23.0 loguru==0.7.2 requests 建立ws2sse_proxy.py #!/usr/bin/env python3 """ MCP WebSocket 转 SSE 代理（完整版） - 作为客户端连接到远程 WebSocket MCP 服务器 - 使用独立接收循环处理消息，支持并发请求 - 对外提供 SSE 端点供 OpenClaw 连接 """ import asyncio import json import os import logging from contextlib import asynccontextmanager from typing import Dict, Optional import websockets from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse, JSONResponse import uvicorn # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger("mcp-proxy") # 从环境变量读取远程 WebSocket 地址 REMOTE_WS_URL = os.getenv( "REMOTE_WS_URL", "ws://127.0.0.1:8004/mcp_endpoint/call/?token=2svzrombSJCGKEdlm6znodXSkJlwtfDcjsBV9mfNLtA=" ) # 全局状态 remote_ws: Optional[websockets.WebSocketClientProtocol] = None remote_tools: list = [] pending_requests: Dict[int, asyncio.Future] = {} connection_lock = asyncio.Lock() receive_task: Optional[asyncio.Task] = None async def _receive_loop(): """后台接收远程 WebSocket 消息，根据 id 分发结果""" global remote_ws, pending_requests # 必须放在最前面 try: async for message in remote_ws: data = json.loads(message) logger.debug(f"Received from remote: {data}") req_id = data.get("id") if req_id is not None and req_id in pending_requests: future = pending_requests.pop(req_id) future.set_result(data) else: logger.info(f"Ignored message (no matching request): {data}") except websockets.exceptions.ConnectionClosed as e: logger.warning(f"Remote connection closed: {e}") # 取消所有等待的请求 for req_id, future in pending_requests.items(): future.set_exception(Exception("Remote connection closed")) pending_requests.clear() remote_ws = None except Exception as e: logger.error(f"Receive loop error: {e}", exc_info=True) async def connect_to_remote(): """连接到远程 WebSocket 服务器并完成 MCP 握手""" global remote_ws, remote_tools, receive_task async with connection_lock: if remote_ws and not remote_ws.closed: return logger.info(f"Connecting to remote server: {REMOTE_WS_URL}") try: remote_ws = await websockets.connect(REMOTE_WS_URL) logger.info("WebSocket connected, starting MCP handshake...") # 1. 发送 initialize 请求 init_req = { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "mcp-ws-proxy", "version": "2.0"} } } await remote_ws.send(json.dumps(init_req)) # 手动等待初始化响应（因为接收循环尚未启动） init_resp = json.loads(await remote_ws.recv()) logger.info(f"Initialize response: {init_resp}") # 2. 发送 initialized 通知 notif = {"jsonrpc": "2.0", "method": "notifications/initialized"} await remote_ws.send(json.dumps(notif)) # 3. 请求工具列表（同样手动等待） tools_req = {"jsonrpc": "2.0", "id": 2, "method": "tools/list"} await remote_ws.send(json.dumps(tools_req)) tools_resp = json.loads(await remote_ws.recv()) remote_tools = tools_resp.get("result", {}).get("tools", []) logger.info(f"Discovered {len(remote_tools)} tools: {[t['name'] for t in remote_tools]}") # 启动后台接收循环 receive_task = asyncio.create_task(_receive_loop()) except Exception as e: logger.error(f"Failed to connect to remote server: {e}") remote_ws = None raise async def forward_request_to_remote(request_body: dict) -> dict: """将请求转发给远程服务器，并等待响应""" global remote_ws, pending_requests if not remote_ws or remote_ws.closed: await connect_to_remote() req_id = request_body.get("id") if req_id is None: raise ValueError("Request missing 'id' field") # 创建 Future 并存入字典 loop = asyncio.get_event_loop() future = loop.create_future() pending_requests[req_id] = future try: await remote_ws.send(json.dumps(request_body)) logger.debug(f"Sent to remote: {request_body}") # 等待响应，设置超时 response = await asyncio.wait_for(future, timeout=30.0) return response except asyncio.TimeoutError: pending_requests.pop(req_id, None) raise Exception(f"Request {req_id} timed out") except Exception as e: pending_requests.pop(req_id, None) raise e @asynccontextmanager async def lifespan(app: FastAPI): """应用启动时连接远程服务器""" try: await connect_to_remote() except Exception as e: logger.error(f"Startup connection failed: {e}") yield if receive_task: receive_task.cancel() if remote_ws and not remote_ws.closed: await remote_ws.close() app = FastAPI(lifespan=lifespan) @app.get("/sse") async def sse_endpoint(request: Request): """SSE 端点，用于 OpenClaw 连接""" async def event_generator(): # 发送初始化事件（符合 MCP over SSE 规范） init_event = { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": {}, "serverInfo": {"name": "mcp-ws-proxy", "version": "2.0"} } } yield f"data: {json.dumps(init_event)}\n\n" # 保持连接，发送心跳 while not await request.is_disconnected(): await asyncio.sleep(1) yield ": heartbeat\n\n" return StreamingResponse(event_generator(), media_type="text/event-stream") @app.post("/sse") async def sse_post(request: Request): """处理来自 OpenClaw 的 JSON-RPC 请求""" body = await request.json() print("=== 收到请求体 ===") # 添加直接打印 print(json.dumps(body, indent=2)) # 格式化打印 logger.info(f"Received request: {body}") method = body.get("method") req_id = body.get("id") # tools/list 直接返回缓存的工具列表 if method == "tools/list": return JSONResponse({ "jsonrpc": "2.0", "id": req_id, "result": {"tools": remote_tools} }) # initialize 返回本地信息 if method == "initialize": return JSONResponse({ "jsonrpc": "2.0", "id": req_id, "result": { "protocolVersion": "2024-11-05", "capabilities": {}, "serverInfo": {"name": "mcp-ws-proxy", "version": "2.0"} } }) # 其他请求（如 tools/call）转发给远程服务器 try: resp = await forward_request_to_remote(body) return JSONResponse(resp) except Exception as e: logger.error(f"Forwarding failed: {e}", exc_info=True) return JSONResponse({ "jsonrpc": "2.0", "id": req_id, "error": {"code": -32000, "message": str(e)} }, status_code=502) @app.get("/health") async def health(): """健康检查""" return { "status": "ok", "remote_connected": remote_ws is not None and not remote_ws.closed, "tools_count": len(remote_tools) } if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) 构建docker docker compose build mcp-porter 启动docker docker compose mcp-porter up -d 验证是否能通过mcp-porter-server来访问计算工具 构建测试脚本test_proxy.py #!/usr/bin/env python3 """ 增强版 MCP 代理测试脚本 - 动态获取工具参数 schema，自动构造合适的测试参数 - 支持任意工具，无需硬编码参数名 - 提供详细的请求/响应日志 """ import json import requests import argparse import sys from typing import Dict, Any def send_jsonrpc(url: str, method: str, params: Dict = None, req_id: int = 1) -> Dict: """发送 JSON-RPC 请求并返回响应""" payload = { "jsonrpc": "2.0", "id": req_id, "method": method } if params is not None: payload["params"] = params headers = {"Content-Type": "application/json"} print(f"\n>>> 发送请求 [{req_id}]: {method}") print(json.dumps(payload, indent=2, ensure_ascii=False)) try: response = requests.post(url, json=payload, headers=headers, timeout=10) response.raise_for_status() resp_json = response.json() print(f"<<< 收到响应:") print(json.dumps(resp_json, indent=2, ensure_ascii=False)) return resp_json except requests.exceptions.RequestException as e: print(f" 请求失败: {e}") if hasattr(e, 'response') and e.response is not None: print(f"响应状态码: {e.response.status_code}") print(f"响应内容: {e.response.text}") sys.exit(1) except json.JSONDecodeError: print(f" 响应不是有效的 JSON: {response.text}") sys.exit(1) def generate_test_arguments(schema: Dict) -> Dict: """ 根据工具的 inputSchema 生成测试参数 schema 格式示例: { "type": "object", "properties": { "python_expression": {"type": "string", "description": "..."} }, "required": ["python_expression"] } """ if not schema: return {} properties = schema.get("properties", {}) required = schema.get("required", []) args = {} for field in required: prop = properties.get(field, {}) prop_type = prop.get("type", "string") # 根据类型生成合适的测试值 if prop_type == "number": args[field] = 10 # 示例数字 elif prop_type == "integer": args[field] = 5 elif prop_type == "boolean": args[field] = True elif prop_type == "array": args[field] = [] # 空数组 elif prop_type == "object": args[field] = {} # 空对象 else: # 默认字符串 args[field] = "test" if field != "python_expression" else "10 + 5" # 如果没有 required 字段，尝试取第一个属性作为示例 if not required and properties: field = next(iter(properties)) prop = properties[field] prop_type = prop.get("type", "string") if prop_type == "number": args[field] = 10 elif prop_type == "integer": args[field] = 5 else: args[field] = "example" return args def main(): parser = argparse.ArgumentParser(description="增强版 MCP 代理测试") parser.add_argument("--url", default="http://127.0.0.1:8000/sse", help="代理的 SSE 端点 URL (默认: http://127.0.0.1:8000/sse)") parser.add_argument("--tool", help="指定要测试的工具名称，不指定则使用第一个工具") args = parser.parse_args() print(f" 测试代理: {args.url}") # 1. 获取工具列表 print("\n 正在获取工具列表...") tools_resp = send_jsonrpc(args.url, "tools/list", req_id=1) if "error" in tools_resp: print(f" 获取工具列表失败: {tools_resp['error']}") sys.exit(1) tools = tools_resp.get("result", {}).get("tools", []) if not tools: print(" 代理未返回任何工具") sys.exit(0) print(f" 发现 {len(tools)} 个工具:") for idx, tool in enumerate(tools, 1): name = tool.get("name", "未知") desc = tool.get("description", "") print(f" {idx}. {name}: {desc}") # 2. 选择要测试的工具 target_tool = None if args.tool: for tool in tools: if tool.get("name") == args.tool: target_tool = tool break if not target_tool: print(f" 未找到指定工具: {args.tool}") sys.exit(1) else: # 默认选择第一个工具 target_tool = tools[0] print(f"\n 未指定工具，将使用第一个工具: {target_tool['name']}") tool_name = target_tool["name"] tool_schema = target_tool.get("inputSchema", {}) print(f"\n 工具 '{tool_name}' 的输入 schema:") print(json.dumps(tool_schema, indent=2, ensure_ascii=False)) # 3. 生成测试参数 test_args = generate_test_arguments(tool_schema) print(f"\n 生成的测试参数: {test_args}") # 4. 调用工具 params = {"name": tool_name, "arguments": test_args} call_resp = send_jsonrpc(args.url, "tools/call", params, req_id=2) if "error" in call_resp: print(f"\n 调用失败: {call_resp['error']}") sys.exit(1) result = call_resp.get("result", {}) print(f"\n 调用成功，原始结果:") print(json.dumps(result, indent=2, ensure_ascii=False)) # 尝试提取文本内容（如果存在） content = result.get("content", []) for item in content: if item.get("type") == "text": print(f"\n 计算结果文本: {item['text']}") if __name__ == "__main__": main() 测试运行 python3 test_proxy.py 效果 $ ./run_test.sh 测试代理: http://127.0.0.1:8000/sse 正在获取工具列表... >>> 发送请求 [1]: tools/list { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } <<< 收到响应: { "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "calculator", "description": "For mathamatical calculation, always use this tool to calculate the result of a python expression. You can use 'math' or 'random' directly, without 'import'.", "inputSchema": { "properties": { "python_expression": { "type": "string" } }, "required": [ "python_expression" ], "type": "object" }, "outputSchema": { "additionalProperties": true, "type": "object" }, "_meta": { "_fastmcp": { "tags": [] } } } ] } } 发现 1 个工具: 1. calculator: For mathamatical calculation, always use this tool to calculate the result of a python expression. You can use 'math' or 'random' directly, without 'import'. 未指定工具，将使用第一个工具: calculator 工具 'calculator' 的输入 schema: { "properties": { "python_expression": { "type": "string" } }, "required": [ "python_expression" ], "type": "object" } 生成的测试参数: {'python_expression': '10 + 5'} >>> 发送请求 [2]: tools/call { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "calculator", "arguments": { "python_expression": "10 + 5" } } } <<< 收到响应: { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "{\"success\":true,\"result\":15}" } ], "structuredContent": { "success": true, "result": 15 }, "isError": false } } 调用成功，原始结果: { "content": [ { "type": "text", "text": "{\"success\":true,\"result\":15}" } ], "structuredContent": { "success": true, "result": 15 }, "isError": false } 计算结果文本: {"success":true,"result":15} 因为openclaw是docker里面，所有需要访问docker版的mcp-endpoint-server和mcp-porter-server,需要他们都在一个网络里面，这样才能访问访问。 我们在mcp-endpoint-server增加一个docker网络， mcp-endpoint-server改为 $ cat docker-compose.yml # Docker安装全模块 version: '3' services: # MCP 接入点服务 mcp-endpoint-server: image: ghcr.nju.edu.cn/xinnan-tech/mcp-endpoint-server:latest container_name: mcp-endpoint-server restart: always networks: - mcp-shared-network # 改为共享网络 ports: - "8004:8004" security_opt: - seccomp:unconfined environment: - TZ=Asia/Shanghai volumes: # 配置文件目录 - ./data:/app/data networks: mcp-shared-network: name: mcp-shared-network driver: bridge 增加一个mcp-shared-network网络 mcp-porter-server使用这个网络 $ cat docker-compose.yml networks: mcp-shared-network: external: true name: mcp-shared-network services: mcp-porter: build: . # 使用当前目录的 Dockerfile 构建 container_name: mcp-porter restart: always networks: - mcp-shared-network ports: - "8000:8000" volumes: - ./:/app # 将宿主机当前目录挂载到容器 /app，覆盖代码 environment: - TZ=Asia/Shanghai - REMOTE_WS_URL=ws://mcp-endpoint-server:8004/mcp_endpoint/call/?token=${MCP_TOKEN} # 不再直接写 token，使用 .env 文件 openclaw也使用这个网络 $ cat docker-compose.yml networks: mcp-shared-network: external: true name: mcp-shared-network services: openclaw-cli: entrypoint: - node - dist/index.js environment: BROWSER: echo HOME: /home/node OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} TERM: xterm-256color image: ${OPENCLAW_IMAGE:-openclaw:local} init: true networks: - default - mcp-shared-network stdin_open: true tty: true volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace openclaw-gateway: command: - node - dist/index.js - gateway - --bind - ${OPENCLAW_GATEWAY_BIND:-lan} - --port - '18789' env_file: - ./.env environment: GATEWAY_TRUSTED_PROXIES: ${GATEWAY_TRUSTED_PROXIES} HOME: /home/node OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} TERM: xterm-256color image: ${OPENCLAW_IMAGE:-openclaw:local} init: true networks: - default - mcp-shared-network ports: - ${OPENCLAW_GATEWAY_PORT:-18789}:18789 - ${OPENCLAW_BRIDGE_PORT:-18790}:18790 restart: unless-stopped volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro 配置openclaw.json使用容器名称即可 "plugins": { "entries": { "feishu": { "enabled": true }, "openclaw-mcp-adapter": { "enabled": true, "config": { "servers": [ { "name": "mcp-porter-calculator", "transport": "http", "url": "http://mcp-porter:8000/sse" } ], "toolPrefix": true } } } } 重启三个docker，即可通过openclaw来调用虾哥的计算器功能 在web-ui的chat界面发指令： 使用mcp工具来计算10+10