├── .gitignore ├── DockerFile ├── LICENSE ├── README.md ├── README_CN.md ├── glama.json ├── pyproject.toml └── src └── thingspanel_mcp ├── __init__.py ├── api_client.py ├── config.py ├── main.py ├── prompts ├── __init__.py └── common_prompts.py ├── server.py └── tools ├── __init__.py ├── control_tools.py ├── dashboard_tools.py ├── device_tools.py └── telemetry_tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python相关的忽略文件 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # 虚拟环境 7 | .venv/ 8 | venv/ 9 | env/ 10 | 11 | # 发布和打包 12 | dist/ 13 | build/ 14 | *.egg-info/ 15 | 16 | # 集成开发环境特定文件 17 | .vscode/ 18 | .idea/ 19 | *.swp 20 | *.swo 21 | 22 | # 日志文件 23 | *.log 24 | 25 | # 环境配置文件(包含敏感信息) 26 | .env 27 | *.env 28 | .env.local 29 | .env.development 30 | .env.production 31 | 32 | # 包含敏感数据的配置文件 33 | config.json 34 | config.yaml 35 | 36 | # Jupyter笔记本 37 | .ipynb_checkpoints 38 | 39 | # 操作系统生成文件 40 | .DS_Store 41 | Thumbs.db 42 | 43 | # 备份文件 44 | *.bak 45 | *.backup 46 | 47 | # 密钥和敏感文件 48 | *.pem 49 | *.key 50 | *.crt 51 | 52 | # 依赖目录 53 | node_modules/ 54 | 55 | # 临时文件 56 | *.tmp 57 | *.temp 58 | 59 | # 测试覆盖率和测试相关 60 | .coverage 61 | htmlcov/ 62 | .pytest_cache/ 63 | 64 | # MCP特定文件 65 | *.mcp.log -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim 2 | 3 | LABEL description="ThingsPanel MCP (Model Context Protocol) Server for ThingsPanel IoT platform" 4 | 5 | # 更新系统包并安装安全更新 6 | RUN apt-get update && \ 7 | apt-get upgrade -y && \ 8 | apt-get install -y --no-install-recommends \ 9 | ca-certificates \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # 创建非特权用户 14 | RUN groupadd -r mcp && useradd --no-log-init -r -g mcp mcp 15 | 16 | # 设置工作目录 17 | WORKDIR /app 18 | 19 | # 复制项目文件 20 | COPY . /app/ 21 | 22 | # 安装依赖 23 | RUN pip install --no-cache-dir -e . && \ 24 | pip install --no-cache-dir pip --upgrade 25 | 26 | # 设置环境变量 27 | ENV PYTHONUNBUFFERED=1 28 | 29 | # 修改权限 30 | RUN chown -R mcp:mcp /app 31 | 32 | # 切换到非特权用户 33 | USER mcp 34 | 35 | # 设置入口点 36 | ENTRYPOINT ["thingspanel-mcp"] 37 | 38 | # 默认命令(可以被docker run命令覆盖) 39 | CMD ["--transport", "stdio"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE file distributed as part 112 | of the Derivative Works; within the Source form or documentation, 113 | if provided along with the Derivative Works; or, within a 114 | display generated by the Derivative Works, if and wherever such 115 | third-party notices normally appear. The contents of the NOTICE 116 | file are for informational purposes only and do not modify the 117 | License. You may add Your own attribution notices within 118 | Derivative Works that You distribute, alongside or as an addendum 119 | to the NOTICE text from the Work, provided that such additional 120 | attribution notices cannot be construed as modifying the License. 121 | 122 | You may add Your own copyright statement to Your modifications and 123 | may provide additional or different license terms and conditions 124 | for use, reproduction, or distribution of Your modifications, or 125 | for any such Derivative Works as a whole, provided Your use, 126 | reproduction, and distribution of the Work otherwise complies with 127 | the conditions stated in this License. 128 | 129 | 5. Submission of Contributions. Unless You explicitly state otherwise, 130 | any Contribution intentionally submitted for inclusion in the Work 131 | by You to the Licensor shall be under the terms and conditions of 132 | this License, without any additional terms or conditions. 133 | Notwithstanding the above, nothing herein shall supersede or modify 134 | the terms of any separate license agreement you may have executed 135 | with Licensor regarding such Contributions. 136 | 137 | 6. Trademarks. This License does not grant permission to use the trade 138 | names, trademarks, service marks, or product names of the Licensor, 139 | except as required for reasonable and customary use in describing the 140 | origin of the Work and reproducing the content of the NOTICE file. 141 | 142 | 7. Disclaimer of Warranty. Unless required by applicable law or 143 | agreed to in writing, Licensor provides the Work (and each 144 | Contributor provides its Contributions) on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 146 | implied, including, without limitation, any warranties or conditions 147 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 148 | PARTICULAR PURPOSE. You are solely responsible for determining the 149 | appropriateness of using or redistributing the Work and assume any 150 | risks associated with Your exercise of permissions under this License. 151 | 152 | 8. Limitation of Liability. In no event and under no legal theory, 153 | whether in tort (including negligence), contract, or otherwise, 154 | unless required by applicable law (such as deliberate and grossly 155 | negligent acts) or agreed to in writing, shall any Contributor be 156 | liable to You for damages, including any direct, indirect, special, 157 | incidental, or consequential damages of any character arising as a 158 | result of this License or out of the use or inability to use the 159 | Work (including but not limited to damages for loss of goodwill, 160 | work stoppage, computer failure or malfunction, or any and all 161 | other commercial damages or losses), even if such Contributor 162 | has been advised of the possibility of such damages. 163 | 164 | 9. Accepting Warranty or Additional Liability. While redistributing 165 | the Work or Derivative Works thereof, You may choose to offer, 166 | and charge a fee for, acceptance of support, warranty, indemnity, 167 | or other liability obligations and/or rights consistent with this 168 | License. However, in accepting such obligations, You may act only 169 | on Your own behalf and on Your sole responsibility, not on behalf 170 | of any other Contributor, and only if You agree to indemnify, 171 | defend, and hold each Contributor harmless for any liability 172 | incurred by, or claims asserted against, such Contributor by reason 173 | of your accepting any such warranty or additional liability. 174 | 175 | END OF TERMS AND CONDITIONS 176 | 177 | Copyright 2024 ThingsPanel 178 | 179 | Licensed under the Apache License, Version 2.0 (the "License"); 180 | you may not use this file except in compliance with the License. 181 | You may obtain a copy of the License at 182 | 183 | http://www.apache.org/licenses/LICENSE-2.0 184 | 185 | Unless required by applicable law or agreed to in writing, software 186 | distributed under the License is distributed on an "AS IS" BASIS, 187 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 188 | See the License for the specific language governing permissions and 189 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThingsPanel MCP [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Python Version](https://img.shields.io/pypi/pyversions/thingspanel-mcp.svg)](https://pypi.org/project/thingspanel-mcp/) [![PyPI version](https://badge.fury.io/py/thingspanel-mcp.svg)](https://badge.fury.io/py/thingspanel-mcp) 2 | 3 | 4 | 5 | 6 | [ThingsPanel](http://thingspanel.io/) IoT Platform's MCP (Model Context Protocol) Server. 7 | 8 | [English](README.md) | [中文](README_CN.md) 9 | 10 | ## 🚀 Project Overview 11 | 12 | ThingsPanel MCP Server is an innovative intelligent interface that enables you to: 13 | 14 | - Interact with IoT devices using natural language 15 | - Easily retrieve device information 16 | - Monitor device performance and status in real-time 17 | - Simplify device control commands 18 | - Analyze platform-wide statistical data and trends 19 | 20 | ## Target Audience 21 | 22 | ### Intended Users 23 | 24 | - **IoT Solution Developers**: Engineers and developers building solutions on the ThingsPanel IoT platform and seeking AI integration capabilities 25 | - **AI Integration Experts**: Professionals looking to connect AI models with IoT systems 26 | - **System Administrators**: IT personnel managing IoT infrastructure and wanting to enable AI-driven analysis and control 27 | - **Product Teams**: Teams building products that combine IoT and AI functionality 28 | 29 | ### Problems Addressed 30 | 31 | - **Integration Complexity**: Eliminates the need to create custom integrations between AI models and IoT platforms 32 | - **Standardized Access**: Provides a consistent interface for AI models to interact with IoT data and devices 33 | - **Security Control**: Manages authentication and authorization for AI access to IoT systems 34 | - **Lowered Technical Barriers**: Reduces technical obstacles to adding AI capabilities to existing IoT deployments 35 | 36 | ### Ideal Application Scenarios 37 | 38 | - **Natural Language IoT Control**: Enable users to control devices through AI assistants using natural language 39 | - **Intelligent Data Analysis**: Allow AI models to access and analyze IoT sensor data for insights 40 | - **Anomaly Detection**: Connect AI models to device data streams for real-time anomaly detection 41 | - **Predictive Maintenance**: Enable AI-driven predictive maintenance by providing device history access 42 | - **Automated Reporting**: Create systems that can generate IoT data reports and visualizations on demand 43 | - **Operational Optimization**: Use AI to optimize device operations based on historical patterns 44 | 45 | ## ✨ Core Features 46 | 47 | - 🗣️ Natural Language Querying 48 | - 📊 Comprehensive Device Insights 49 | - 🌡️ Real-time Telemetry Data 50 | - 🎮 Convenient Device Control 51 | - 📈 Platform-wide Analytics 52 | 53 | ## 🛠️ Prerequisites 54 | 55 | - Python 3.8+ 56 | - ThingsPanel Account 57 | - ThingsPanel API Key 58 | 59 | ## 📦 Installation 60 | 61 | ### Option 1: Pip Installation 62 | 63 | ```bash 64 | pip install thingspanel-mcp 65 | ``` 66 | 67 | ### Option 2: Source Code Installation 68 | 69 | ```bash 70 | # Clone the repository 71 | git clone https://github.com/ThingsPanel/thingspanel-mcp.git 72 | 73 | # Navigate to project directory 74 | cd thingspanel-mcp 75 | 76 | # Install the project 77 | pip install -e . 78 | ``` 79 | 80 | ## 🔐 Configuration 81 | 82 | ### Configuration Methods (Choose One) 83 | 84 | #### Method 1: Direct Command Line Configuration (Recommended) 85 | 86 | ```bash 87 | thingspanel-mcp --api-key "Your API Key" --base-url "Your ThingsPanel Base URL" 88 | ``` 89 | 90 | #### Method 2: Environment Variable Configuration 91 | 92 | If you want to avoid repeated input, set environment variables: 93 | 94 | ```bash 95 | # Add to ~/.bashrc, ~/.zshrc, or corresponding shell config file 96 | export THINGSPANEL_API_KEY="Your API Key" 97 | export THINGSPANEL_BASE_URL="Your ThingsPanel Base URL" 98 | 99 | # Then run 100 | source ~/.bashrc # or source ~/.zshrc 101 | ``` 102 | 103 | 💡 Tips: 104 | 105 | - API keys are typically obtained from the API KEY management in the ThingsPanel platform 106 | - Base URL refers to your ThingsPanel platform address, e.g., `http://demo.thingspanel.cn/` 107 | - Command-line configuration is recommended to protect sensitive information 108 | 109 | ## 🖥️ Claude Desktop Integration 110 | 111 | Add the following to your Claude desktop configuration file (`claude_desktop_config.json`): 112 | 113 | ```json 114 | { 115 | "mcpServers": { 116 | "thingspanel": { 117 | "command": "thingspanel-mcp", 118 | "args": [ 119 | "--api-key", "Your API Key", 120 | "--base-url", "Your Base URL" 121 | ] 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | ## 🤔 Interaction Examples 128 | 129 | Using the ThingsPanel MCP Server, you can now make natural language queries such as: 130 | 131 | - "What is the current temperature of my sensor?" 132 | - "List all active devices" 133 | - "Turn on the automatic sprinkler system" 134 | - "Show device activity for the last 24 hours" 135 | 136 | ## 🛡️ Security 137 | 138 | - Secure credential management 139 | - Uses ThingsPanel official API 140 | - Supports token-based authentication 141 | 142 | ## License 143 | 144 | Apache License 2.0 145 | 146 | ## 🌟 Support Us 147 | 148 | If this project helps you, please give us a star on GitHub! ⭐ 149 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ThingsPanel MCP 2 | 3 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) 4 | [![Python Version](https://img.shields.io/pypi/pyversions/thingspanel-mcp.svg)](https://pypi.org/project/thingspanel-mcp/) 5 | [![PyPI version](https://badge.fury.io/py/thingspanel-mcp.svg)](https://badge.fury.io/py/thingspanel-mcp) 6 | 7 | [ThingsPanel](http://thingspanel.io/) 物联网平台的MCP(Model Context Protocol)服务器。 8 | 9 | [English](README.md) | [中文](README_CN.md) 10 | 11 | ## 🚀 项目简介 12 | 13 | ThingsPanel MCP 服务器是一个革新性的智能接口,让您可以: 14 | 15 | - 使用自然语言与物联网设备交互 16 | - 轻松获取设备信息 17 | - 实时监控设备性能和状态 18 | - 简单发送设备控制指令 19 | - 分析平台整体统计数据和趋势 20 | 21 | ## 适用人群 22 | 23 | ### 目标用户 24 | 25 | - **物联网解决方案开发者**:在ThingsPanel物联网平台上构建解决方案并希望集成AI能力的工程师和开发人员 26 | - **AI集成专家**:寻求将AI模型与物联网系统连接的专业人士 27 | - **系统管理员**:负责管理物联网基础设施并希望启用AI驱动的分析和控制的IT人员 28 | - **产品团队**:构建结合物联网和AI功能的产品的团队 29 | 30 | ### 解决的问题 31 | 32 | - **集成复杂性**:消除了在AI模型和物联网平台之间构建自定义集成的需求 33 | - **标准化访问**:为AI模型提供与物联网数据和设备交互的一致接口 34 | - **安全控制**:管理AI访问物联网系统的身份验证和授权 35 | - **降低技术门槛**:降低为现有物联网部署添加AI能力的技术障碍 36 | 37 | ### 理想应用场景 38 | 39 | - **自然语言物联网控制**:使用户能够通过AI助手使用自然语言控制设备 40 | - **智能数据分析**:允许AI模型访问和分析物联网传感器数据以获取洞察 41 | - **异常检测**:将AI模型连接到设备数据流,实现实时异常检测 42 | - **预测性维护**:通过提供设备历史访问,实现AI驱动的预测性维护 43 | - **自动化报告**:创建能够根据请求生成物联网数据报告和可视化的系统 44 | - **运营优化**:使用AI基于历史模式优化设备操作 45 | 46 | ## ✨ 核心功能 47 | 48 | - 🗣️ 自然语言查询 49 | - 📊 全面设备洞察 50 | - 🌡️ 实时遥测数据 51 | - 🎮 便捷设备控制 52 | - 📈 平台全面分析 53 | 54 | ## 🛠️ 环境准备 55 | 56 | - Python 3.8 及以上版本 57 | - ThingsPanel 账户 58 | - ThingsPanel API 密钥 59 | 60 | ## 📦 安装指南 61 | 62 | ### 方式一:Pip 安装 63 | 64 | ```bash 65 | pip install thingspanel-mcp 66 | ``` 67 | 68 | ### 方式二:源代码安装 69 | 70 | ```bash 71 | # 克隆仓库 72 | git clone https://github.com/ThingsPanel/thingspanel-mcp.git 73 | 74 | # 进入项目目录 75 | cd thingspanel-mcp 76 | 77 | # 安装项目 78 | pip install -e . 79 | 80 | # 卸载项目 81 | pip uninstall thingspanel-mcp 82 | ``` 83 | 84 | 85 | ## 🔐 配置设置 86 | 87 | ### 配置方式(选择其一) 88 | 89 | #### 方式一:命令行直接配置(推荐) 90 | 91 | ```bash 92 | thingspanel-mcp --api-key "您的API密钥" --base-url "您的ThingsPanel基础URL" 93 | thingspanel-mcp --api-key "sk_626ece730afadf89ea65755ca17fc4ccf547f3c1c7b5506d67d8a1d38ca808d5" --base-url "http://demo.thingspanel.cn" 94 | 95 | ``` 96 | 97 | #### 方式二:环境变量配置 98 | 99 | 如果您希望在多次使用时避免重复输入,可以设置环境变量: 100 | 101 | ```bash 102 | # 在 ~/.bashrc, ~/.zshrc 或对应的 shell 配置文件中添加 103 | export THINGSPANEL_API_KEY="您的API密钥" 104 | export THINGSPANEL_BASE_URL="您的ThingsPanel基础URL" 105 | 106 | # 然后运行 107 | source ~/.bashrc # 或 source ~/.zshrc 108 | ``` 109 | 110 | #### 方式三:Docker启动 111 | 112 | ```bash 113 | docker run -it --rm thingspanel-mcp --api-key "您的API密钥" --base-url "您的ThingsPanel基础URL" 114 | ``` 115 | 116 | 💡 提示: 117 | 118 | - API密钥通常在 ThingsPanel 平台的API KEY管理中获取。 119 | - 基础URL指的是您的 ThingsPanel 平台地址,例如 `http://demo.thingspanel.cn/` 120 | - 建议优先使用命令行配置,以保护敏感信息 121 | 122 | ## 🖥️ Claude 桌面版集成 123 | 124 | 在您的 Claude 桌面配置文件 (`claude_desktop_config.json`) 中添加以下内容。根据您的操作系统选择合适的配置: 125 | 126 | ### Windows 配置示例 127 | 128 | ```json 129 | { 130 | "mcpServers": { 131 | "thingspanel": { 132 | "command": "thingspanel-mcp", 133 | "args": [ 134 | "--api-key", "您的API密钥", 135 | "--base-url", "您的基础URL" 136 | ] 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ### macOS 配置示例 143 | 144 | ```json 145 | { 146 | "mcpServers": { 147 | "thingspanel": { 148 | "command": "/Library/Frameworks/Python.framework/Versions/3.12/bin/thingspanel-mcp", 149 | "args": [], 150 | "env": { 151 | "THINGSPANEL_API_KEY": "您的API密钥", 152 | "THINGSPANEL_BASE_URL": "您的基础URL" 153 | } 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | ### Docker部署配置 160 | 161 | ```json 162 | { 163 | "mcpServers": { 164 | "thingspanel": { 165 | "command": "docker", 166 | "args": ["run", "--rm", "-i", "thingspanel-mcp", "--transport", "stdio", "--api-key", "您的API密钥", "--base-url", "您的基础URL"] 167 | } 168 | } 169 | } 170 | 171 | 💡 提示: 172 | 173 | - macOS 用户需要使用 Python 可执行文件的完整路径 174 | - 可以通过运行 `which thingspanel-mcp` 命令找到具体路径 175 | - 建议使用环境变量方式配置敏感信息,避免直接暴露在配置文件中 176 | - 请根据您的 Python 安装位置调整路径 177 | 178 | ## 🤔 交互示例 179 | 180 | 使用 ThingsPanel MCP 服务器,您现在可以进行如下自然语言查询: 181 | 182 | - "我的传感器当前温度是多少?" 183 | - "列出所有活跃设备" 184 | - "打开自动喷灌系统" 185 | - "显示最近24小时的设备活动情况" 186 | 187 | ## 🛡️ 安全性 188 | 189 | - 凭证安全管理 190 | - 使用 ThingsPanel 官方 API 191 | - 支持基于令牌的身份验证 192 | 193 | ## 许可证 194 | 195 | Apache License 2.0 196 | 197 | ## 🌟 支持我们 198 | 199 | 如果这个项目对您有帮助,请在 GitHub 上给我们一个星标!⭐ 200 | -------------------------------------------------------------------------------- /glama.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://glama.ai/mcp/schemas/server.json", 3 | "maintainers": [ 4 | "zjhong" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "thingspanel-mcp" 7 | version = "0.1.6" 8 | description = "MCP server for ThingsPanel IoT platform" 9 | readme = "README.md" 10 | authors = [ 11 | { name = "ThingsPanel", email = "info@thingspanel.com" } 12 | ] 13 | license = {file = "LICENSE"} 14 | requires-python = ">=3.8" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [ 21 | "mcp>=1.2.0", 22 | "httpx>=0.23.0", 23 | ] 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/ThingsPanel/thingspanel-mcp" 27 | 28 | [project.scripts] 29 | thingspanel-mcp = "thingspanel_mcp.main:main" 30 | 31 | [tool.setuptools] 32 | package-dir = {"" = "src"} 33 | 34 | [tool.setuptools.packages.find] 35 | where = ["src"] -------------------------------------------------------------------------------- /src/thingspanel_mcp/__init__.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/__init__.py 2 | import logging 3 | from .server import ThingsPanelServer 4 | from .config import config 5 | 6 | # 设置日志格式 7 | logging.basicConfig( 8 | level=logging.INFO, 9 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 10 | ) 11 | 12 | # 导出主要类和函数 13 | __all__ = ['ThingsPanelServer', 'config'] 14 | 15 | # 版本信息 16 | __version__ = '0.1.0' -------------------------------------------------------------------------------- /src/thingspanel_mcp/api_client.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/api_client.py 2 | import httpx 3 | import logging 4 | import json 5 | from typing import Dict, Any, Optional, List, Union 6 | from .config import config 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class ThingsPanelClient: 11 | def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None): 12 | self.api_key = api_key or config.api_key 13 | self.base_url = base_url or config.base_url 14 | if not self.api_key: 15 | logger.warning("API key not provided. API calls will likely fail.") 16 | 17 | async def _request(self, method: str, endpoint: str, params=None, json_data=None) -> Dict[str, Any]: 18 | """发送HTTP请求到ThingsPanel API""" 19 | url = f"{self.base_url}{endpoint}" 20 | headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} 21 | 22 | try: 23 | async with httpx.AsyncClient() as client: 24 | response = await client.request( 25 | method, 26 | url, 27 | headers=headers, 28 | params=params, 29 | json=json_data, 30 | timeout=30.0 31 | ) 32 | response.raise_for_status() 33 | return response.json() 34 | except httpx.HTTPStatusError as e: 35 | logger.error(f"HTTP error: {e.response.status_code} - {e.response.text}") 36 | raise 37 | except httpx.RequestError as e: 38 | logger.error(f"Request error: {str(e)}") 39 | raise 40 | except Exception as e: 41 | logger.error(f"Unexpected error: {str(e)}") 42 | raise 43 | 44 | # 设备相关方法 45 | async def get_devices(self, page: int = 1, page_size: int = 10, search: str = None) -> Dict[str, Any]: 46 | """ 47 | 获取设备列表 48 | 49 | 参数: 50 | page: 页码,默认1 51 | page_size: 每页数量,默认10 52 | search: 搜索关键字 53 | """ 54 | params = { 55 | "page": page, 56 | "page_size": page_size 57 | } 58 | if search: 59 | params["search"] = search 60 | 61 | return await self._request("GET", "/api/v1/device", params=params) 62 | 63 | async def get_device_detail(self, device_id: str) -> Dict[str, Any]: 64 | """获取设备详情""" 65 | return await self._request("GET", f"/api/v1/device/detail/{device_id}") 66 | 67 | async def get_device_online_status(self, device_id: str) -> Dict[str, Any]: 68 | """获取设备在线状态""" 69 | return await self._request("GET", f"/api/v1/device/online/status/{device_id}") 70 | 71 | # 遥测数据相关方法 72 | async def get_current_telemetry(self, device_id: str) -> Dict[str, Any]: 73 | """获取设备当前遥测数据""" 74 | return await self._request("GET", f"/api/v1/telemetry/datas/current/{device_id}") 75 | 76 | async def get_telemetry_by_keys(self, device_id: str, keys: List[str]) -> Dict[str, Any]: 77 | """根据key获取遥测数据""" 78 | params = { 79 | "device_id": device_id, 80 | "keys": keys 81 | } 82 | return await self._request("GET", "/api/v1/telemetry/datas/current/keys", params=params) 83 | 84 | async def get_telemetry_statistics( 85 | self, 86 | device_id: str, 87 | key: str, 88 | time_range: str = "last_1h", 89 | aggregate_window: str = "no_aggregate", 90 | aggregate_function: Optional[str] = None, 91 | start_time: Optional[int] = None, 92 | end_time: Optional[int] = None 93 | ) -> Dict[str, Any]: 94 | """获取设备遥测数据统计""" 95 | params = { 96 | "device_id": device_id, 97 | "key": key, 98 | "time_range": time_range, 99 | "aggregate_window": aggregate_window 100 | } 101 | 102 | if aggregate_function and aggregate_window != "no_aggregate": 103 | params["aggregate_function"] = aggregate_function 104 | 105 | if time_range == "custom": 106 | if start_time: 107 | params["start_time"] = start_time 108 | if end_time: 109 | params["end_time"] = end_time 110 | 111 | return await self._request("GET", "/api/v1/telemetry/datas/statistic", params=params) 112 | 113 | async def publish_telemetry(self, device_id: str, value: Union[Dict[str, Any], str]) -> Dict[str, Any]: 114 | """下发遥测数据""" 115 | # 确保值是正确的格式(JSON字符串) 116 | if isinstance(value, dict): 117 | value_str = json.dumps(value) 118 | else: 119 | value_str = value 120 | 121 | data = { 122 | "device_id": device_id, 123 | "value": value_str 124 | } 125 | return await self._request("POST", "/api/v1/telemetry/datas/pub", json_data=data) 126 | 127 | # 属性数据相关方法 128 | async def get_device_attributes(self, device_id: str) -> Dict[str, Any]: 129 | """获取设备属性""" 130 | return await self._request("GET", f"/api/v1/attribute/datas/{device_id}") 131 | 132 | # 命令相关方法 133 | async def get_command_logs( 134 | self, 135 | device_id: str, 136 | page: int = 1, 137 | page_size: int = 10, 138 | status: Optional[str] = None, 139 | operation_type: Optional[str] = None 140 | ) -> Dict[str, Any]: 141 | """获取命令下发记录""" 142 | params = { 143 | "device_id": device_id, 144 | "page": page, 145 | "page_size": page_size 146 | } 147 | 148 | if status: 149 | params["status"] = status 150 | if operation_type: 151 | params["operation_type"] = operation_type 152 | 153 | return await self._request("GET", "/api/v1/command/datas/set/logs", params=params) 154 | 155 | # 看板相关方法 156 | 157 | async def get_tenant_id(self) -> Dict[str, Any]: 158 | """获取租户ID""" 159 | return await self._request("GET", "/api/v1/user/tenant/id") 160 | 161 | async def get_tenant_devices_info(self) -> Dict[str, Any]: 162 | """获取租户下设备信息""" 163 | return await self._request("GET", "/api/v1/board/tenant/device/info") 164 | 165 | async def get_message_count(self) -> Dict[str, Any]: 166 | """获取租户大致消息数量""" 167 | return await self._request("GET", "/api/v1/telemetry/datas/msg/count") 168 | 169 | async def get_device_trend(self) -> Dict[str, Any]: 170 | """获取设备在线离线趋势""" 171 | return await self._request("GET", "/api/v1/board/trend") 172 | 173 | async def get_device_model_sources(self, device_template_id: str) -> Dict[str, Any]: 174 | """获取设备模板的数据源列表(遥测、属性等)""" 175 | params = { 176 | "id": device_template_id 177 | } 178 | return await self._request("GET", "/api/v1/device/model/source/at/list", params=params) 179 | 180 | async def publish_attributes(self, device_id: str, value: Union[Dict[str, Any], str]) -> Dict[str, Any]: 181 | """设置设备属性""" 182 | # 确保值是正确的格式(JSON字符串) 183 | if isinstance(value, dict): 184 | value_str = json.dumps(value) 185 | else: 186 | value_str = value 187 | 188 | data = { 189 | "device_id": device_id, 190 | "value": value_str 191 | } 192 | return await self._request("POST", "/api/v1/attribute/datas/pub", json_data=data) 193 | 194 | async def publish_command(self, device_id: str, value: Union[Dict[str, Any], str], identifier: str) -> Dict[str, Any]: 195 | """下发设备命令""" 196 | # 确保值是正确的格式(JSON字符串) 197 | if isinstance(value, dict): 198 | value_str = json.dumps(value) 199 | else: 200 | value_str = value 201 | 202 | data = { 203 | "device_id": device_id, 204 | "value": value_str, 205 | "Identify": identifier 206 | } 207 | return await self._request("POST", "/api/v1/command/datas/pub", json_data=data) 208 | 209 | async def get_device_model_commands(self, device_template_id: str, page: int = 1, page_size: int = 100) -> Dict[str, Any]: 210 | """获取设备模板的命令详情""" 211 | params = { 212 | "page": page, 213 | "page_size": page_size, 214 | "device_template_id": device_template_id 215 | } 216 | return await self._request("GET", "/api/v1/device/model/commands", params=params) 217 | 218 | async def get_device_model_by_type(self, device_template_id: str, model_type: str, page: int = 1, page_size: int = 100) -> Dict[str, Any]: 219 | """获取设备模板的指定类型物模型信息""" 220 | if model_type not in ["telemetry", "attributes", "commands", "events"]: 221 | raise ValueError(f"不支持的物模型类型: {model_type}") 222 | 223 | params = { 224 | "page": page, 225 | "page_size": page_size, 226 | "device_template_id": device_template_id 227 | } 228 | return await self._request("GET", f"/api/v1/device/model/{model_type}", params=params) -------------------------------------------------------------------------------- /src/thingspanel_mcp/config.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/config.py 2 | import os 3 | import json 4 | from pathlib import Path 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | class Config: 10 | def __init__(self): 11 | self.base_url = "http://demo.thingspanel.cn" 12 | self.api_key = None 13 | self.load_config() 14 | 15 | def load_config(self): 16 | """加载配置,优先使用环境变量,其次使用配置文件""" 17 | # 从环境变量加载 18 | self.api_key = os.environ.get("THINGSPANEL_API_KEY") 19 | if os.environ.get("THINGSPANEL_BASE_URL"): 20 | self.base_url = os.environ.get("THINGSPANEL_BASE_URL") 21 | 22 | # 如果环境变量中没有API密钥,尝试从配置文件加载 23 | if not self.api_key: 24 | config_path = os.environ.get( 25 | "THINGSPANEL_CONFIG_PATH", 26 | str(Path.home() / ".thingspanel" / "config.json") 27 | ) 28 | try: 29 | if os.path.exists(config_path): 30 | with open(config_path, 'r') as f: 31 | config_data = json.load(f) 32 | self.api_key = config_data.get("api_key") 33 | if config_data.get("base_url"): 34 | self.base_url = config_data.get("base_url") 35 | except Exception as e: 36 | logger.warning(f"Failed to load config file: {e}") 37 | 38 | def is_configured(self): 39 | """检查是否已配置API密钥""" 40 | return bool(self.api_key) 41 | 42 | # 创建全局配置实例 43 | config = Config() -------------------------------------------------------------------------------- /src/thingspanel_mcp/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import logging 5 | 6 | from thingspanel_mcp import ThingsPanelServer, config 7 | 8 | def main(): 9 | # 设置参数解析 10 | parser = argparse.ArgumentParser(description='ThingsPanel MCP 服务器') 11 | parser.add_argument('--api-key', help='ThingsPanel API密钥') 12 | parser.add_argument('--base-url', help='ThingsPanel API基础URL') 13 | parser.add_argument('--transport', choices=['stdio', 'sse'], default='stdio', 14 | help='传输类型 (默认: stdio)') 15 | parser.add_argument('--verbose', '-v', action='store_true', help='启用详细日志') 16 | args = parser.parse_args() 17 | 18 | # 设置日志级别 19 | log_level = logging.DEBUG if args.verbose else logging.INFO 20 | logging.basicConfig(level=log_level) 21 | 22 | # 设置API密钥 23 | if args.api_key: 24 | os.environ['THINGSPANEL_API_KEY'] = args.api_key 25 | print(f"从命令行设置API密钥: {args.api_key[:5]}...") 26 | 27 | # 设置基础URL 28 | if args.base_url: 29 | os.environ['THINGSPANEL_BASE_URL'] = args.base_url 30 | print(f"从命令行设置基础URL: {args.base_url}") 31 | 32 | # 重载配置模块以确保环境变量被应用 33 | from thingspanel_mcp.config import config 34 | config.load_config() 35 | 36 | # 创建并运行服务器 37 | server = ThingsPanelServer() 38 | server.run(transport=args.transport) -------------------------------------------------------------------------------- /src/thingspanel_mcp/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/prompts/__init__.py 2 | from . import common_prompts 3 | 4 | __all__ = ['common_prompts'] -------------------------------------------------------------------------------- /src/thingspanel_mcp/prompts/common_prompts.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/prompts/common_prompts.py 2 | from typing import Dict, Any, Optional, List 3 | 4 | def welcome_prompt() -> List[Dict[str, Any]]: 5 | """欢迎提示,介绍如何使用ThingsPanel MCP服务""" 6 | return [ 7 | { 8 | "role": "user", 9 | "content": { 10 | "type": "text", 11 | "text": "我想了解如何使用ThingsPanel MCP服务" 12 | } 13 | }, 14 | { 15 | "role": "assistant", 16 | "content": { 17 | "type": "text", 18 | "text": """# 欢迎使用ThingsPanel MCP服务 19 | 20 | ThingsPanel MCP服务让您可以使用自然语言与ThingsPanel物联网平台交互,轻松管理和查询您的物联网设备和数据。 21 | 22 | ## 您可以做什么: 23 | 24 | 1. **查询设备信息** 25 | - "列出所有设备" 26 | - "查询设备详情" 27 | - "检查设备在线状态" 28 | 29 | 2. **获取遥测数据** 30 | - "获取设备的当前温度" 31 | - "查看设备最近一小时的湿度数据" 32 | - "显示设备的电池电量历史记录" 33 | 34 | 3. **控制设备** 35 | - "打开设备的开关" 36 | - "设置设备温度为25度" 37 | - "向设备发送重启命令" 38 | 39 | 4. **查看平台概览** 40 | - "获取租户概览" 41 | - "显示设备在线趋势" 42 | - "统计消息数量" 43 | 44 | ## 使用示例: 45 | 46 | - "请列出我的所有设备" 47 | - "获取设备'温湿度传感器'的详细信息" 48 | - "设备'客厅空调'当前的温度是多少?" 49 | - "展示设备'办公室传感器'最近24小时的温度历史数据" 50 | - "查看租户总览信息" 51 | 52 | 随时提问,我将帮助您轻松管理和了解您的物联网设备! 53 | 54 | 注意:如果有多个同名设备,我会请您提供更多信息以确认具体设备。""" 55 | } 56 | } 57 | ] 58 | 59 | def device_query_prompt() -> List[Dict[str, Any]]: 60 | """设备查询提示模板""" 61 | return [ 62 | { 63 | "role": "user", 64 | "content": { 65 | "type": "text", 66 | "text": "如何查询设备信息?" 67 | } 68 | }, 69 | { 70 | "role": "assistant", 71 | "content": { 72 | "type": "text", 73 | "text": """# 设备查询指南 74 | 75 | 您可以通过以下方式查询ThingsPanel平台上的设备信息: 76 | 77 | ## 列出设备 78 | - "显示前10个设备" 79 | - "由于设备数量可能庞大,不可以列出所有设备,可以提示用户列出第几页的设备,默认一页10个设备" 80 | - "搜索设备"(支持大小写不敏感的模糊搜索,不要多次尝试,没搜索到就及时反馈用户) 81 | 82 | ## 查看设备详情 83 | - "获取设备{device_name}的详细信息" 84 | - "查看设备{device_name}的详情" 85 | 86 | ## 检查设备状态 87 | - "设备{device_name}在线吗?" 88 | - "检查设备{device_name}的状态" 89 | - "哪些设备目前在线?" 90 | 91 | ## 查询设备属性 92 | - "设备{device_name}的属性有哪些?" 93 | - "显示设备{device_name}的配置信息" 94 | 95 | 请尝试使用这些查询方式来获取您的设备信息! 96 | 97 | 注意:AI助手在处理查询时,如果需要设备ID但用户只提供了设备名称,应使用模糊匹配接口通过设备名称查找对应的设备ID。如果匹配到多个设备,应向用户确认具体是哪个设备。""" 98 | } 99 | } 100 | ] 101 | 102 | def telemetry_query_prompt() -> List[Dict[str, Any]]: 103 | """遥测数据查询提示模板""" 104 | return [ 105 | { 106 | "role": "user", 107 | "content": { 108 | "type": "text", 109 | "text": "如何查询设备的遥测数据?" 110 | } 111 | }, 112 | { 113 | "role": "assistant", 114 | "content": { 115 | "type": "text", 116 | "text": """# 遥测数据查询指南 117 | 118 | 您可以通过以下方式查询ThingsPanel平台上的设备遥测数据: 119 | 120 | ## 获取当前数据 121 | - "获取设备{device_name}的当前遥测数据" 122 | - "设备{device_name}的温度是多少?" 123 | - "显示设备{device_name}的湿度值" 124 | 125 | ## 查询历史数据 126 | - "获取设备{device_name}最近一小时的温度数据" 127 | - "显示设备{device_name}过去24小时的湿度历史" 128 | - "查询设备{device_name}的电池电量趋势" 129 | 130 | ## 数据聚合与统计 131 | - "计算设备{device_name}今天的平均温度" 132 | - "获取设备{device_name}上周的最高湿度值" 133 | - "统计设备{device_name}本月的运行时间" 134 | 135 | 可用的时间范围选项: 136 | - 最近5分钟、15分钟、30分钟 137 | - 最近1小时、3小时、6小时、12小时、24小时 138 | - 最近3天、7天、15天、30天、60天、90天 139 | - 最近6个月、1年 140 | 141 | 请尝试使用这些查询方式来获取您的设备遥测数据! 142 | 143 | 注意:AI助手在处理查询时,如果用户只提供设备名称而没有设备ID,应使用模糊匹配接口通过设备名称查找对应的设备ID。如果匹配到多个设备,应向用户确认具体是哪个设备。""" 144 | } 145 | } 146 | ] 147 | 148 | def device_control_prompt() -> List[Dict[str, Any]]: 149 | """设备控制提示模板""" 150 | return [ 151 | { 152 | "role": "user", 153 | "content": { 154 | "type": "text", 155 | "text": "如何控制设备?" 156 | } 157 | }, 158 | { 159 | "role": "assistant", 160 | "content": { 161 | "type": "text", 162 | "text": """# 设备控制指南 163 | 164 | 您可以通过以下方式控制ThingsPanel平台上的设备: 165 | 166 | ## 步骤1: 查询物模型(必须) 167 | 在发送控制命令前,必须先查询设备的物模型信息,以确保了解可用的控制点和参数: 168 | - "获取设备{device_name}的物模型信息" 169 | - "查看设备{device_name}的物模型" 170 | 171 | 物模型包含设备支持的遥测点、属性和命令,只有符合物模型的控制指令才能被设备正确接收和执行。 172 | 173 | ## 步骤2: 基于物模型发送控制指令 174 | 根据物模型信息,选择合适的控制方式: 175 | 176 | ### 遥测控制 177 | 用于控制设备的实时数据,基于物模型中的遥测点: 178 | - "控制设备{device_name}的参数" 179 | - "设置设备{device_name}的属性值" 180 | 181 | **具体命令示例:** 182 | - `control_device_telemetry(device_id="abc123", control_data={"parameter_name": value})` 183 | - `control_device_telemetry(device_id="abc123", control_data="parameter_name=value")` 184 | 185 | ## 属性设置 186 | 用于配置设备的静态属性,适用于任何类型的设备属性: 187 | - "设置设备{device_name}的属性" 188 | - "配置设备{device_name}的参数" 189 | 190 | **具体命令示例:** 191 | - `set_device_attributes(device_id="abc123", attribute_data={"attribute_name": value})` 192 | 193 | ## 命令下发 194 | 用于执行特定功能的命令,适用于任何类型的命令: 195 | - "向设备{device_name}发送命令" 196 | - "执行设备{device_name}的操作" 197 | 198 | **具体命令示例:** 199 | - `send_device_command(device_id="abc123", command_data={"method": "CommandName", "params": {"param": value}})` 200 | 201 | ## 推荐使用的标准控制流程 202 | 203 | 为了确保命令符合设备物模型,推荐使用以下辅助函数,它会自动先查询物模型再发送命令: 204 | 205 | ## 使用示例 206 | 1. 先获取物模型: 207 | "获取设备温湿度传感器的物模型信息" 208 | 209 | 2. 根据物模型发送控制命令: 210 | "控制设备温湿度传感器,设置temperature=25" 211 | 212 | 请确保您有权限控制目标设备,并且所发送的命令符合设备的物模型规范。 213 | 214 | 注意:AI助手在处理控制命令时,如果需要设备ID但用户只提供了设备名称,应使用模糊匹配接口通过设备名称查找对应的设备ID。如果匹配到多个设备,应向用户确认具体是哪个设备,以避免错误控制。在执行控制命令前,必须先确认设备ID和物模型。""" 215 | } 216 | } 217 | ] 218 | 219 | def dashboard_prompt() -> List[Dict[str, Any]]: 220 | """平台概览提示模板""" 221 | return [ 222 | { 223 | "role": "user", 224 | "content": { 225 | "type": "text", 226 | "text": "如何查看平台总体情况?" 227 | } 228 | }, 229 | { 230 | "role": "assistant", 231 | "content": { 232 | "type": "text", 233 | "text": """# 平台概览查询指南 234 | 235 | 您可以通过以下方式查询ThingsPanel平台的总体情况: 236 | 237 | ## 租户概览 238 | - "获取租户概览信息" 239 | - "显示租户统计数据" 240 | - "查询平台用户数量" 241 | 242 | ## 设备统计 243 | - "统计平台设备总数" 244 | - "查看设备在线率" 245 | - "获取设备活跃情况" 246 | 247 | ## 趋势分析 248 | - "显示设备在线趋势" 249 | - "查看最近24小时的设备活跃度" 250 | - "获取平台消息数量趋势" 251 | 252 | ## 综合报告 253 | - "生成平台运行报告" 254 | - "展示平台健康状况" 255 | - "获取租户资源使用情况" 256 | 257 | 这些查询可以帮助您全面了解您的ThingsPanel平台状态和运行情况!""" 258 | } 259 | } 260 | ] -------------------------------------------------------------------------------- /src/thingspanel_mcp/server.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/server.py 2 | import logging 3 | import os 4 | from pathlib import Path 5 | from typing import Dict, Any, List, Optional, Union 6 | from mcp.server.fastmcp import FastMCP, Context 7 | from .config import config 8 | from .tools import device_tools, telemetry_tools, dashboard_tools, control_tools 9 | from .prompts import common_prompts 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class ThingsPanelServer: 14 | """ThingsPanel MCP 服务器""" 15 | 16 | def __init__(self, server_name: str = "ThingsPanel"): 17 | """初始化ThingsPanel MCP服务器""" 18 | self.server = FastMCP(server_name) 19 | self._setup_tools() 20 | self._setup_prompts() 21 | 22 | def _setup_tools(self): 23 | """设置服务器工具""" 24 | # 设备相关工具 25 | self.server.tool()(device_tools.list_devices) 26 | self.server.tool()(device_tools.get_device_detail) 27 | self.server.tool()(device_tools.check_device_status) 28 | 29 | # 遥测数据相关工具 30 | self.server.tool()(telemetry_tools.get_device_telemetry) 31 | self.server.tool()(telemetry_tools.get_telemetry_by_key) 32 | self.server.tool()(telemetry_tools.get_telemetry_history) 33 | 34 | # 看板相关工具 35 | self.server.tool()(dashboard_tools.get_tenant_summary) 36 | self.server.tool()(dashboard_tools.get_device_trend_report) 37 | 38 | # 设备控制相关工具 39 | self.server.tool()(control_tools.get_device_model_info) 40 | self.server.tool()(control_tools.control_device_telemetry) 41 | self.server.tool()(control_tools.set_device_attributes) 42 | self.server.tool()(control_tools.send_device_command) 43 | self.server.tool()(control_tools.control_device_with_model_check) 44 | 45 | def _setup_prompts(self): 46 | """设置预定义提示""" 47 | # 添加常用提示 48 | self.server.prompt()(self._welcome_prompt) 49 | self.server.prompt()(self._device_query_prompt) 50 | self.server.prompt()(self._telemetry_query_prompt) 51 | self.server.prompt()(self._device_control_prompt) 52 | self.server.prompt()(self._dashboard_prompt) 53 | 54 | async def _welcome_prompt(self) -> List[Dict[str, Any]]: 55 | """欢迎提示""" 56 | return common_prompts.welcome_prompt() 57 | 58 | async def _device_query_prompt(self) -> List[Dict[str, Any]]: 59 | """设备查询提示""" 60 | return common_prompts.device_query_prompt() 61 | 62 | async def _telemetry_query_prompt(self) -> List[Dict[str, Any]]: 63 | """遥测数据查询提示""" 64 | return common_prompts.telemetry_query_prompt() 65 | 66 | async def _device_control_prompt(self) -> List[Dict[str, Any]]: 67 | """设备控制提示""" 68 | return common_prompts.device_control_prompt() 69 | 70 | async def _dashboard_prompt(self) -> List[Dict[str, Any]]: 71 | """平台概览提示""" 72 | return common_prompts.dashboard_prompt() 73 | 74 | def check_configuration(self) -> bool: 75 | """检查配置是否完整""" 76 | if not config.api_key: 77 | logger.warning("API密钥未配置,服务无法正常工作") 78 | return False 79 | return True 80 | 81 | def run(self, transport: str = 'stdio'): 82 | """运行服务器""" 83 | # 重新加载配置,确保环境变量中的设置被应用 84 | from .config import config 85 | config.load_config() 86 | 87 | if not self.check_configuration(): 88 | logger.error("配置不完整,服务器启动失败") 89 | print("配置不完整,服务器启动失败。请确保API密钥已正确配置。") 90 | print("您可以通过以下方式配置API密钥:") 91 | print("1. 设置环境变量 THINGSPANEL_API_KEY") 92 | print("2. 创建配置文件 ~/.thingspanel/config.json 并包含 {\"api_key\": \"您的API密钥\"}") 93 | return 94 | 95 | logger.info(f"ThingsPanel MCP 服务器启动,使用 {transport} 传输") 96 | self.server.run(transport=transport) -------------------------------------------------------------------------------- /src/thingspanel_mcp/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/tools/__init__.py 2 | from . import device_tools 3 | from . import telemetry_tools 4 | from . import dashboard_tools 5 | from . import control_tools 6 | 7 | __all__ = ['device_tools', 'telemetry_tools', 'dashboard_tools', 'control_tools'] -------------------------------------------------------------------------------- /src/thingspanel_mcp/tools/control_tools.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Any, Optional, Union 2 | import logging 3 | import json 4 | from ..api_client import ThingsPanelClient 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | async def get_device_model_info(device_id: str, model_type: str = "all") -> str: 9 | """ 10 | 获取设备物模型信息 11 | 12 | 参数: 13 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 14 | model_type: 物模型类型,可选值:'all'、'telemetry'、'attributes'、'commands'、'events';在控制设备前,建议使用all查询。 15 | 16 | 返回: 17 | 格式化的物模型信息文本 18 | """ 19 | client = ThingsPanelClient() 20 | try: 21 | # 首先获取设备详情,找到设备模板ID 22 | device_detail = await client.get_device_detail(device_id) 23 | 24 | if device_detail.get("code") != 200: 25 | return f"获取设备详情失败:{device_detail.get('message', '未知错误')}" 26 | 27 | device_data = device_detail.get("data", {}) 28 | 29 | # 从device_config字段中获取设备模板ID 30 | device_config = device_data.get("device_config", {}) 31 | device_template_id = device_config.get("device_template_id") 32 | 33 | if not device_template_id: 34 | return f"设备 {device_id} 未关联设备模板,无法获取物模型信息。设备配置信息:{json.dumps(device_config, ensure_ascii=False)}" 35 | 36 | formatted_info = [f"# 设备 {device_id} 物模型信息\n"] 37 | formatted_info.append(f"设备模板ID: {device_template_id}\n") 38 | 39 | # 确定需要查询的模型类型 40 | model_types = [] 41 | if model_type.lower() == "all": 42 | model_types = ["telemetry", "attributes", "commands", "events"] 43 | else: 44 | model_types = [model_type.lower()] 45 | 46 | # 查询每种模型类型 47 | for type_name in model_types: 48 | endpoint = f"/api/v1/device/model/{type_name}" 49 | 50 | # 查询指定类型的物模型 51 | model_result = await client._request("GET", endpoint, params={ 52 | "page": 1, 53 | "page_size": 100, 54 | "device_template_id": device_template_id 55 | }) 56 | 57 | if model_result.get("code") != 200: 58 | formatted_info.append(f"\n## {type_name.capitalize()} 查询失败\n") 59 | formatted_info.append(f"错误信息: {model_result.get('message', '未知错误')}\n") 60 | continue 61 | 62 | # 提取模型列表 63 | model_list = model_result.get("data", {}).get("list", []) 64 | 65 | # 显示类型标题 66 | type_display_map = { 67 | "telemetry": "遥测", 68 | "attributes": "属性", 69 | "commands": "命令", 70 | "events": "事件" 71 | } 72 | type_display = type_display_map.get(type_name, type_name.capitalize()) 73 | formatted_info.append(f"\n## {type_display} ({type_name})\n") 74 | 75 | if not model_list: 76 | formatted_info.append(f"没有找到任何{type_display}定义\n") 77 | continue 78 | 79 | # 处理命令类型的特殊情况 80 | if type_name == "commands": 81 | for item in model_list: 82 | item_name = item.get("data_name", "未知") 83 | item_id = item.get("data_identifier", "未知") 84 | 85 | formatted_info.append(f"### {item_name} (`{item_id}`)\n") 86 | 87 | # 处理命令参数 88 | params_str = item.get("params", "[]") 89 | try: 90 | params = json.loads(params_str) if isinstance(params_str, str) else params_str 91 | except json.JSONDecodeError: 92 | params = [] 93 | 94 | if params and isinstance(params, list): 95 | formatted_info.append("参数列表:\n") 96 | for param in params: 97 | if isinstance(param, dict): 98 | param_name = param.get("name", "") 99 | param_type = param.get("type", "") 100 | param_desc = param.get("description", "") 101 | param_required = "是" if param.get("required", False) else "否" 102 | 103 | formatted_info.append(f"- **{param_name}** (类型: {param_type}, 必填: {param_required})") 104 | if param_desc: 105 | formatted_info.append(f" 描述: {param_desc}") 106 | 107 | # 生成命令示例 108 | formatted_info.append("\n使用示例:") 109 | example_params = {} 110 | if params and isinstance(params, list): 111 | for param in params: 112 | if isinstance(param, dict) and "name" in param: 113 | # 根据类型生成示例值 114 | param_type = param.get("type", "").lower() 115 | if param_type == "string": 116 | example_params[param["name"]] = "value" 117 | elif param_type == "number": 118 | example_params[param["name"]] = 0 119 | elif param_type == "boolean": 120 | example_params[param["name"]] = False 121 | else: 122 | example_params[param["name"]] = "value" 123 | 124 | example_cmd = { 125 | "method": item_id, 126 | "params": example_params 127 | } 128 | formatted_info.append(f"```json\n{json.dumps(example_cmd, ensure_ascii=False, indent=2)}\n```\n") 129 | else: 130 | # 处理其他类型的物模型 131 | for item in model_list: 132 | item_name = item.get("data_name", "未知") 133 | item_id = item.get("data_identifier", "未知") 134 | data_type = item.get("data_type", "未知") 135 | unit = item.get("unit", "") 136 | read_write = item.get("read_write_flag", "") 137 | 138 | rw_display = "" 139 | if read_write == "R": 140 | rw_display = "只读" 141 | elif read_write == "W": 142 | rw_display = "只写" 143 | elif read_write == "RW": 144 | rw_display = "可读写" 145 | 146 | unit_display = f", 单位: {unit}" if unit else "" 147 | rw_display = f", 权限: {rw_display}" if rw_display else "" 148 | 149 | formatted_info.append(f"- **{item_name}** (标识符: `{item_id}`, 类型: {data_type}{unit_display}{rw_display})") 150 | 151 | return "\n".join(formatted_info) 152 | 153 | except Exception as e: 154 | logger.error(f"获取设备物模型信息出错: {str(e)}") 155 | return f"获取设备物模型信息时发生错误: {str(e)}" 156 | 157 | async def control_device_telemetry(device_id: str, control_data: Union[Dict[str, Any], str]) -> str: 158 | """ 159 | 发送遥测数据控制设备 - 通用接口,可用于控制任何类型的遥测数据,物模型中带可写权限的遥测数据 160 | 161 | 参数: 162 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 163 | control_data: 控制数据,格式如 {"temperature": 28.5, "light": 2000, "switch": true} 164 | """ 165 | client = ThingsPanelClient() 166 | try: 167 | # 处理不同格式的输入 168 | if isinstance(control_data, str): 169 | # 尝试解析为JSON 170 | try: 171 | control_json = json.loads(control_data) 172 | except json.JSONDecodeError: 173 | # 不是JSON格式,尝试解析为key=value格式 174 | if "=" in control_data: 175 | key, value = control_data.split("=", 1) 176 | try: 177 | # 尝试将值转换为适当的类型 178 | if value.lower() == "true": 179 | parsed_value = True 180 | elif value.lower() == "false": 181 | parsed_value = False 182 | elif value.isdigit(): 183 | parsed_value = int(value) 184 | elif "." in value and all(part.isdigit() for part in value.split(".", 1)): 185 | parsed_value = float(value) 186 | else: 187 | parsed_value = value 188 | control_json = {key.strip(): parsed_value} 189 | except: 190 | control_json = {key.strip(): value.strip()} 191 | else: 192 | return f"控制数据格式错误,请提供有效的格式: JSON或key=value" 193 | else: 194 | control_json = control_data 195 | 196 | # 按照API要求,确保value是JSON字符串 197 | data = { 198 | "device_id": device_id, 199 | "value": json.dumps(control_json) 200 | } 201 | 202 | # 发送请求 203 | result = await client._request("POST", "/api/v1/telemetry/datas/pub", json_data=data) 204 | 205 | if result.get("code") != 200: 206 | return f"发送遥测控制命令失败:{result.get('message', '未知错误')}" 207 | 208 | return f"成功向设备 {device_id} 发送遥测控制命令: {json.dumps(control_json, ensure_ascii=False)}" 209 | 210 | except Exception as e: 211 | logger.error(f"发送遥测控制命令出错: {str(e)}") 212 | return f"发送遥测控制命令时发生错误: {str(e)}" 213 | 214 | async def set_device_attributes(device_id: str, attribute_data: Union[Dict[str, Any], str]) -> str: 215 | """ 216 | 设置设备属性 - 通用接口,可用于设置任何类型的设备属性 217 | 218 | 参数: 219 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 220 | attribute_data: 属性数据,格式如 {"ip": "127.0.0.1", "mac": "xx:xx:xx:xx:xx:xx", "port": 1883} 221 | """ 222 | client = ThingsPanelClient() 223 | try: 224 | # 处理不同格式的输入 225 | if isinstance(attribute_data, str): 226 | # 尝试解析为JSON 227 | try: 228 | attribute_json = json.loads(attribute_data) 229 | except json.JSONDecodeError: 230 | # 不是JSON格式,尝试解析为key=value格式 231 | if "=" in attribute_data: 232 | key, value = attribute_data.split("=", 1) 233 | attribute_json = {key.strip(): value.strip()} 234 | else: 235 | return f"属性数据格式错误,请提供有效的格式: JSON或key=value" 236 | else: 237 | attribute_json = attribute_data 238 | 239 | # 按照API要求,确保value是JSON字符串 240 | data = { 241 | "device_id": device_id, 242 | "value": json.dumps(attribute_json) 243 | } 244 | 245 | # 发送请求 246 | result = await client._request("POST", "/api/v1/attribute/datas/pub", json_data=data) 247 | 248 | if result.get("code") != 200: 249 | return f"设置设备属性失败:{result.get('message', '未知错误')}" 250 | 251 | return f"成功设置设备 {device_id} 的属性: {json.dumps(attribute_json, ensure_ascii=False)}" 252 | 253 | except Exception as e: 254 | logger.error(f"设置设备属性出错: {str(e)}") 255 | return f"设置设备属性时发生错误: {str(e)}" 256 | 257 | async def send_device_command(device_id: str, command_data: Union[Dict[str, Any], str], command_identifier: Optional[str] = None) -> str: 258 | """ 259 | 向设备发送控制命令,比如:打开卧室灯,在发送控件命令前,必须通过get_device_model_info查询设备物模型,确保命令名称和参数符合设备物模型要求。 260 | 261 | 参数: 262 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 263 | command_data: 命令数据,格式如 {"method": "ReSet", "params": {"switch": 1, "light": "close"}} 264 | command_identifier: 命令标识符,如果提供则使用此标识符 265 | 266 | 注意: 267 | 在发送命令前,应先使用get_device_model_info函数查询设备物模型,确保控制命令名称和参数符合设备物模型要求。 268 | """ 269 | client = ThingsPanelClient() 270 | try: 271 | # 处理不同格式的输入 272 | if isinstance(command_data, str): 273 | # 尝试解析为JSON 274 | try: 275 | command_json = json.loads(command_data) 276 | except json.JSONDecodeError: 277 | return f"命令数据格式错误,请提供有效的JSON格式。建议先使用get_device_model_info查询设备支持的命令。" 278 | else: 279 | command_json = command_data 280 | 281 | # 提取方法名,用于日志和提示 282 | method_name = "未知" 283 | if isinstance(command_json, dict) and "method" in command_json: 284 | method_name = command_json.get("method") 285 | 286 | # 添加物模型检查提示 287 | logger.info(f"准备向设备 {device_id} 发送 {method_name} 命令,请确保已通过get_device_model_info检查过设备物模型") 288 | 289 | # 从命令数据中提取标识符,如果没有提供 290 | if not command_identifier: 291 | if isinstance(command_json, dict) and "method" in command_json: 292 | command_identifier = command_json.get("method") 293 | else: 294 | command_identifier = "command" # 使用默认值 295 | 296 | # 从命令中提取params部分作为value 297 | params_value = None 298 | if isinstance(command_json, dict) and "params" in command_json: 299 | params_value = command_json.get("params") 300 | 301 | # 构建请求数据 302 | data = { 303 | "device_id": device_id, 304 | "Identify": command_identifier 305 | } 306 | 307 | # 只有当params存在时才添加value字段 308 | if params_value is not None: 309 | data["value"] = json.dumps(params_value) 310 | 311 | # 发送请求 312 | result = await client._request("POST", "/api/v1/command/datas/pub", json_data=data) 313 | 314 | if result.get("code") != 200: 315 | return f"发送设备命令失败:{result.get('message', '未知错误')}。建议检查设备物模型确认命令格式是否正确。" 316 | 317 | return f"成功向设备 {device_id} 发送命令: {method_name},参数: {json.dumps(params_value, ensure_ascii=False)}" 318 | 319 | except Exception as e: 320 | logger.error(f"发送设备命令出错: {str(e)}") 321 | return f"发送设备命令时发生错误: {str(e)}" 322 | 323 | async def control_device_with_model_check(device_id: str, command_type: str, command_data: Union[Dict[str, Any], str]) -> str: 324 | """ 325 | 先查询物模型,然后再发送控制命令的标准流程 326 | 327 | 参数: 328 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 329 | command_type: 命令类型,可选值:'telemetry'、'attribute'、'command' 330 | command_data: 命令数据 331 | 332 | 返回: 333 | 物模型信息和命令执行结果 334 | """ 335 | # 映射命令类型到物模型类型 336 | model_type_map = { 337 | 'telemetry': 'telemetry', 338 | 'attribute': 'attributes', 339 | 'command': 'commands' 340 | } 341 | 342 | # 获取对应的物模型类型 343 | model_type = model_type_map.get(command_type.lower()) 344 | if not model_type: 345 | return f"不支持的命令类型: {command_type},请选择 'telemetry'、'attribute' 或 'command'" 346 | 347 | # 查询对应类型的物模型 348 | model_info = await get_device_model_info(device_id, model_type=model_type) 349 | 350 | # 根据命令类型选择控制方法 351 | control_result = "" 352 | if command_type.lower() == 'telemetry': 353 | control_result = await control_device_telemetry(device_id, command_data) 354 | elif command_type.lower() == 'attribute': 355 | control_result = await set_device_attributes(device_id, command_data) 356 | elif command_type.lower() == 'command': 357 | if isinstance(command_data, dict) and "method" in command_data: 358 | control_result = await send_device_command(device_id, command_data) 359 | else: 360 | return f"命令数据格式错误,command类型必须包含method字段。请参考物模型调整命令格式。\n\n物模型信息:\n{model_info}" 361 | 362 | # 返回物模型信息和控制结果 363 | return f"设备物模型信息:\n{model_info}\n\n控制结果:\n{control_result}" -------------------------------------------------------------------------------- /src/thingspanel_mcp/tools/dashboard_tools.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/tools/dashboard_tools.py 2 | from typing import Dict, Any 3 | import logging 4 | from ..api_client import ThingsPanelClient 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | async def get_tenant_summary() -> str: 9 | """ 10 | 获取租户信息总览 11 | """ 12 | client = ThingsPanelClient() 13 | try: 14 | 15 | # 获取租户ID 16 | tenant_id_result = await client.get_tenant_id() 17 | tenant_id = tenant_id_result.get("data", "未知") if tenant_id_result.get("code") == 200 else "未知" 18 | 19 | # 获取设备信息 20 | devices_info_result = await client.get_tenant_devices_info() 21 | if devices_info_result.get("code") != 200: 22 | return f"获取设备信息失败:{devices_info_result.get('message', '未知错误')}" 23 | 24 | # 获取消息数量 25 | message_count_result = await client.get_message_count() 26 | message_count = message_count_result.get("data", {}).get("msg", 0) if message_count_result.get("code") == 200 else 0 27 | 28 | # 解析设备信息 29 | device_data = devices_info_result.get("data", {}) 30 | device_total = device_data.get("device_total", 0) 31 | device_on = device_data.get("device_on", 0) 32 | device_activity = device_data.get("device_activity", 0) 33 | 34 | # 计算设备在线率 35 | online_rate = (device_on / device_total * 100) if device_total > 0 else 0 36 | 37 | summary = ( 38 | f"租户ID: {tenant_id}\n\n" 39 | f"设备统计:\n" 40 | f"- 设备总数: {device_total}\n" 41 | f"- 在线设备: {device_on}\n" 42 | f"- 在线率: {online_rate:.2f}%\n" 43 | f"- 激活设备: {device_activity}\n\n" 44 | f"消息统计:\n" 45 | f"- 总消息数: {message_count}\n" 46 | ) 47 | 48 | return summary 49 | 50 | except Exception as e: 51 | logger.error(f"获取租户信息出错: {str(e)}") 52 | return f"获取租户信息时发生错误: {str(e)}" 53 | 54 | async def get_device_trend_report() -> str: 55 | """ 56 | 获取设备在线趋势报告 57 | """ 58 | client = ThingsPanelClient() 59 | try: 60 | result = await client.get_device_trend() 61 | 62 | if result.get("code") != 200: 63 | return f"获取设备趋势失败:{result.get('message', '未知错误')}" 64 | 65 | trend_data = result.get("data", {}).get("points", []) 66 | if not trend_data: 67 | return "没有找到设备趋势数据。" 68 | 69 | # 获取最新的几个数据点 70 | recent_points = trend_data[-5:] if len(trend_data) > 5 else trend_data 71 | 72 | trend_info = [] 73 | for point in recent_points: 74 | timestamp = point.get("timestamp", "未知") 75 | device_total = point.get("device_total", 0) 76 | device_online = point.get("device_online", 0) 77 | device_offline = point.get("device_offline", 0) 78 | online_rate = (device_online / device_total * 100) if device_total > 0 else 0 79 | 80 | trend_info.append( 81 | f"时间: {timestamp}\n" 82 | f"设备总数: {device_total}\n" 83 | f"在线设备: {device_online}\n" 84 | f"离线设备: {device_offline}\n" 85 | f"在线率: {online_rate:.2f}%\n" 86 | ) 87 | 88 | return "设备在线趋势报告 (最近时间点):\n\n" + "\n".join(trend_info) 89 | 90 | except Exception as e: 91 | logger.error(f"获取设备趋势出错: {str(e)}") 92 | return f"获取设备趋势时发生错误: {str(e)}" -------------------------------------------------------------------------------- /src/thingspanel_mcp/tools/device_tools.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/tools/device_tools.py 2 | from typing import Dict, List, Any, Optional 3 | import logging 4 | from ..api_client import ThingsPanelClient 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | async def list_devices(search: Optional[str] = None, page: int = 1, page_size: int = 10) -> str: 9 | """ 10 | 可以根据用户说出来的设备名称或设备编号进行模糊搜索,获取设备ID、设备名称、设备编号、在线状态、激活状态、创建时间 11 | 注意:支持大小写不敏感的模糊搜索,如果1次没搜索到可尝试分词搜索,按自然单词或者空格拆分,如果分词搜索也没搜索到就及时反馈用户 12 | """ 13 | client = ThingsPanelClient() 14 | try: 15 | result = await client.get_devices(page=page, page_size=page_size, search=search) 16 | 17 | if result.get("code") != 200: 18 | return f"获取设备列表失败:{result.get('message', '未知错误')}" 19 | 20 | devices = result.get("data", {}).get("list", []) 21 | total = result.get("data", {}).get("total", 0) 22 | 23 | if not devices: 24 | return "没有找到符合条件的设备。" 25 | 26 | device_info = [] 27 | for device in devices: 28 | status = "在线" if device.get("is_online") == 1 else "离线" 29 | 30 | # 检查激活状态 31 | activate_status = "未激活" 32 | if device.get("activate_flag") == "active": 33 | activate_status = "已激活" 34 | 35 | device_info.append( 36 | f"设备ID: {device.get('id')}\n" 37 | f"设备名称: {device.get('name')}\n" 38 | f"设备编号: {device.get('device_number')}\n" 39 | f"设备模板类型: {device.get('device_config_name', '未设置')}\n" 40 | f"配置类型: {device.get('access_way', '未知')}\n" 41 | f"在线状态: {status}\n" 42 | f"激活状态: {activate_status}\n" 43 | f"创建时间: {device.get('created_at', '未知')}\n" 44 | ) 45 | 46 | header = f"共找到 {total} 个设备,当前显示第 {page} 页,每页 {page_size} 条:\n\n" 47 | return header + "\n".join(device_info) 48 | 49 | except Exception as e: 50 | logger.error(f"获取设备列表出错: {str(e)}") 51 | return f"获取设备列表时发生错误: {str(e)}" 52 | 53 | async def get_device_detail(device_id: str) -> str: 54 | """ 55 | 根据设备ID获取设备详细信息 56 | 57 | 参数: 58 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 59 | """ 60 | client = ThingsPanelClient() 61 | try: 62 | result = await client.get_device_detail(device_id) 63 | 64 | if result.get("code") != 200: 65 | return f"获取设备详情失败:{result.get('message', '未知错误')}" 66 | 67 | device = result.get("data", {}) 68 | if not device: 69 | return f"未找到设备 {device_id} 的详情信息。" 70 | 71 | is_online = "在线" if device.get("is_online") == 1 else "离线" 72 | activate_flag = "已激活" if device.get("activate_flag") == "active" else "未激活" 73 | is_enabled = "已启用" if device.get("is_enabled") == "enabled" else "已禁用" 74 | 75 | detail_info = ( 76 | f"设备ID: {device.get('id')}\n" 77 | f"设备名称: {device.get('name')}\n" 78 | f"设备编号: {device.get('device_number', '未设置')}\n" 79 | f"在线状态: {is_online}\n" 80 | f"激活状态: {activate_flag}\n" 81 | f"启用状态: {is_enabled}\n" 82 | f"接入方式: {device.get('access_way', '未知')}\n" 83 | f"创建时间: {device.get('created_at', '未知')}\n" 84 | f"更新时间: {device.get('update_at', '未知')}\n" 85 | ) 86 | 87 | # 如果有设备配置信息,添加到详情中 88 | if device.get("device_config"): 89 | config = device.get("device_config", {}) 90 | detail_info += ( 91 | f"\n设备配置信息:\n" 92 | f"配置名称: {config.get('name', '未知')}\n" 93 | f"设备类型: {config.get('device_type', '未知')}\n" 94 | f"协议类型: {config.get('protocol_type', '未知')}\n" 95 | f"凭证类型: {config.get('voucher_type', '未知')}\n" 96 | ) 97 | 98 | return detail_info 99 | 100 | except Exception as e: 101 | logger.error(f"获取设备详情出错: {str(e)}") 102 | return f"获取设备详情时发生错误: {str(e)}" 103 | 104 | async def check_device_status(device_id: str) -> str: 105 | """ 106 | 检查设备的在线状态 107 | 108 | 参数: 109 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 110 | """ 111 | client = ThingsPanelClient() 112 | try: 113 | result = await client.get_device_online_status(device_id) 114 | 115 | if result.get("code") != 200: 116 | return f"获取设备状态失败:{result.get('message', '未知错误')}" 117 | 118 | status_data = result.get("data", {}) 119 | is_online = status_data.get("is_online", 0) 120 | 121 | if is_online == 1: 122 | return f"设备 {device_id} 当前状态:在线" 123 | else: 124 | return f"设备 {device_id} 当前状态:离线" 125 | 126 | except Exception as e: 127 | logger.error(f"检查设备状态出错: {str(e)}") 128 | return f"检查设备状态时发生错误: {str(e)}" -------------------------------------------------------------------------------- /src/thingspanel_mcp/tools/telemetry_tools.py: -------------------------------------------------------------------------------- 1 | # src/thingspanel_mcp/tools/telemetry_tools.py 2 | from typing import Dict, List, Any, Optional, Union 3 | import logging 4 | import json 5 | from ..api_client import ThingsPanelClient 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | async def get_device_telemetry(device_id: str) -> str: 10 | """ 11 | 根据设备ID获取设备最新遥测数据 12 | 13 | 参数: 14 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 15 | """ 16 | client = ThingsPanelClient() 17 | try: 18 | result = await client.get_current_telemetry(device_id) 19 | 20 | if result.get("code") != 200: 21 | return f"获取设备遥测数据失败:{result.get('message', '未知错误')}" 22 | 23 | telemetry_data = result.get("data", []) 24 | if not telemetry_data: 25 | return f"设备 {device_id} 没有遥测数据。" 26 | 27 | telemetry_info = [] 28 | for item in telemetry_data: 29 | key = item.get("key", "未知") 30 | value = item.get("value", "未知") 31 | timestamp = item.get("ts", "未知") 32 | unit = item.get("unit", "") 33 | label = item.get("label", key) 34 | 35 | telemetry_info.append( 36 | f"指标: {label} ({key})\n" 37 | f"值: {value} {unit}\n" 38 | f"时间: {timestamp}\n" 39 | ) 40 | 41 | return f"设备 {device_id} 的遥测数据:\n\n" + "\n".join(telemetry_info) 42 | 43 | except Exception as e: 44 | logger.error(f"获取设备遥测数据出错: {str(e)}") 45 | return f"获取设备遥测数据时发生错误: {str(e)}" 46 | 47 | async def get_telemetry_by_key(device_id: str, key: str) -> str: 48 | """ 49 | 根据指定的key获取设备遥测当前数据 50 | 51 | 参数: 52 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 53 | key: 要查询的遥测数据键名,如"status"、"temperature";请不要自行猜测,需要从设备物模型查询接口中确认是哪个key 54 | """ 55 | client = ThingsPanelClient() 56 | try: 57 | result = await client.get_telemetry_by_keys(device_id, [key]) 58 | 59 | if result.get("code") != 200: 60 | return f"获取遥测数据失败:{result.get('message', '未知错误')}" 61 | 62 | telemetry_data = result.get("data", []) 63 | if not telemetry_data: 64 | return f"设备 {device_id} 没有key为 {key} 的遥测数据。" 65 | 66 | data_item = telemetry_data[0] 67 | value = data_item.get("value", "未知") 68 | timestamp = data_item.get("ts", "未知") 69 | unit = data_item.get("unit", "") 70 | label = data_item.get("label", key) 71 | 72 | return ( 73 | f"设备 {device_id} 的 {label} ({key}) 数据:\n" 74 | f"值: {value} {unit}\n" 75 | f"时间: {timestamp}" 76 | ) 77 | 78 | except Exception as e: 79 | logger.error(f"获取遥测数据出错: {str(e)}") 80 | return f"获取遥测数据时发生错误: {str(e)}" 81 | 82 | async def get_telemetry_history( 83 | device_id: str, 84 | key: str, 85 | time_range: str = "last_1h", 86 | aggregate_window: str = "no_aggregate", 87 | aggregate_function: Optional[str] = None 88 | ) -> str: 89 | """ 90 | 获取设备遥测数据历史数据 91 | 92 | 参数: 93 | device_id: 设备ID示例"4f7040db-8a9c-4c81-d85b-fe574b8a3fa9",如果只知道设备名称,请先模糊搜索列表确认具体是哪个设备ID 94 | key: 要查询的遥测数据键名,如"status"、"temperature";请不要自行猜测,需要从设备物模型查询接口中确认是哪个key 95 | """ 96 | client = ThingsPanelClient() 97 | try: 98 | result = await client.get_telemetry_statistics( 99 | device_id=device_id, 100 | key=key, 101 | time_range=time_range, 102 | aggregate_window=aggregate_window, 103 | aggregate_function=aggregate_function 104 | ) 105 | 106 | if result.get("code") != 200: 107 | return f"获取遥测历史数据失败:{result.get('message', '未知错误')}" 108 | 109 | data = result.get("data", {}) 110 | 111 | # 检查data是否为列表 112 | if isinstance(data, list): 113 | # 处理列表格式的返回数据 114 | if not data: 115 | return f"设备 {device_id} 在所选时间范围内没有 {key} 的历史数据。" 116 | 117 | # 直接展示所有数据点 118 | data_points = [] 119 | for point in data: 120 | data_points.append(f"时间: {point.get('x')}, 值: {point.get('y')}") 121 | 122 | return ( 123 | f"设备 {device_id} 的 {key} 历史数据:\n" 124 | f"数据点数量: {len(data)}\n\n" 125 | f"数据点列表:\n" + 126 | "\n".join(data_points) 127 | ) 128 | 129 | # 原有逻辑,处理字典格式的返回数据 130 | time_series = data.get("time_series", []) 131 | time_range_info = data.get("x_time_range", {}) 132 | 133 | if not time_series: 134 | return f"设备 {device_id} 在所选时间范围内没有 {key} 的历史数据。" 135 | 136 | # 获取时间范围信息 137 | start_time = time_range_info.get("start", "未知") 138 | end_time = time_range_info.get("end", "未知") 139 | 140 | # 选取一部分数据点展示(最多10个) 141 | display_count = min(10, len(time_series)) 142 | step = len(time_series) // display_count if len(time_series) > display_count else 1 143 | 144 | data_points = [] 145 | for i in range(0, len(time_series), step): 146 | if len(data_points) >= display_count: 147 | break 148 | 149 | point = time_series[i] 150 | data_points.append(f"时间: {point.get('x')}, 值: {point.get('y')}") 151 | 152 | time_range_desc = { 153 | "last_5m": "最近5分钟", 154 | "last_15m": "最近15分钟", 155 | "last_30m": "最近30分钟", 156 | "last_1h": "最近1小时", 157 | "last_3h": "最近3小时", 158 | "last_6h": "最近6小时", 159 | "last_12h": "最近12小时", 160 | "last_24h": "最近24小时", 161 | "last_3d": "最近3天", 162 | "last_7d": "最近7天", 163 | "last_15d": "最近15天", 164 | "last_30d": "最近30天", 165 | "last_60d": "最近60天", 166 | "last_90d": "最近90天", 167 | "last_6m": "最近6个月", 168 | "last_1y": "最近1年", 169 | "custom": "自定义时间范围" 170 | }.get(time_range, time_range) 171 | 172 | agg_desc = "无聚合" if aggregate_window == "no_aggregate" else f"聚合间隔: {aggregate_window}" 173 | if aggregate_function and aggregate_window != "no_aggregate": 174 | agg_func_desc = { 175 | "avg": "平均值", 176 | "max": "最大值", 177 | "min": "最小值", 178 | "sum": "总和", 179 | "diff": "差值" 180 | }.get(aggregate_function, aggregate_function) 181 | agg_desc += f", 聚合方法: {agg_func_desc}" 182 | 183 | return ( 184 | f"设备 {device_id} 的 {key} 历史数据 ({time_range_desc}):\n" 185 | f"时间范围: {start_time} 至 {end_time}\n" 186 | f"{agg_desc}\n" 187 | f"数据点数量: {len(time_series)}\n\n" 188 | f"数据样例 (显示 {len(data_points)} 个数据点):\n" + 189 | "\n".join(data_points) 190 | ) 191 | 192 | except Exception as e: 193 | logger.error(f"获取遥测历史数据出错: {str(e)}") 194 | return f"获取遥测历史数据时发生错误: {str(e)}" --------------------------------------------------------------------------------