├── .DS_Store ├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── README_EN.md ├── README_PYPI.md ├── config.yaml ├── docs ├── en │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ └── testing.md ├── images │ ├── README.md │ ├── client_config.png │ ├── cline_config.png │ ├── config.png │ ├── connect.jpg │ ├── howitworks.png │ ├── logo.png │ ├── stru_chs.png │ ├── stru_eng.PNG │ ├── test_output.png │ ├── workflow_chs.png │ └── workflow_eng.png └── zh │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ └── testing.md ├── install.py ├── install_macos.py ├── install_ubuntu.py ├── pyproject.toml ├── src └── mcp2mqtt │ ├── __init__.py │ └── server.py ├── tests ├── __init__.py ├── responder.py ├── test_mqtt_server.py └── test_server.py └── uv.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.11 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mcp2mqtt Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mcp2mqtt: 连接物理世界与AI大模型的桥梁 2 | 3 | [English](README_EN.md) | 简体中文 4 | 5 |
6 | mcp2mqtt Logo 7 |

通过自然语言控制硬件,开启物联网新纪元

8 |
9 | 10 | ## 系统架构 11 | 12 |
13 | 系统架构图 14 |

mcp2mqtt 系统架构图

15 |
16 | 17 | ## 工作流程 18 | 19 |
20 | 工作流程图 21 |

mcp2mqtt 工作流程图

22 |
23 | 24 | ## 项目愿景 25 | 26 | mcp2mqtt 是一个将物联网设备接入AI大模型的项目,它通过 Model Context Protocol (MCP) 和 MQTT 协议将物理世界与 AI 大模型无缝连接。最终实现: 27 | - 用自然语言控制你的硬件设备 28 | - AI 实时响应并调整物理参数 29 | - 让你的设备具备理解和执行复杂指令的能力 30 | - 通过MQTT协议实现设备间的互联互通 31 | 32 | ## 主要特性 33 | 34 | - **智能MQTT通信** 35 | - 支持MQTT协议的发布/订阅模式 36 | - 支持多种MQTT服务器(如Mosquitto、EMQ X等) 37 | - 支持QoS服务质量保证 38 | - 支持主题过滤和消息路由 39 | - 实时状态监控和错误处理 40 | 41 | - **MCP 协议集成** 42 | - 完整支持 Model Context Protocol 43 | - 支持资源管理和工具调用 44 | - 灵活的提示词系统 45 | - 通过MQTT实现命令的发布与响应 46 | 47 | ## 配置说明 48 | 49 | ### MQTT配置 50 | ```yaml 51 | mqtt: 52 | broker: "localhost" # MQTT服务器地址 53 | port: 1883 # MQTT服务器端口 54 | client_id: "mcp2mqtt_client" # MQTT客户端ID 55 | username: "mqtt_user" # MQTT用户名 56 | password: "mqtt_password" # MQTT密码 57 | keepalive: 60 # 保持连接时间 58 | topics: 59 | command: 60 | publish: "mcp/command" # 发送命令的主题 61 | subscribe: "mcp/response" # 接收响应的主题 62 | status: 63 | publish: "mcp/status" # 发送状态的主题 64 | subscribe: "mcp/control" # 接收控制命令的主题 65 | ``` 66 | 67 | ### 命令配置 68 | ```yaml 69 | commands: 70 | set_pwm: 71 | command: "CMD_PWM {frequency}" 72 | need_parse: false 73 | data_type: "ascii" 74 | prompts: 75 | - "把PWM调到最大" 76 | - "把PWM调到最小" 77 | mqtt_topic: "mcp/pwm" # MQTT发布主题 78 | response_topic: "mcp/pwm/response" # MQTT响应主题 79 | ``` 80 | 81 | ## MQTT 命令和响应 82 | 83 | ### 命令格式 84 | 85 | 命令使用简单的文本格式: 86 | 87 | 1. PWM 控制: 88 | - 命令:`PWM {值}` 89 | - 示例: 90 | - `PWM 100`(最大值) 91 | - `PWM 0`(关闭) 92 | - `PWM 50`(50%) 93 | - 响应:`CMD PWM {值} OK` 94 | 95 | 2. LED 控制: 96 | - 命令:`LED {状态}` 97 | - 示例: 98 | - `LED on`(打开) 99 | - `LED off`(关闭) 100 | - 响应:`CMD LED {状态} OK` 101 | 102 | 3. 设备信息: 103 | - 命令:`INFO` 104 | - 响应:`CMD INFO {设备信息}` 105 | 106 | ### 错误响应 107 | 108 | 如果发生错误,响应格式将为: 109 | `ERROR: {错误信息}` 110 | 111 | ## 支持的客户端 112 | 113 | mcp2mqtt 支持所有实现了 MCP 协议的客户端,以及支持MQTT协议的物联网设备: 114 | 115 | | 客户端类型 | 特性支持 | 说明 | 116 | |--------|----------|------| 117 | | Claude Desktop | 完整支持 | 推荐使用,支持所有 MCP 功能 | 118 | | Continue | 完整支持 | 优秀的开发工具集成 | 119 | | Cline | 资源+工具 | 支持多种 AI 提供商 | 120 | | MQTT设备 | 发布/订阅 | 支持所有MQTT协议的物联网设备 | 121 | 122 | ## 快速开始 123 | 124 | ### 1. 安装 125 | 126 | #### Windows用户 127 | 下载 [install.py](https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install.py) 128 | ```bash 129 | python install.py 130 | ``` 131 | #### macOS用户 132 | ```bash 133 | # 下载安装脚本 134 | curl -O https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install_macos.py 135 | 136 | # 运行安装脚本 137 | python3 install_macos.py 138 | ``` 139 | 140 | #### Ubuntu/Raspberry Pi用户 141 | ```bash 142 | # 下载安装脚本 143 | curl -O https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install_ubuntu.py 144 | 145 | # 运行安装脚本 146 | python3 install_ubuntu.py 147 | ``` 148 | 149 | 安装脚本会自动完成以下操作: 150 | - 检查系统环境 151 | - 安装必要的依赖 152 | - 创建默认配置文件 153 | - 配置Claude桌面版(如果已安装) 154 | 155 | ### 手动分步安装依赖 156 | ```bash 157 | windows 158 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 159 | MacOS 160 | curl -LsSf https://astral.sh/uv/install.sh | sh 161 | ``` 162 | 主要依赖uv工具,所以当python和uv以及Claude或Cline安装好后就可以了。 163 | 164 | ### 基本配置 165 | 在你的 MCP 客户端(如 Claude Desktop 或 Cline)配置文件中添加以下内容: 166 | 注意:如果使用的自动安装那么会自动配置Calude Desktop无需此步。 167 | 使用默认配置文件: 168 | ```json 169 | { 170 | "mcpServers": { 171 | "mcp2mqtt": { 172 | "command": "uvx", 173 | "args": [ 174 | "mcp2mqtt" 175 | ] 176 | } 177 | } 178 | } 179 | ``` 180 | > 注意:修改配置后需要重启Cline或者Claude客户端软件 181 | ## 配置说明 182 | ### 配置文件位置 183 | 复制配置文件(`config.yaml`)可以放在位置: 184 | 用户主目录(推荐个人使用) 185 | ```bash 186 | # Windows系统 187 | C:\Users\用户名\.mcp2mqtt\config.yaml 188 | 189 | # macOS系统 190 | /Users/用户名/.mcp2mqtt/config.yaml 191 | 192 | # Linux系统 193 | /home/用户名/.mcp2mqtt/config.yaml 194 | ``` 195 | - 适用场景:个人配置 196 | - 需要创建 `.mcp2mqtt` 目录: 197 | ```bash 198 | # Windows系统(在命令提示符中) 199 | mkdir "%USERPROFILE%\.mcp2mqtt" 200 | 201 | # macOS/Linux系统 202 | mkdir -p ~/.mcp2mqtt 203 | ``` 204 | 205 | 指定配置文件: 206 | 比如指定加载Pico配置文件:Pico_config.yaml 207 | ```json 208 | { 209 | "mcpServers": { 210 | "mcp2mqtt": { 211 | "command": "uvx", 212 | "args": [ 213 | "mcp2mqtt", 214 | "--config", 215 | "Pico" //指定配置文件名,不需要添加_config.yaml后缀 216 | ] 217 | } 218 | } 219 | } 220 | ``` 221 | 为了能使用多个mqtt,我们可以新增多个mcp2mqtt的服务 指定不同的配置文件名即可。 222 | 如果要接入多个设备,如有要连接第二个设备: 223 | 指定加载Pico2配置文件:Pico2_config.yaml 224 | ```json 225 | { 226 | "mcpServers": { 227 | "mcp2mqtt2": { 228 | "command": "uvx", 229 | "args": [ 230 | "mcp2mqtt", 231 | "--config", 232 | "Pico2" //指定配置文件名,不需要添加_config.yaml后缀 233 | ] 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | ### 硬件连接 240 | 1. 将你的设备通过网络连接到mqtt服务器 241 | 2. 也可以用tests目录下的responder.py来模拟设备 242 | 243 | ## 运行测试 244 | 245 | ### 启动设备模拟器 246 | 247 | 项目在 `tests` 目录中包含了一个设备模拟器。它可以模拟一个硬件设备,能够: 248 | - 响应 PWM 控制命令 249 | - 提供设备信息 250 | - 控制 LED 状态 251 | 252 | 启动模拟器: 253 | ```bash 254 | python tests/responder.py 255 | ``` 256 | 257 | 你应该能看到模拟器正在运行并已连接到 MQTT 服务器的输出信息。 258 | 259 | ### 启动客户端Claude 桌面版或Cline 260 |
261 | Cline Configuration Example 262 |

Example in Cline

263 |
264 | 265 | ### 从源码快速开始 266 | 1. 从源码安装 267 | ```bash 268 | # 通过源码安装: 269 | git clone https://github.com/mcp2everything/mcp2mqtt.git 270 | cd mcp2mqtt 271 | 272 | # 创建虚拟环境 273 | uv venv .venv 274 | 275 | # 激活虚拟环境 276 | # Windows: 277 | .venv\Scripts\activate 278 | # Linux/macOS: 279 | source .venv/bin/activate 280 | 281 | # 安装开发依赖 282 | uv pip install --editable . 283 | ``` 284 | 285 | ### MCP客户端配置 286 | 在使用支持MCP协议的客户端(如Claude Desktop或Cline)时,需要在客户端的配置文件中添加以下内容: 287 | 直接自动安装的配置方式 288 | 源码开发的配置方式 289 | #### 使用默认演示参数: 290 | ```json 291 | { 292 | "mcpServers": { 293 | "mcp2mqtt": { 294 | "command": "uv", 295 | "args": [ 296 | "--directory", 297 | "你的实际路径/mcp2mqtt", // 例如: "C:/Users/Administrator/Documents/develop/my-mcp-server/mcp2mqtt" 298 | "run", 299 | "mcp2mqtt" 300 | ] 301 | } 302 | } 303 | } 304 | ``` 305 | #### 指定参数文件名 306 | ```json 307 | { 308 | "mcpServers": { 309 | "mcp2mqtt": { 310 | "command": "uv", 311 | "args": [ 312 | "--directory", 313 | "你的实际路径/mcp2mqtt", // 例如: "C:/Users/Administrator/Documents/develop/my-mcp-server/mcp2mqtt" 314 | "run", 315 | "mcp2mqtt", 316 | "--config", // 可选参数,指定配置文件名 317 | "Pico" // 可选参数,指定配置文件名,不需要添加_config.yaml后缀 318 | ] 319 | } 320 | } 321 | } 322 | ``` 323 |
324 | Cline Configuration Example 325 |

Example in Cline

326 |
327 | ### 配置文件位置 328 | 配置文件(`config.yaml`)可以放在不同位置,程序会按以下顺序查找: 329 | #### 1. 当前工作目录(适合开发测试) 330 | - 路径:`./config.yaml` 331 | - 示例:如果你在 `C:\Projects` 运行程序,它会查找 `C:\Projects\config.yaml` 332 | - 适用场景:开发和测试 333 | - 不需要特殊权限 334 | 335 | #### 2. 用户主目录(推荐个人使用) 336 | ```bash 337 | # Windows系统 338 | C:\Users\用户名\.mcp2mqtt\config.yaml 339 | 340 | # macOS系统 341 | /Users/用户名/.mcp2mqtt/config.yaml 342 | 343 | # Linux系统 344 | /home/用户名/.mcp2mqtt/config.yaml 345 | ``` 346 | - 适用场景:个人配置 347 | - 需要创建 `.mcp2mqtt` 目录: 348 | ```bash 349 | # Windows系统(在命令提示符中) 350 | mkdir "%USERPROFILE%\.mcp2mqtt" 351 | 352 | # macOS/Linux系统 353 | mkdir -p ~/.mcp2mqtt 354 | ``` 355 | 356 | #### 3. 系统级配置(适合多用户环境) 357 | ```bash 358 | # Windows系统(需要管理员权限) 359 | C:\ProgramData\mcp2mqtt\config.yaml 360 | 361 | # macOS/Linux系统(需要root权限) 362 | /etc/mcp2mqtt/config.yaml 363 | ``` 364 | - 适用场景:多用户共享配置 365 | - 创建目录并设置权限: 366 | ```bash 367 | # Windows系统(以管理员身份运行) 368 | mkdir "C:\ProgramData\mcp2mqtt" 369 | 370 | # macOS/Linux系统(以root身份运行) 371 | sudo mkdir -p /etc/mcp2mqtt 372 | sudo chown root:root /etc/mcp2mqtt 373 | sudo chmod 755 /etc/mcp2mqtt 374 | ``` 375 | 376 | 程序会按照上述顺序查找配置文件,使用找到的第一个有效配置文件。根据你的需求选择合适的位置: 377 | - 开发测试:使用当前目录 378 | - 个人使用:建议使用用户主目录(推荐) 379 | - 多用户环境:使用系统级配置(ProgramData或/etc) 380 | 381 | 3. 运行服务器: 382 | ```bash 383 | # 确保已激活虚拟环境 384 | .venv\Scripts\activate 385 | 386 | # 运行服务器(使用默认配置config.yaml 案例中用的LOOP_BACK 模拟串口,无需真实串口和串口设备) 387 | uv run src/mcp2mqtt/server.py 388 | 或 389 | uv run mcp2mqtt 390 | # 运行服务器(使用指定配置Pico_config.yaml) 391 | uv run src/mcp2mqtt/server.py --config Pico 392 | 或 393 | uv run mcp2mqtt --config Pico 394 | ``` 395 | 396 | 397 | ## 文档 398 | 399 | - [安装指南](./docs/zh/installation.md) 400 | - [API文档](./docs/zh/api.md) 401 | - [配置说明](./docs/zh/configuration.md) -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # mcp2mqtt: Bridge Between Physical World and AI Large Models 2 | 3 | English | [简体中文](README.md) 4 | 5 |
6 | mcp2mqtt Logo 7 |

Control Hardware Through Natural Language, Opening a New Era of IoT

8 |
9 | 10 | mcp2mqtt is a serial communication server based on the MCP service interface protocol, designed for communication with serial devices. It provides a simple configuration approach for defining and managing serial commands. 11 | 12 | ## Features 13 | 14 | - 🔌 Automatic serial port detection and connection management 15 | - 📝 Simple YAML configuration 16 | - 🛠️ Customizable commands and response parsing 17 | - 🌐 Multi-language prompt support 18 | - 🚀 Asynchronous communication support 19 | - Auto-detect and connect to serial ports at 115200 baud rate 20 | - Control PWM frequency (range: 0-100) 21 | - Compliant with Claude MCP protocol 22 | - Comprehensive error handling and status feedback 23 | - Cross-platform support (Windows, Linux, macOS) 24 | - **Smart MQTT Communication** 25 | - Support for MQTT protocol publish/subscribe model 26 | - Support for various MQTT servers (e.g., Mosquitto, EMQ X) 27 | - QoS (Quality of Service) guarantee 28 | - Topic filtering and message routing 29 | - Real-time status monitoring and error handling 30 | 31 | ## System Architecture 32 | 33 |
34 | System Architecture 35 |

mcp2mqtt System Architecture

36 |
37 | 38 | ## Workflow 39 | 40 |
41 | Workflow 42 |

mcp2mqtt Workflow

43 |
44 | 45 | ## Project Vision 46 | 47 | mcp2mqtt is a project that connects IoT devices with AI large models through Model Context Protocol (MCP) and MQTT protocol, achieving seamless integration between the physical world and AI models. The ultimate goals are: 48 | - Control your hardware devices using natural language 49 | - Real-time AI response and physical parameter adjustment 50 | - Enable your devices to understand and execute complex instructions 51 | - Enable device interconnection through MQTT protocol 52 | 53 | ## Key Features 54 | 55 | - **MCP Protocol Integration** 56 | - Complete support for Model Context Protocol 57 | - Resource management and tool invocation support 58 | - Flexible prompt system 59 | - Command publishing and response through MQTT 60 | 61 | ## Configuration Guide 62 | 63 | ### MQTT Configuration 64 | ```yaml 65 | mqtt: 66 | broker: "localhost" # MQTT server address 67 | port: 1883 # MQTT server port 68 | client_id: "mcp2mqtt_client" # MQTT client ID 69 | username: "mqtt_user" # MQTT username 70 | password: "mqtt_password" # MQTT password 71 | keepalive: 60 # Keep-alive time 72 | topics: 73 | command: 74 | publish: "mcp/command" # Command publishing topic 75 | subscribe: "mcp/response" # Response subscription topic 76 | status: 77 | publish: "mcp/status" # Status publishing topic 78 | subscribe: "mcp/control" # Control command subscription topic 79 | ``` 80 | 81 | ### Command Configuration 82 | ```yaml 83 | commands: 84 | set_pwm: 85 | command: "CMD_PWM {frequency}" 86 | need_parse: false 87 | data_type: "ascii" 88 | prompts: 89 | - "Set PWM to maximum" 90 | - "Set PWM to minimum" 91 | mqtt_topic: "mcp/pwm" # MQTT publish topic 92 | response_topic: "mcp/pwm/response" # MQTT response topic 93 | ``` 94 | 95 | ## Supported Clients 96 | 97 | mcp2mqtt supports all clients implementing the MCP protocol and IoT devices supporting the MQTT protocol: 98 | 99 | | Client Type | Feature Support | Description | 100 | |------------|----------------|-------------| 101 | | Claude Desktop | Full Support | Recommended, supports all MCP features | 102 | | Continue | Full Support | Excellent development tool integration | 103 | | Cline | Resource + Tools | Supports multiple AI providers | 104 | | MQTT Devices | Pub/Sub | Supports all MQTT-compatible IoT devices | 105 | 106 | ## Quick Start 107 | 108 | ### Prepare 109 | Python>=3.11 110 | Claude Desktop or Cline+Vscode 111 | 112 | 113 | ### Installation 114 | 115 | #### For Windows Users 116 | ```bash 117 | # Download the installation script 118 | curl -O https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install.py 119 | 120 | # Run the installation script 121 | python install.py 122 | ``` 123 | 124 | #### For macOS Users 125 | ```bash 126 | # Download the installation script 127 | curl -O https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install_macos.py 128 | 129 | # Run the installation script 130 | python3 install_macos.py 131 | ``` 132 | 133 | #### For Ubuntu/Raspberry Pi Users 134 | ```bash 135 | # Download the installation script 136 | curl -O https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/main/install_ubuntu.py 137 | 138 | # Run the installation script 139 | python3 install_ubuntu.py 140 | ``` 141 | 142 | The installation script will automatically: 143 | - ✅ Check system environment 144 | - ✅ Install required dependencies 145 | - ✅ Create default configuration file 146 | - ✅ Configure Claude Desktop (if installed) 147 | - ✅ Check serial devices 148 | 149 | ### Configuration File Location 150 | 151 | The configuration file (`config.yaml`) can be placed in different locations depending on your needs: 152 | 153 | #### 1. Current Working Directory (For Development) 154 | - Path: `./config.yaml` 155 | - Example: If you run the program from `C:\Projects`, it will look for `C:\Projects\config.yaml` 156 | - Best for: Development and testing 157 | - No special permissions required 158 | 159 | #### 2. User's Home Directory (Recommended for Personal Use) 160 | ```bash 161 | # Windows 162 | C:\Users\YourName\.mcp2mqtt\config.yaml 163 | 164 | # macOS 165 | /Users/YourName/.mcp2mqtt/config.yaml 166 | 167 | # Linux 168 | /home/username/.mcp2mqtt/config.yaml 169 | ``` 170 | - Best for: Personal configuration 171 | - Create the `.mcp2mqtt` directory if it doesn't exist: 172 | ```bash 173 | # Windows (in Command Prompt) 174 | mkdir "%USERPROFILE%\.mcp2mqtt" 175 | 176 | # macOS/Linux 177 | mkdir -p ~/.mcp2mqtt 178 | ``` 179 | 180 | #### 3. System-wide Configuration (For Multi-user Setup) 181 | ```bash 182 | # Windows (requires admin rights) 183 | C:\ProgramData\mcp2mqtt\config.yaml 184 | 185 | # macOS/Linux (requires sudo/root) 186 | /etc/mcp2mqtt/config.yaml 187 | ``` 188 | - Best for: Shared configuration in multi-user environments 189 | - Create the directory with appropriate permissions: 190 | ```bash 191 | # Windows (as administrator) 192 | mkdir "C:\ProgramData\mcp2mqtt" 193 | 194 | # macOS/Linux (as root) 195 | sudo mkdir -p /etc/mcp2mqtt 196 | sudo chown root:root /etc/mcp2mqtt 197 | sudo chmod 755 /etc/mcp2mqtt 198 | ``` 199 | 200 | The program searches for the configuration file in this order and uses the first valid file it finds. Choose the location based on your needs: 201 | - For testing: use current directory 202 | - For personal use: use home directory (recommended) 203 | - For system-wide settings: use ProgramData or /etc 204 | 205 | ### Serial Port Configuration 206 | 207 | Configure serial port and commands in `config.yaml`: 208 | ```yaml 209 | # config.yaml 210 | serial: 211 | port: COM11 # or auto-detect 212 | baud_rate: 115200 213 | 214 | commands: 215 | set_pwm: 216 | command: "PWM {frequency}\n" 217 | need_parse: false 218 | prompts: 219 | - "Set PWM to {value}%" 220 | ``` 221 | 222 | 3.MCP json Configuration 223 | Add the following to your MCP client (like Claude Desktop or Cline) configuration file, making sure to update the path to your actual installation path: 224 | 225 | ```json 226 | { 227 | "mcpServers": { 228 | "mcp2mqtt": { 229 | "command": "uvx", 230 | "args": ["mcp2mqtt"] 231 | } 232 | } 233 | } 234 | ``` 235 | if you want to develop locally, you can use the following configuration: 236 | ```json 237 | { 238 | "mcpServers": { 239 | "mcp2mqtt": { 240 | "command": "uv", 241 | "args": [ 242 | "--directory", 243 | "your project path/mcp2mqtt", // ex: "C:/Users/Administrator/Documents/develop/my-mcp-server/mcp2mqtt" 244 | "run", 245 | "mcp2mqtt" 246 | ] 247 | } 248 | } 249 | } 250 | ``` 251 | 252 | > **Important Notes:** 253 | > 1. Use absolute paths only 254 | > 2. Use forward slashes (/) or double backslashes (\\) as path separators 255 | > 3. Ensure the path points to your actual project installation directory 256 | 257 | 258 | 259 | 4. launch your client(claude desktop or cline): 260 | 261 | 262 | ## Interacting with Claude 263 | 264 | Once the service is running, you can control PWM through natural language conversations with Claude. Here are some example prompts: 265 | 266 | - "Set PWM to 50%" 267 | - "Turn PWM to maximum" 268 | - "Turn off PWM output" 269 | - "Adjust PWM frequency to 75%" 270 | - "Can you set PWM to 25%?" 271 | 272 | Claude will understand your intent and automatically invoke the appropriate commands. No need to remember specific command formats - just express your needs in natural language. 273 | 274 |
275 | Cline Configuration Example 276 |

Example in Claude

277 |
278 |
279 | Cline Configuration Example 280 |

Example in Cline

281 |
282 | 283 | ## Documentation 284 | 285 | - [Installation Guide](./docs/en/installation.md) 286 | - [API Documentation](./docs/en/api.md) 287 | - [Configuration Guide](./docs/en/configuration.md) 288 | 289 | ## Examples 290 | 291 | ### 1. Simple Command Configuration 292 | ```yaml 293 | commands: 294 | led_control: 295 | command: "LED {state}\n" 296 | need_parse: false 297 | prompts: 298 | - "Turn on LED" 299 | - "Turn off LED" 300 | ``` 301 | 302 | ### 2. Command with Response Parsing 303 | ```yaml 304 | commands: 305 | get_temperature: 306 | command: "GET_TEMP\n" 307 | need_parse: true 308 | prompts: 309 | - "Get temperature" 310 | ``` 311 | 312 | Response example: 313 | ```python 314 | { 315 | "status": "success", 316 | "result": { 317 | "raw": "OK TEMP=25.5" 318 | } 319 | } 320 | ``` 321 | 322 | ## Requirements 323 | 324 | - Python 3.11+ 325 | - pyserial 326 | - mcp 327 | 328 | ## Installation from source code 329 | 330 | #### Manual Installation 331 | ```bash 332 | # Install from source: 333 | git clone https://github.com/mcp2everything/mcp2mqtt.git 334 | cd mcp2mqtt 335 | 336 | # Create virtual environment 337 | uv venv .venv 338 | 339 | # Activate virtual environment 340 | # Windows: 341 | .venv\Scripts\activate 342 | # Linux/macOS: 343 | source .venv/bin/activate 344 | 345 | # Install development dependencies 346 | uv pip install --editable . 347 | ``` 348 | 349 | ## Running the Service 350 | 351 | Use the `uv run` command to automatically build, install, and run the service: 352 | 353 | ```bash 354 | uv run src/mcp2mqtt/server.py 355 | ``` 356 | 357 | This command will: 358 | 1. Build the mcp2mqtt package 359 | 2. Install it in the current environment 360 | 3. Start the server 361 | 362 | 363 | ## Contributing 364 | 365 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 366 | 367 | Please make sure to update tests as appropriate. 368 | 369 | ### Development Setup 370 | 371 | 1. Fork and clone the repository 372 | 2. Create a virtual environment: 373 | ```bash 374 | uv venv 375 | # Windows: 376 | .venv\Scripts\activate 377 | # Linux/macOS: 378 | source .venv/bin/activate 379 | ``` 380 | 3. Install development dependencies: 381 | ```bash 382 | uv pip install -e ".[dev]" 383 | ``` 384 | 385 | ### Running Tests 386 | 387 | ```bash 388 | uv pytest tests/ 389 | ``` 390 | 391 | ## License 392 | 393 | [MIT](LICENSE) 394 | 395 | ## Acknowledgments 396 | 397 | - Thanks to the [Claude](https://claude.ai) team for the MCP protocol 398 | - [pySerial](https://github.com/pyserial/pyserial) for serial communication 399 | - All contributors and users of this project 400 | 401 | ## Support 402 | 403 | If you encounter any issues or have questions: 404 | 1. Check the [Issues](https://github.com/mcp2everything/mcp2mqtt/issues) page 405 | 2. Read our [Wiki](https://github.com/mcp2everything/mcp2mqtt/wiki) 406 | 3. Create a new issue if needed 407 | -------------------------------------------------------------------------------- /README_PYPI.md: -------------------------------------------------------------------------------- 1 | # mcp2mqtt: Bridge between AI Models and Physical World 2 | 3 | Connect AI Large Language Models to hardware devices through the Model Context Protocol (MCP). 4 | 5 | [GitHub Repository](https://github.com/mcp2everything/mcp2mqtt) | [Documentation](https://github.com/mcp2everything/mcp2mqtt/tree/main/docs) 6 | 7 | ## Features 8 | 9 | - **Intelligent Serial Communication** 10 | - Automatic port detection and configuration 11 | - Multiple baud rate support (default 115200) 12 | - Real-time status monitoring and error handling 13 | 14 | - **MCP Protocol Integration** 15 | - Full Model Context Protocol support 16 | - Resource management and tool invocation 17 | - Flexible prompt system 18 | 19 | ## Supported Clients 20 | 21 | mcp2mqtt supports all clients implementing the MCP protocol, including: 22 | 23 | - Claude Desktop (Test ok) 24 | - Continue (Should work) 25 | - Cline (Test ok) 26 | 27 | ## Quick Start 28 | make sure you have installed uv 29 | ``` 30 | ```bash 31 | windows 32 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 33 | MacOS 34 | curl -LsSf https://astral.sh/uv/install.sh | sh 35 | ``` 36 | ## Basic Configuration 37 | 38 | Add the following to your MCP client configuration: 39 | 40 | ```json 41 | { 42 | "mcpServers": { 43 | "mcp2mqtt": { 44 | "command": "uvx", 45 | "args": ["mcp2mqtt"] 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Serial Port Configuration 52 | 53 | Create or modify `config.yaml` to configure serial port parameters: 54 | 55 | ```yaml 56 | serial: 57 | port: COM11 # Windows example, on Linux might be /dev/ttyUSB0 58 | baud_rate: 115200 # Baud rate 59 | timeout: 1.0 # Serial timeout (seconds) 60 | read_timeout: 0.5 # Read timeout (seconds) 61 | ``` 62 | 63 | If `port` is not specified, the program will automatically search for available serial ports. 64 | 65 | ## Configuration File Location 66 | 67 | The configuration file (`config.yaml`) can be placed in different locations depending on your needs. The program searches for the configuration file in the following order: 68 | 69 | ### 1. Current Working Directory (For Development) 70 | - Path: `./config.yaml` 71 | - Example: If you run the program from `C:\Projects`, it will look for `C:\Projects\config.yaml` 72 | - Best for: Development and testing 73 | - No special permissions required 74 | 75 | ### 2. User's Home Directory (Recommended for Personal Use) 76 | - Windows: `C:\Users\YourName\.mcp2mqtt\config.yaml` 77 | - macOS: `/Users/YourName/.mcp2mqtt/config.yaml` 78 | - Linux: `/home/username/.mcp2mqtt/config.yaml` 79 | - Best for: Personal configuration 80 | - Create the `.mcp2mqtt` directory if it doesn't exist 81 | - No special permissions required 82 | 83 | ### 3. System-wide Configuration (For Multi-user Setup) 84 | - Windows: `C:\ProgramData\mcp2mqtt\config.yaml` (requires admin rights) 85 | - macOS/Linux: `/etc/mcp2mqtt/config.yaml` (requires sudo/root) 86 | - Best for: Shared configuration in multi-user environments 87 | - Create the directory with appropriate permissions 88 | 89 | The program will use the first valid configuration file it finds in this order. Choose the location based on your needs: 90 | - For testing: use current directory 91 | - For personal use: use home directory (recommended) 92 | - For system-wide settings: use ProgramData or /etc 93 | 94 | ## Serial Port Configuration 95 | 96 | Create your `config.yaml` in one of the above locations with the following structure: 97 | 98 | ```yaml 99 | serial: 100 | port: COM11 # or /dev/ttyUSB0 for Linux 101 | baud_rate: 115200 102 | timeout: 1.0 103 | read_timeout: 0.5 104 | 105 | commands: 106 | # Add your commands here 107 | # See the Command Configuration section for examples 108 | ``` 109 | 110 | ## Command Configuration 111 | 112 | Add or remove custom commands in `config.yaml`: 113 | 114 | ```yaml 115 | commands: 116 | # PWM control command example 117 | set_pwm: 118 | command: "PWM {frequency}\n" # Actual command format to send 119 | need_parse: false # No need to parse response 120 | prompts: # Prompt list 121 | - "Set PWM to {value}" 122 | - "Turn off PWM" 123 | 124 | # LED control command example 125 | led_control: 126 | command: "LED {state}\n" # state can be on/off or other values 127 | need_parse: false 128 | prompts: 129 | - "Turn on LED" 130 | - "Turn off LED" 131 | - "Set LED state to {state}" 132 | 133 | # Command example with response parsing 134 | get_sensor: 135 | command: "GET_SENSOR\n" 136 | need_parse: true # Need to parse response 137 | prompts: 138 | - "Read sensor data" 139 | ``` 140 | 141 | ### Response Parsing 142 | 143 | 1. Simple Response (`need_parse: false`): 144 | - Device returns message starting with "OK" indicates success 145 | - Other responses will be treated as errors 146 | 147 | 2. Parsed Response (`need_parse: true`): 148 | - Complete response will be returned in the `result.raw` field 149 | 150 | ## Documentation 151 | 152 | For detailed documentation, please visit our [GitHub repository](https://github.com/mcp2everything/mcp2mqtt). 153 | 154 | ## Support 155 | 156 | If you encounter any issues or have questions: 157 | 1. Check our [Issues](https://github.com/mcp2everything/mcp2mqtt/issues) page 158 | 2. Read our [Wiki](https://github.com/mcp2everything/mcp2mqtt/wiki) 159 | 3. Create a new issue if needed 160 | 161 | ## License 162 | 163 | This project is licensed under the MIT License. 164 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | mqtt: 2 | # MQTT服务器配置 - 使用EMQX公共测试服务器 3 | broker: "broker.emqx.io" # EMQX公共测试服务器地址 4 | port: 1883 # TCP端口 5 | websocket_port: 8083 # WebSocket端口 6 | ssl_port: 8883 # SSL/TLS端口 7 | ws_ssl_port: 8084 # WebSocket Secure端口 8 | quic_port: 14567 # QUIC端口 9 | client_id: "mcp2mqtt_test_client" # 测试客户端ID 10 | username: "" # 公共测试服务器不需要认证 11 | password: "" # 公共测试服务器不需要认证 12 | keepalive: 60 13 | response_start_string: "CMD" # 应答的开始字符串,用于验证响应 14 | 15 | # MCP工具定义 16 | tools: 17 | set_pwm: 18 | name: "set_pwm" 19 | description: "设置PWM频率,范围0-100" 20 | parameters: 21 | - name: "frequency" 22 | type: "integer" 23 | description: "PWM频率值(0-100)" 24 | required: true 25 | mqtt_topic: "mcp2mqtt/pwm" 26 | response_topic: "mcp2mqtt/pwm/response" 27 | response_format: "CMD PWM {frequency} OK" 28 | 29 | get_pico_info: 30 | name: "get_pico_info" 31 | description: "获取Pico开发板信息" 32 | parameters: [] 33 | mqtt_topic: "mcp2mqtt/info" 34 | response_topic: "mcp2mqtt/info/response" 35 | response_format: "CMD INFO {info} OK" 36 | 37 | led_control: 38 | name: "led_control" 39 | description: "控制LED开关" 40 | parameters: 41 | - name: "state" 42 | type: "string" 43 | description: "LED状态(on/off)" 44 | required: true 45 | enum: ["on", "off"] 46 | mqtt_topic: "mcp2mqtt/led" 47 | response_topic: "mcp2mqtt/led/response" 48 | response_format: "CMD LED {state} OK" 49 | 50 | # 命令示例(用于测试) 51 | commands: 52 | set_pwm: 53 | command: "PWM {frequency}" 54 | need_parse: true 55 | data_type: "ascii" 56 | prompts: 57 | - "把PWM调到最大" # 返回 CMD PWM 100 OK 58 | - "把PWM调到最小" # 返回 CMD PWM 0 OK 59 | - "请将PWM设置为{value}" # 返回 CMD PWM {value} OK 60 | - "关闭PWM" # 返回 CMD PWM 0 OK 61 | - "把PWM调到一半" # 返回 CMD PWM 50 OK 62 | mqtt_topic: "mcp2mqtt/pwm" 63 | response_topic: "mcp2mqtt/pwm/response" 64 | 65 | get_pico_info: 66 | command: "INFO" 67 | need_parse: true 68 | data_type: "ascii" 69 | prompts: 70 | - "查询Pico板信息" # 返回 CMD INFO {设备信息} 71 | - "显示开发板状态" # 返回 CMD INFO {设备信息} 72 | mqtt_topic: "mcp2mqtt/info" 73 | response_topic: "mcp2mqtt/info/response" 74 | 75 | led_control: 76 | command: "LED {state}" 77 | need_parse: true 78 | data_type: "ascii" 79 | prompts: 80 | - "打开LED" # 返回 CMD LED on OK 81 | - "关闭LED" # 返回 CMD LED off OK 82 | - "设置LED状态为{state}" # 返回 CMD LED {state} OK 83 | mqtt_topic: "mcp2mqtt/led" 84 | response_topic: "mcp2mqtt/led/response" 85 | -------------------------------------------------------------------------------- /docs/en/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ### **Community Code of Conduct** 2 | 3 | Thank you for participating in and supporting the **mcp2mqtt** project! To maintain a healthy and open community, we have established the following code of conduct, which all participants are expected to follow: 4 | 5 | #### **1. Respect Others** 6 | - Show respect for the opinions, backgrounds, and identities of all community members. 7 | - Harassment, insults, threats, or personal attacks of any kind are strictly prohibited. 8 | 9 | #### **2. Avoid Sensitive Topics** 10 | - Discussions about politics, religion, or other potentially divisive topics are discouraged. 11 | - Hate speech, discriminatory content, or any offensive material is not allowed. 12 | 13 | #### **3. Comply with Laws and Regulations** 14 | - Do not post illegal content or use this project for any unlawful purposes. 15 | - Ensure your actions comply with the laws and regulations of your country or region. 16 | 17 | #### **4. Promote Community Harmony** 18 | - Avoid off-topic or repetitive discussions; keep the focus on project-related topics. 19 | - Suggestions and feedback are welcome but should remain constructive. 20 | 21 | #### **Violation Handling** 22 | Community moderators reserve the right to warn, remove content, or ban users who violate this code of conduct. 23 | 24 | Thank you for your understanding and cooperation in building a friendly, open, and inclusive developer community! 25 | 26 | --- -------------------------------------------------------------------------------- /docs/en/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### **Contribution Guide** 2 | 3 | Thank you for your interest and support for the **mcp2mqtt** project! We welcome your contributions. This guide will help you get started quickly and collaborate effectively. 4 | 5 | #### **Project Philosophy** 6 | The core goal of mcp2mqtt is to enable a **“zero-code”** experience for users. Through simple **configuration files**, users can: 7 | - Map prompts to serial port commands; 8 | - Enable natural language interaction with devices. 9 | 10 | We are committed to making the project accessible to all users without requiring complex coding skills. 11 | 12 | --- 13 | 14 | #### **How to Contribute** 15 | 16 | 1. **Fork the Project** 17 | - Click the **Fork** button in the top-right corner of the GitHub repository to copy it to your account. 18 | 19 | 2. **Clone the Repository** 20 | ```bash 21 | git clone https://github.com/mcp2everything/mcp2mqtt.git 22 | cd mcp2mqtt 23 | ``` 24 | 25 | 3. **Create a New Branch** 26 | Create a dedicated branch for your changes with a clear name: 27 | ```bash 28 | git checkout -b feature/feature-name 29 | ``` 30 | 31 | 4. **Implement Changes** 32 | - Ensure your modifications align with the project philosophy, focusing on **configuration-driven functionality**. 33 | - Test your changes locally to ensure everything works as expected. 34 | 35 | 5. **Commit Your Changes** 36 | - Add and commit your changes: 37 | ```bash 38 | git add . 39 | git commit -m "Description of the changes" 40 | git push origin feature/feature-name 41 | ``` 42 | 43 | 6. **Submit a Pull Request** 44 | - Go to your Fork’s GitHub page and click **New Pull Request**. 45 | - Provide a clear description of your changes and the reasons behind them. 46 | 47 | --- 48 | 49 | #### **Contribution Tips** 50 | 51 | - **Code Quality**: Write clear, well-commented code. 52 | - **Test Coverage**: Add appropriate test cases for your features. 53 | - **Documentation**: Update documentation if your changes impact usability. 54 | 55 | --- 56 | 57 | #### **Review Process** 58 | Once you submit a pull request, we will review it based on: 59 | 1. **Code Quality**: Adherence to project philosophy and coding standards. 60 | 2. **Functionality Testing**: Ensuring the new features work as intended without breaking existing ones. 61 | 3. **Documentation**: Verifying that all relevant documentation is updated and clear. 62 | 63 | Approved contributions will be merged, and we greatly appreciate your efforts! 64 | 65 | --- 66 | 67 | #### **Contact Us** 68 | If you encounter any issues during the contribution process, feel free to reach out via GitHub Issues. 69 | 70 | --- -------------------------------------------------------------------------------- /docs/en/testing.md: -------------------------------------------------------------------------------- 1 | # Testing Guide 2 | 3 | This guide explains how to test the MCP2MQTT service with a simulated device. 4 | 5 | ## Prerequisites 6 | 7 | - Python >= 3.11 8 | - UV package manager 9 | - MQTT broker (default: broker.emqx.io) 10 | - MCP client (e.g., Claude) 11 | 12 | ## Setup 13 | 14 | 1. Install dependencies: 15 | ```bash 16 | uv venv .venv 17 | source .venv/bin/activate 18 | uv pip install -e . 19 | ``` 20 | 21 | 2. Configure MQTT settings in `config.yaml`: 22 | ```yaml 23 | mqtt: 24 | broker: "broker.emqx.io" # Use default or your own broker 25 | port: 1883 26 | client_id: "mcp2mqtt_client" 27 | ``` 28 | 29 | ## Running Tests 30 | 31 | ### 1. Start Device Simulator 32 | 33 | The project includes a device simulator in the `tests` directory. This simulates a hardware device that can: 34 | - Respond to PWM control commands 35 | - Provide device information 36 | - Control LED state 37 | 38 | Start the simulator: 39 | ```bash 40 | python tests/responder.py 41 | ``` 42 | 43 | You should see output indicating that the simulator is running and connected to the MQTT broker. 44 | 45 | ### 2. Start MCP2MQTT Service 46 | 47 | In a new terminal: 48 | ```bash 49 | uv run mcp2mqtt 50 | ``` 51 | 52 | The service will: 53 | - Load configuration 54 | - Connect to MQTT broker 55 | - Register available tools 56 | - Wait for MCP commands 57 | 58 | ### 3. Configure MCP Client 59 | 60 | Add the MCP2MQTT service to your MCP client (e.g., Claude): 61 | - Server name: mcp2mqtt 62 | - Version: 0.1.0 63 | - Tools: 64 | - set_pwm 65 | - get_pico_info 66 | - led_control 67 | 68 | ### 4. Test Commands 69 | 70 | Try these example commands: 71 | 72 | 1. Set PWM frequency: 73 | ``` 74 | set_pwm frequency=50 75 | ``` 76 | Expected response: `CMD PWM 50 OK` 77 | 78 | 2. Get device information: 79 | ``` 80 | get_pico_info 81 | ``` 82 | Expected response: `CMD INFO Device:Pico Status:Running OK` 83 | 84 | 3. Control LED: 85 | ``` 86 | led_control state=on 87 | ``` 88 | Expected response: `CMD LED on OK` 89 | 90 | ## Troubleshooting 91 | 92 | 1. Connection Issues: 93 | - Check MQTT broker address and port 94 | - Verify network connectivity 95 | - Check firewall settings 96 | 97 | 2. Command Failures: 98 | - Verify simulator is running 99 | - Check MQTT topics match in config 100 | - Review service logs for errors 101 | 102 | 3. Response Timeouts: 103 | - Increase timeout value in configuration 104 | - Check network latency 105 | - Verify broker QoS settings 106 | 107 | ## Next Steps 108 | 109 | After testing with the simulator, you can: 110 | 1. Connect real hardware devices 111 | 2. Customize MQTT topics and message formats 112 | 3. Add new tools and commands 113 | 4. Implement additional device features 114 | -------------------------------------------------------------------------------- /docs/images/README.md: -------------------------------------------------------------------------------- 1 | # Images Directory 2 | 3 | This directory contains images used in documentation. 4 | 5 | ## Guidelines for adding images: 6 | 1. Use descriptive names (e.g., `pico-wiring-diagram.png`) 7 | 2. Keep image sizes reasonable (compress if needed) 8 | 3. Use PNG format for diagrams and screenshots 9 | 4. Use JPG for photos 10 | -------------------------------------------------------------------------------- /docs/images/client_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/client_config.png -------------------------------------------------------------------------------- /docs/images/cline_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/cline_config.png -------------------------------------------------------------------------------- /docs/images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/config.png -------------------------------------------------------------------------------- /docs/images/connect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/connect.jpg -------------------------------------------------------------------------------- /docs/images/howitworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/howitworks.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/stru_chs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/stru_chs.png -------------------------------------------------------------------------------- /docs/images/stru_eng.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/stru_eng.PNG -------------------------------------------------------------------------------- /docs/images/test_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/test_output.png -------------------------------------------------------------------------------- /docs/images/workflow_chs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/workflow_chs.png -------------------------------------------------------------------------------- /docs/images/workflow_eng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp2everything/mcp2mqtt/586705490d829d188d34187f4bc1a0f038352552/docs/images/workflow_eng.png -------------------------------------------------------------------------------- /docs/zh/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### **社区行为准则** 4 | 5 | 感谢您参与和支持 **mcp2mqtt** 项目!为维护健康、开放的社区环境,我们制定了以下行为准则,请每位参与者共同遵守: 6 | 7 | #### **1. 尊重他人** 8 | - 请尊重每位社区成员的观点、背景和身份。 9 | - 禁止任何形式的辱骂、诋毁、威胁或人身攻击行为。 10 | 11 | #### **2. 禁止敏感话题讨论** 12 | - 请避免谈论政治、宗教或其他可能引发争议的话题。 13 | - 不允许传播仇恨言论、歧视性内容或其他冒犯性信息。 14 | 15 | #### **3. 遵守法律与规定** 16 | - 禁止发布违法内容,或使用本项目从事非法活动。 17 | - 请确保您的行为符合所在国家或地区的法律法规。 18 | 19 | #### **4. 维护社区和谐** 20 | - 避免无关或重复性讨论,保持讨论与项目主题相关。 21 | - 欢迎提出意见或建议,但请以建设性为前提。 22 | 23 | #### **违规处理** 24 | 如违反本准则,社区管理员有权警告、删除相关内容,甚至禁言或移除违规用户。 25 | 26 | 感谢您的理解与配合,共同建设一个友好、开放、包容的开发者社区! 27 | 28 | --- -------------------------------------------------------------------------------- /docs/zh/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### **贡献指南** 4 | 5 | 感谢您对 **mcp2mqtt** 项目的关注和支持!我们非常欢迎您的参与。以下是本项目的贡献指南,旨在帮助您快速上手并与我们合作。 6 | 7 | #### **项目设计理念** 8 | mcp2mqtt 的核心目标是实现 **“用户零代码”** 的体验。通过简单的 **配置文件**,用户可以: 9 | - 定义提示词与串口命令的对应关系; 10 | - 实现自然语言与设备的对话。 11 | 12 | 我们致力于让每一位用户都能通过配置文件快速上手,无需编写复杂代码。 13 | 14 | --- 15 | 16 | #### **如何贡献** 17 | 18 | 1. **Fork 本项目** 19 | - 点击 GitHub 仓库右上角的 **Fork** 按钮,将项目复制到您的账户中。 20 | 21 | 2. **克隆到本地** 22 | ```bash 23 | git clone https://github.com/您的用户名/mcp2mqtt.git 24 | cd mcp2mqtt 25 | ``` 26 | 27 | 3. **创建新分支** 28 | 为您的修改创建一个独立分支,命名应简洁明了: 29 | ```bash 30 | git checkout -b feature/功能名称 31 | ``` 32 | 33 | 4. **实现功能或修复问题** 34 | - 请确保遵循项目的设计理念,即以 **配置文件驱动功能** 为核心。 35 | - 修改完成后,进行本地测试,确保功能正常。 36 | 37 | 5. **提交修改** 38 | - 将代码提交到您的分支: 39 | ```bash 40 | git add . 41 | git commit -m "描述此次修改的内容" 42 | git push origin feature/功能名称 43 | ``` 44 | 45 | 6. **发起 Pull Request** 46 | - 进入您 Fork 的项目页面,点击 **New Pull Request** 按钮。 47 | - 请在描述中详细说明修改内容及原因。 48 | 49 | --- 50 | 51 | #### **贡献建议** 52 | 53 | - **代码规范**:请确保您的代码清晰、注释完善。 54 | - **测试覆盖**:为您的功能编写相应测试用例。 55 | - **文档更新**:如果修改会影响使用,请同步更新文档。 56 | 57 | --- 58 | 59 | #### **审核流程** 60 | 提交后,我们会进行以下审核: 61 | 1. **代码质量**:符合项目设计理念和代码规范; 62 | 2. **功能测试**:新功能是否符合预期,是否影响现有功能; 63 | 3. **文档检查**:文档是否完整清晰。 64 | 65 | 审核通过后,我们将合并代码并感谢您的贡献! 66 | 67 | --- 68 | 69 | #### **联系我们** 70 | 如果在贡献过程中遇到任何问题,欢迎通过 GitHub Issues 与我们联系! 71 | 72 | --- -------------------------------------------------------------------------------- /docs/zh/testing.md: -------------------------------------------------------------------------------- 1 | # 测试指南 2 | 3 | 本指南说明如何使用模拟设备测试 MCP2MQTT 服务。 4 | 5 | ## 前提条件 6 | 7 | - Python >= 3.11 8 | - UV 包管理器 9 | - MQTT 服务器(默认:broker.emqx.io) 10 | - MCP 客户端(如 Claude) 11 | 12 | ## 设置 13 | 14 | 1. 安装依赖: 15 | ```bash 16 | uv venv .venv 17 | source .venv/bin/activate 18 | uv pip install -e . 19 | ``` 20 | 21 | 2. 在 `config.yaml` 中配置 MQTT 设置: 22 | ```yaml 23 | mqtt: 24 | broker: "broker.emqx.io" # 使用默认或你自己的服务器 25 | port: 1883 26 | client_id: "mcp2mqtt_client" 27 | ``` 28 | 29 | ## 运行测试 30 | 31 | ### 1. 启动设备模拟器 32 | 33 | 项目在 `tests` 目录中包含了一个设备模拟器。它可以模拟一个硬件设备,能够: 34 | - 响应 PWM 控制命令 35 | - 提供设备信息 36 | - 控制 LED 状态 37 | 38 | 启动模拟器: 39 | ```bash 40 | python tests/responder.py 41 | ``` 42 | 43 | 你应该能看到模拟器正在运行并已连接到 MQTT 服务器的输出信息。 44 | 45 | ### 2. 启动 MCP2MQTT 服务 46 | 47 | 在新的终端中: 48 | ```bash 49 | uv run mcp2mqtt 50 | ``` 51 | 52 | 服务将: 53 | - 加载配置 54 | - 连接到 MQTT 服务器 55 | - 注册可用工具 56 | - 等待 MCP 命令 57 | 58 | ### 3. 配置 MCP 客户端 59 | 60 | 将 MCP2MQTT 服务添加到你的 MCP 客户端(如 Claude): 61 | - 服务器名称:mcp2mqtt 62 | - 版本:0.1.0 63 | - 工具: 64 | - set_pwm 65 | - get_pico_info 66 | - led_control 67 | 68 | ### 4. 测试命令 69 | 70 | 尝试这些示例命令: 71 | 72 | 1. 设置 PWM 频率: 73 | ``` 74 | set_pwm frequency=50 75 | ``` 76 | 预期响应:`CMD PWM 50 OK` 77 | 78 | 2. 获取设备信息: 79 | ``` 80 | get_pico_info 81 | ``` 82 | 预期响应:`CMD INFO Device:Pico Status:Running OK` 83 | 84 | 3. 控制 LED: 85 | ``` 86 | led_control state=on 87 | ``` 88 | 预期响应:`CMD LED on OK` 89 | 90 | ## 故障排除 91 | 92 | 1. 连接问题: 93 | - 检查 MQTT 服务器地址和端口 94 | - 验证网络连接 95 | - 检查防火墙设置 96 | 97 | 2. 命令失败: 98 | - 确认模拟器正在运行 99 | - 检查配置中的 MQTT 主题是否匹配 100 | - 查看服务日志中的错误 101 | 102 | 3. 响应超时: 103 | - 在配置中增加超时值 104 | - 检查网络延迟 105 | - 验证服务器 QoS 设置 106 | 107 | ## 下一步 108 | 109 | 在使用模拟器测试后,你可以: 110 | 1. 连接真实的硬件设备 111 | 2. 自定义 MQTT 主题和消息格式 112 | 3. 添加新的工具和命令 113 | 4. 实现额外的设备功能 114 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import platform 4 | import subprocess 5 | import json 6 | import shutil 7 | from pathlib import Path 8 | 9 | def check_os(): 10 | """检查操作系统""" 11 | system = platform.system() 12 | if system != "Windows": 13 | print(f"⚠️ 警告: 当前操作系统为 {system},本脚本主要针对 Windows 系统优化") 14 | if not input("是否继续安装? (y/n): ").lower().startswith('y'): 15 | sys.exit(1) 16 | return system 17 | 18 | def check_python_version(): 19 | """检查Python版本""" 20 | version = sys.version_info 21 | if version.major < 3 or (version.major == 3 and version.minor < 11): 22 | print(f"❌ 错误: Python版本必须大于等于3.11,当前版本为 {sys.version.split()[0]}") 23 | sys.exit(1) 24 | print(f"✅ Python版本检查通过: {sys.version.split()[0]}") 25 | 26 | def check_and_install_uv(): 27 | """检查和安装uv""" 28 | try: 29 | subprocess.run(["uv", "--version"], capture_output=True) 30 | print("✅ uv 已安装") 31 | # 检查uvx是否可用 32 | try: 33 | subprocess.run(["uvx", "--version"], capture_output=True) 34 | print("✅ uvx 已安装") 35 | except FileNotFoundError: 36 | print("⚙️ 正在配置 uvx...") 37 | subprocess.run([sys.executable, "-m", "uv", "pip", "install", "--system", "uv"], check=True) 38 | print("✅ uvx 配置成功") 39 | except FileNotFoundError: 40 | print("⚙️ 正在安装 uv...") 41 | try: 42 | subprocess.run([sys.executable, "-m", "pip", "install", "uv"], check=True) 43 | print("✅ uv 安装成功") 44 | # 安装完uv后配置uvx 45 | print("⚙️ 正在配置 uvx...") 46 | subprocess.run([sys.executable, "-m", "uv", "pip", "install", "--system", "uv"], check=True) 47 | print("✅ uvx 配置成功") 48 | except subprocess.CalledProcessError: 49 | print("❌ uv 安装失败") 50 | sys.exit(1) 51 | 52 | def create_config(): 53 | """创建默认配置文件""" 54 | config_dir = Path.home() / ".mcp2mqtt" 55 | config_file = config_dir / "config.yaml" 56 | 57 | if not config_dir.exists(): 58 | config_dir.mkdir(parents=True) 59 | print(f"✅ 创建配置目录: {config_dir}") 60 | 61 | if not config_file.exists(): 62 | config_content = """serial: 63 | port: COM1 # 请修改为实际的COM端口号 64 | baud_rate: 115200 65 | 66 | commands: 67 | set_pwm: 68 | command: "PWM {frequency}\\n" 69 | need_parse: false 70 | prompts: 71 | - "把PWM调到{value}" 72 | - "Set PWM to {value}%" 73 | """ 74 | config_file.write_text(config_content, encoding='utf-8') 75 | print(f"✅ 创建配置文件: {config_file}") 76 | print("⚠️ 请修改配置文件中的COM端口号为实际值") 77 | else: 78 | print(f"ℹ️ 配置文件已存在: {config_file}") 79 | 80 | def check_and_configure_claude(): 81 | """检查和配置Claude桌面客户端""" 82 | claude_config_dir = Path.home() / "AppData/Roaming/Claude" 83 | if not claude_config_dir.exists(): 84 | print(f"ℹ️ 未检测到Claude桌面客户端目录: {claude_config_dir}") 85 | return 86 | 87 | config_file = claude_config_dir / "claude_desktop_config.json" 88 | if not config_file.exists(): 89 | print(f"ℹ️ Claude配置文件不存在: {config_file}") 90 | return 91 | 92 | try: 93 | with open(config_file, 'r', encoding='utf-8') as f: 94 | config = json.load(f) 95 | except json.JSONDecodeError: 96 | print("❌ Claude配置文件格式错误") 97 | return 98 | 99 | if "mcpServers" not in config: 100 | config["mcpServers"] = {} 101 | 102 | if "mcp2mqtt" not in config["mcpServers"]: 103 | config["mcpServers"]["mcp2mqtt"] = { 104 | "command": "uvx", 105 | "args": ["mcp2mqtt"] 106 | } 107 | with open(config_file, 'w', encoding='utf-8') as f: 108 | json.dump(config, f, indent=2) 109 | print("✅ 已添加mcp2mqtt配置到Claude") 110 | else: 111 | print("ℹ️ Claude已配置mcp2mqtt") 112 | 113 | def check_vscode(): 114 | """检查VSCode安装""" 115 | vscode_path = Path.home() / "AppData/Local/Programs/Microsoft VS Code" 116 | if vscode_path.exists(): 117 | print(""" 118 | ℹ️ 检测到VSCode安装 119 | 请在VSCode中添加以下MCP服务器配置: 120 | { 121 | "mcp2mqtt": { 122 | "command": "uvx", 123 | "args": ["mcp2mqtt"] 124 | } 125 | } 126 | """) 127 | else: 128 | print("ℹ️ 未检测到VSCode安装") 129 | 130 | def main(): 131 | print("=== mcp2mqtt 安装程序 ===") 132 | 133 | # 1. 检查操作系统 134 | system = check_os() 135 | print(f"✅ 操作系统: {system}") 136 | 137 | # 2. 检查Python版本 138 | check_python_version() 139 | 140 | # 3. 检查和安装uv/uvx 141 | check_and_install_uv() 142 | 143 | # 4. 创建配置文件 144 | create_config() 145 | 146 | # 5. 检查和配置Claude 147 | check_and_configure_claude() 148 | 149 | # 6. 检查VSCode 150 | check_vscode() 151 | 152 | print("\n✨ 安装完成!") 153 | print("📝 请确保:") 154 | print("1. 修改配置文件中的COM端口号") 155 | print("2. 检查Claude或VSCode的MCP服务器配置") 156 | print("3. 重启Claude或VSCode以使配置生效") 157 | print("\n💡 提示:mcp2mqtt 将在首次运行时自动下载") 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /install_macos.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import platform 4 | import subprocess 5 | import json 6 | from pathlib import Path 7 | 8 | def check_os(): 9 | """检查操作系统""" 10 | system = platform.system() 11 | if system != "Darwin": 12 | print(f"❌ 错误: 此脚本仅支持 MacOS 系统,当前系统为 {system}") 13 | sys.exit(1) 14 | print(f"✅ 操作系统: MacOS {platform.mac_ver()[0]}") 15 | return system 16 | 17 | def check_python_version(): 18 | """检查Python版本""" 19 | version = sys.version_info 20 | if version.major < 3 or (version.major == 3 and version.minor < 11): 21 | print(f"❌ 错误: Python版本必须大于等于3.11,当前版本为 {sys.version.split()[0]}") 22 | sys.exit(1) 23 | print(f"✅ Python版本检查通过: {sys.version.split()[0]}") 24 | 25 | def check_homebrew(): 26 | """检查是否安装了Homebrew""" 27 | try: 28 | subprocess.run(["brew", "--version"], capture_output=True) 29 | print("✅ Homebrew 已安装") 30 | except FileNotFoundError: 31 | print("❌ 请先安装 Homebrew") 32 | print("💡 安装命令: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"") 33 | sys.exit(1) 34 | 35 | def check_and_install_uv(): 36 | """检查和安装uv""" 37 | try: 38 | subprocess.run(["uv", "--version"], capture_output=True) 39 | print("✅ uv 已安装") 40 | # 检查uvx是否可用 41 | try: 42 | subprocess.run(["uvx", "--version"], capture_output=True) 43 | print("✅ uvx 已安装") 44 | except FileNotFoundError: 45 | print("⚙️ 正在配置 uvx...") 46 | subprocess.run([sys.executable, "-m", "uv", "pip", "install", "--system", "uv"], check=True) 47 | print("✅ uvx 配置成功") 48 | except FileNotFoundError: 49 | print("⚙️ 正在安装 uv...") 50 | try: 51 | # 使用 Homebrew 安装 uv 52 | subprocess.run(["brew", "install", "astral-sh/tap/uv"], check=True) 53 | print("✅ uv 安装成功") 54 | # 安装完uv后配置uvx 55 | print("⚙️ 正在配置 uvx...") 56 | subprocess.run([sys.executable, "-m", "uv", "pip", "install", "--system", "uv"], check=True) 57 | print("✅ uvx 配置成功") 58 | except subprocess.CalledProcessError: 59 | print("❌ uv 安装失败") 60 | sys.exit(1) 61 | 62 | def create_config(): 63 | """创建默认配置文件""" 64 | config_dir = Path.home() / ".mcp2mqtt" 65 | config_file = config_dir / "config.yaml" 66 | 67 | if not config_dir.exists(): 68 | config_dir.mkdir(parents=True) 69 | print(f"✅ 创建配置目录: {config_dir}") 70 | 71 | if not config_file.exists(): 72 | config_content = """serial: 73 | port: /dev/tty.usbserial-* # 串口设备名,支持通配符 74 | baud_rate: 115200 75 | 76 | commands: 77 | set_pwm: 78 | command: "PWM {frequency}\\n" 79 | need_parse: false 80 | prompts: 81 | - "把PWM调到{value}" 82 | - "Set PWM to {value}%" 83 | """ 84 | config_file.write_text(config_content, encoding='utf-8') 85 | print(f"✅ 创建配置文件: {config_file}") 86 | print("⚠️ 请修改配置文件中的串口设备名为实际值") 87 | else: 88 | print(f"ℹ️ 配置文件已存在: {config_file}") 89 | 90 | def check_and_configure_claude(): 91 | """检查和配置Claude桌面客户端""" 92 | claude_config_dir = Path.home() / "Library/Application Support/Claude" 93 | if not claude_config_dir.exists(): 94 | print(f"ℹ️ 未检测到Claude桌面客户端目录: {claude_config_dir}") 95 | return 96 | 97 | config_file = claude_config_dir / "claude_desktop_config.json" 98 | if not config_file.exists(): 99 | print(f"ℹ️ Claude配置文件不存在: {config_file}") 100 | return 101 | 102 | try: 103 | with open(config_file, 'r', encoding='utf-8') as f: 104 | config = json.load(f) 105 | except json.JSONDecodeError: 106 | print("❌ Claude配置文件格式错误") 107 | return 108 | 109 | if "mcpServers" not in config: 110 | config["mcpServers"] = {} 111 | 112 | if "mcp2mqtt" not in config["mcpServers"]: 113 | config["mcpServers"]["mcp2mqtt"] = { 114 | "command": "uvx", 115 | "args": ["mcp2mqtt"] 116 | } 117 | with open(config_file, 'w', encoding='utf-8') as f: 118 | json.dump(config, f, indent=2) 119 | print("✅ 已添加mcp2mqtt配置到Claude") 120 | else: 121 | print("ℹ️ Claude已配置mcp2mqtt") 122 | 123 | def check_vscode(): 124 | """检查VSCode安装""" 125 | vscode_path = Path("/Applications/Visual Studio Code.app") 126 | if vscode_path.exists(): 127 | print(""" 128 | ℹ️ 检测到VSCode安装 129 | 请在VSCode中添加以下MCP服务器配置: 130 | { 131 | "mcp2mqtt": { 132 | "command": "uvx", 133 | "args": ["mcp2mqtt"] 134 | } 135 | } 136 | """) 137 | else: 138 | print("ℹ️ 未检测到VSCode安装") 139 | 140 | def check_serial_devices(): 141 | """检查串口设备""" 142 | devices = list(Path("/dev").glob("tty.usbserial-*")) 143 | if devices: 144 | print("\n检测到以下串口设备:") 145 | for device in devices: 146 | print(f"- {device}") 147 | print("💡 请在配置文件中使用正确的设备名") 148 | else: 149 | print("\n⚠️ 未检测到串口设备,请确保:") 150 | print("1. 设备已正确连接") 151 | print("2. 已安装串口驱动") 152 | print("💡 常用串口芯片驱动:") 153 | print("- CH340/CH341: https://www.wch.cn/downloads/CH341SER_MAC_ZIP.html") 154 | print("- CP210x: https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers") 155 | print("- FTDI: https://ftdichip.com/drivers/vcp-drivers/") 156 | 157 | def main(): 158 | print("=== mcp2mqtt MacOS 安装程序 ===") 159 | 160 | # 1. 检查操作系统 161 | system = check_os() 162 | 163 | # 2. 检查Python版本 164 | check_python_version() 165 | 166 | # 3. 检查Homebrew 167 | check_homebrew() 168 | 169 | # 4. 检查和安装uv/uvx 170 | check_and_install_uv() 171 | 172 | # 5. 创建配置文件 173 | create_config() 174 | 175 | # 6. 检查和配置Claude 176 | check_and_configure_claude() 177 | 178 | # 7. 检查VSCode 179 | check_vscode() 180 | 181 | # 8. 检查串口设备 182 | check_serial_devices() 183 | 184 | print("\n✨ 安装完成!") 185 | print("📝 请确保:") 186 | print("1. 修改配置文件中的串口设备名") 187 | print("2. 检查Claude或VSCode的MCP服务器配置") 188 | print("3. 重启Claude或VSCode以使配置生效") 189 | print("\n💡 提示:") 190 | print("- mcp2mqtt 将在首次运行时自动下载") 191 | print("- 串口设备名通常为 /dev/tty.usbserial-* 格式") 192 | print("- 如遇到权限问题,请确保当前用户有串口设备的读写权限") 193 | 194 | if __name__ == "__main__": 195 | main() 196 | -------------------------------------------------------------------------------- /install_ubuntu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ==================================================== 3 | # Project: mcp2mqtt 4 | # Description: Installation script for Ubuntu/Raspberry Pi 5 | # Repository: https://github.com/mcp2everything/mcp2mqtt.git 6 | # License: MIT License 7 | # Author: mcp2everything 8 | # Copyright (c) 2024 mcp2everything 9 | # ==================================================== 10 | 11 | import os 12 | import sys 13 | import json 14 | import subprocess 15 | from pathlib import Path 16 | import shutil 17 | 18 | def get_uv_path(): 19 | """Get uv executable path.""" 20 | # 检查是否已安装uv 21 | try: 22 | result = subprocess.run(['which', 'uv'], capture_output=True, text=True) 23 | if result.returncode == 0: 24 | return result.stdout.strip() 25 | except: 26 | pass 27 | return None 28 | 29 | def install_uv(): 30 | """Install uv package manager.""" 31 | print("Installing uv package manager...") 32 | try: 33 | # 使用curl安装uv 34 | curl_command = 'curl -LsSf https://astral.sh/uv/install.sh | sh' 35 | subprocess.run(curl_command, shell=True, check=True) 36 | 37 | # 添加uv到PATH 38 | home = str(Path.home()) 39 | bashrc_path = os.path.join(home, '.bashrc') 40 | with open(bashrc_path, 'a') as f: 41 | f.write('\n# uv package manager\nexport PATH="$HOME/.cargo/bin:$PATH"\n') 42 | 43 | print("uv installed successfully!") 44 | return True 45 | except Exception as e: 46 | print(f"Error installing uv: {e}") 47 | return False 48 | 49 | def install_mcp2mqtt(): 50 | """Install mcp2mqtt package.""" 51 | try: 52 | # 创建虚拟环境并安装包 53 | subprocess.run(['uv', 'venv', '.venv'], check=True) 54 | subprocess.run(['.venv/bin/uv', 'pip', 'install', 'mcp2mqtt'], check=True) 55 | return True 56 | except Exception as e: 57 | print(f"Error installing mcp2mqtt: {e}") 58 | return False 59 | 60 | def configure_claude_desktop(): 61 | """Configure Claude Desktop with mcp2mqtt.""" 62 | try: 63 | home = str(Path.home()) 64 | config_dir = os.path.join(home, '.config', 'claude-desktop') 65 | os.makedirs(config_dir, exist_ok=True) 66 | 67 | config_file = os.path.join(config_dir, 'config.json') 68 | config = { 69 | "mcpServers": { 70 | "mcp2mqtt": { 71 | "command": "uvx", 72 | "args": ["mcp2mqtt"] 73 | } 74 | } 75 | } 76 | 77 | # 如果配置文件已存在,则更新而不是覆盖 78 | if os.path.exists(config_file): 79 | with open(config_file, 'r') as f: 80 | existing_config = json.load(f) 81 | existing_config.setdefault('mcpServers', {}) 82 | existing_config['mcpServers']['mcp2mqtt'] = config['mcpServers']['mcp2mqtt'] 83 | config = existing_config 84 | 85 | with open(config_file, 'w') as f: 86 | json.dump(config, f, indent=4) 87 | 88 | print("Claude Desktop configured successfully!") 89 | return True 90 | except Exception as e: 91 | print(f"Error configuring Claude Desktop: {e}") 92 | return False 93 | 94 | def setup_config(): 95 | """Setup configuration files.""" 96 | try: 97 | home = str(Path.home()) 98 | config_dir = os.path.join(home, '.mcp2mqtt') 99 | os.makedirs(config_dir, exist_ok=True) 100 | 101 | # 复制默认配置文件 102 | default_config = os.path.join(os.path.dirname(__file__), 'config.yaml') 103 | user_config = os.path.join(config_dir, 'config.yaml') 104 | 105 | if os.path.exists(default_config): 106 | shutil.copy2(default_config, user_config) 107 | print(f"Configuration file copied to: {user_config}") 108 | return True 109 | except Exception as e: 110 | print(f"Error setting up configuration: {e}") 111 | return False 112 | 113 | def main(): 114 | """Main installation process.""" 115 | print("Starting mcp2mqtt installation for Ubuntu/Raspberry Pi...") 116 | 117 | # 检查Python版本 118 | if sys.version_info < (3, 11): 119 | print("Error: Python 3.11 or higher is required") 120 | sys.exit(1) 121 | 122 | # 安装uv(如果需要) 123 | if not get_uv_path(): 124 | if not install_uv(): 125 | print("Failed to install uv package manager") 126 | sys.exit(1) 127 | 128 | # 安装mcp2mqtt 129 | if not install_mcp2mqtt(): 130 | print("Failed to install mcp2mqtt") 131 | sys.exit(1) 132 | 133 | # 配置Claude Desktop 134 | if not configure_claude_desktop(): 135 | print("Warning: Failed to configure Claude Desktop") 136 | 137 | # 设置配置文件 138 | if not setup_config(): 139 | print("Warning: Failed to setup configuration files") 140 | 141 | print("\nInstallation completed!") 142 | print("\nTo use mcp2mqtt:") 143 | print("1. Activate the virtual environment:") 144 | print(" source .venv/bin/activate") 145 | print("2. Run the server:") 146 | print(" uv run mcp2mqtt") 147 | print("\nFor more information, visit: https://github.com/mcp2everything/mcp2mqtt") 148 | 149 | if __name__ == "__main__": 150 | main() 151 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mcp2mqtt" 7 | version = "0.1.0" 8 | description = "MCP MQTT Service for PWM Control and Device Communication" 9 | readme = "README_PYPI.md" # 使用PyPI专用README 10 | requires-python = ">=3.10" 11 | license = "MIT" 12 | authors = [ 13 | { name = "mcp2everything", email = "mcp2everything@gmail.com" }, 14 | ] 15 | maintainers = [ 16 | { name = "mcp2everything", email = "mcp2everything@gmail.com" }, 17 | ] 18 | keywords = [ 19 | "mcp", 20 | "mqtt", 21 | "pwm", 22 | "control", 23 | "device", 24 | "communication", 25 | ] 26 | classifiers = [ 27 | "Development Status :: 4 - Beta", 28 | "Programming Language :: Python :: 3", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | ] 32 | dependencies = [ 33 | "pyyaml>=6.0.1", 34 | "paho-mqtt>=1.6.1", 35 | "mcp-python>=0.1.0", 36 | ] 37 | 38 | [project.urls] 39 | Homepage = "https://github.com/mcp2everything/mcp2mqtt" 40 | Repository = "https://github.com/mcp2everything/mcp2mqtt.git" 41 | Issues = "https://github.com/mcp2everything/mcp2mqtt/issues" 42 | Changelog = "https://github.com/mcp2everything/mcp2mqtt/blob/main/CHANGELOG.md" 43 | 44 | [project.scripts] 45 | mcp2mqtt = "mcp2mqtt:main" 46 | 47 | [project.optional-dependencies] 48 | dev = [ 49 | "pytest>=7.4.3", 50 | ] 51 | -------------------------------------------------------------------------------- /src/mcp2mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | # ==================================================== 2 | # Project: mcp2mqtt 3 | # Description: A protocol conversion tool that enables 4 | # hardware devices to communicate with 5 | # large language models (LLM) through serial ports. 6 | # Repository: https://github.com/mcp2everything/mcp2mqtt.git 7 | # License: MIT License 8 | # Author: mcp2everything 9 | # Copyright (c) 2024 mcp2everything 10 | # 11 | # Permission is hereby granted, free of charge, to any person 12 | # obtaining a copy of this software and associated documentation 13 | # files (the "Software"), to deal in the Software without restriction, 14 | # including without limitation the rights to use, copy, modify, merge, 15 | # publish, distribute, sublicense, and/or sell copies of the Software, 16 | # and to permit persons to whom the Software is furnished to do so, 17 | # subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be 20 | # included in all copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 24 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 26 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 27 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 28 | # IN THE SOFTWARE. 29 | # ==================================================== 30 | from . import server 31 | import asyncio 32 | import argparse 33 | 34 | 35 | def main(): 36 | """Main entry point for the package.""" 37 | parser = argparse.ArgumentParser(description='mcp2mqtt Server') 38 | parser.add_argument('--config', 39 | default="default", 40 | help='Configuration name (without _config.yaml suffix)') 41 | 42 | args = parser.parse_args() 43 | asyncio.run(server.main(args.config)) 44 | 45 | 46 | # Expose important items at package level 47 | __version__ = "0.1.0" 48 | __all__ = ['main', 'server'] 49 | -------------------------------------------------------------------------------- /src/mcp2mqtt/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import asyncio 4 | import json 5 | import time 6 | from dataclasses import dataclass, field 7 | from typing import Any, Dict, List, Optional 8 | 9 | import paho.mqtt.client as mqtt 10 | import yaml 11 | from mcp.server.models import InitializationOptions 12 | import mcp.types as types 13 | from mcp.server import NotificationOptions, Server 14 | import mcp.server.stdio 15 | 16 | # 设置日志级别为 DEBUG 17 | logging.basicConfig( 18 | level=logging.DEBUG, 19 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 20 | ) 21 | logger = logging.getLogger(__name__) 22 | 23 | @dataclass 24 | class Tool: 25 | """Tool configuration.""" 26 | name: str 27 | description: str 28 | parameters: List[Dict[str, Any]] = field(default_factory=list) 29 | mqtt_topic: str = "" 30 | response_topic: str = "" 31 | response_format: str = "" 32 | 33 | @dataclass 34 | class Config: 35 | """Configuration for mcp2mqtt service.""" 36 | mqtt_broker: str = "broker.emqx.io" 37 | mqtt_port: int = 1883 38 | mqtt_client_id: str = "mcp2mqtt_client" 39 | mqtt_username: str = "" 40 | mqtt_password: str = "" 41 | mqtt_keepalive: int = 60 42 | mqtt_response_start_string: str = "CMD" 43 | tools: Dict[str, Tool] = field(default_factory=dict) 44 | 45 | @staticmethod 46 | def load(config_path: str = "config.yaml") -> 'Config': 47 | """Load configuration from YAML file.""" 48 | try: 49 | logger.info(f"Opening configuration file: {config_path}") 50 | with open(config_path, 'r') as f: 51 | data = yaml.safe_load(f) 52 | logger.info("Successfully parsed YAML configuration") 53 | 54 | # 加载 MQTT 配置 55 | mqtt_config = data.get('mqtt', {}) 56 | logger.info("Loading MQTT configuration...") 57 | config = Config( 58 | mqtt_broker=mqtt_config.get('broker', "broker.emqx.io"), 59 | mqtt_port=mqtt_config.get('port', 1883), 60 | mqtt_client_id=mqtt_config.get('client_id', "mcp2mqtt_client"), 61 | mqtt_username=mqtt_config.get('username', ""), 62 | mqtt_password=mqtt_config.get('password', ""), 63 | mqtt_keepalive=mqtt_config.get('keepalive', 60), 64 | mqtt_response_start_string=mqtt_config.get('response_start_string', "CMD") 65 | ) 66 | logger.info("MQTT configuration loaded") 67 | 68 | # 加载工具配置 69 | logger.info("Loading tools configuration...") 70 | tools_count = 0 71 | for tool_name, tool_data in data.get('tools', {}).items(): 72 | logger.info(f"Loading tool: {tool_name}") 73 | config.tools[tool_name] = Tool( 74 | name=tool_data.get('name', ''), 75 | description=tool_data.get('description', ''), 76 | parameters=tool_data.get('parameters', []), 77 | mqtt_topic=tool_data.get('mqtt_topic', ''), 78 | response_topic=tool_data.get('response_topic', ''), 79 | response_format=tool_data.get('response_format', '') 80 | ) 81 | tools_count += 1 82 | logger.info(f"Loaded {tools_count} tools") 83 | 84 | return config 85 | 86 | except Exception as e: 87 | logger.error(f"Error loading config: {e}") 88 | raise 89 | 90 | class MQTTConnection: 91 | """MQTT connection manager.""" 92 | 93 | def __init__(self, config): 94 | """Initialize MQTT connection.""" 95 | self.config = config 96 | self.client = None 97 | self.connected = False 98 | self.response_start_string = config.mqtt_response_start_string 99 | self.response = None 100 | self.response_received = asyncio.Event() 101 | logger.info(f"Initialized MQTT connection manager") 102 | 103 | def setup_client(self): 104 | """Setup MQTT client""" 105 | if self.client is not None: 106 | return 107 | 108 | self.client = mqtt.Client(client_id=f"{self.config.mqtt_client_id}_{int(time.time())}") 109 | if self.config.mqtt_username: 110 | self.client.username_pw_set(self.config.mqtt_username, self.config.mqtt_password) 111 | self.client.on_connect = self.on_connect 112 | self.client.on_message = self.on_message 113 | self.client.on_disconnect = self.on_disconnect 114 | 115 | def on_connect(self, client, userdata, flags, rc): 116 | """Callback for when the client receives a CONNACK response from the server.""" 117 | if rc == 0: 118 | self.connected = True 119 | logger.info("Connected to MQTT broker successfully") 120 | else: 121 | logger.error(f"Failed to connect to MQTT broker with result code: {rc}") 122 | self.connected = False 123 | 124 | def on_message(self, client, userdata, msg): 125 | """Callback for when a PUBLISH message is received from the server.""" 126 | try: 127 | payload = msg.payload.decode() 128 | logger.info(f"Received message on topic {msg.topic}: {payload}") 129 | self.response = payload 130 | self.response_received.set() 131 | except Exception as e: 132 | logger.error(f"Error processing message: {e}") 133 | 134 | def on_disconnect(self, client, userdata, rc): 135 | """Callback for when the client disconnects from the server.""" 136 | self.connected = False 137 | if rc != 0: 138 | logger.warning(f"Unexpected disconnection from MQTT broker with result code: {rc}") 139 | else: 140 | logger.info("Disconnected from MQTT broker") 141 | 142 | async def connect_and_send(self, topic: str, message: str, response_topic: str = None, timeout: int = 5) -> Optional[str]: 143 | """Connect to broker, send message, wait for response, and disconnect.""" 144 | try: 145 | # 设置客户端 146 | self.setup_client() 147 | self.response = None 148 | self.response_received.clear() 149 | 150 | # 连接到服务器 151 | logger.info(f"Connecting to MQTT broker at {self.config.mqtt_broker}") 152 | self.client.connect( 153 | self.config.mqtt_broker, 154 | self.config.mqtt_port, 155 | keepalive=10 # 使用较短的 keepalive 156 | ) 157 | 158 | # 如果需要等待响应,订阅响应主题 159 | if response_topic: 160 | self.client.subscribe(response_topic) 161 | logger.info(f"Subscribed to response topic: {response_topic}") 162 | 163 | # 启动循环 164 | self.client.loop_start() 165 | 166 | # 等待连接成功 167 | start_time = time.time() 168 | while not self.connected and time.time() - start_time < timeout: 169 | await asyncio.sleep(0.1) 170 | 171 | if not self.connected: 172 | raise Exception("Failed to connect to MQTT broker") 173 | 174 | # 发送消息 175 | logger.info(f"Publishing message to {topic}: {message}") 176 | self.client.publish(topic, message) 177 | 178 | # 如果需要等待响应 179 | response = None 180 | if response_topic: 181 | try: 182 | # 等待响应 183 | await asyncio.wait_for(self.response_received.wait(), timeout) 184 | response = self.response 185 | except asyncio.TimeoutError: 186 | logger.error("Timeout waiting for response") 187 | raise Exception("Timeout waiting for response") 188 | 189 | return response 190 | 191 | except Exception as e: 192 | logger.error(f"Error in connect_and_send: {e}") 193 | raise 194 | finally: 195 | # 清理连接 196 | self.cleanup() 197 | 198 | def cleanup(self): 199 | """Clean up MQTT connection.""" 200 | try: 201 | if self.client: 202 | self.client.loop_stop() 203 | if self.connected: 204 | self.client.disconnect() 205 | self.client = None 206 | self.connected = False 207 | logger.info("Cleaned up MQTT connection") 208 | except Exception as e: 209 | logger.error(f"Error cleaning up connection: {e}") 210 | 211 | # 创建 MCP 服务器 212 | server = Server("mcp2mqtt") 213 | config = None 214 | 215 | @server.list_tools() 216 | async def handle_list_tools() -> List[types.Tool]: 217 | """List available tools.""" 218 | tools = [] 219 | for tool_name, tool_config in config.tools.items(): 220 | tools.append( 221 | types.Tool( 222 | name=tool_config.name, 223 | description=tool_config.description, 224 | inputSchema={ 225 | "type": "object", 226 | "properties": { 227 | param["name"]: { 228 | "type": param["type"], 229 | "description": param["description"], 230 | **({"enum": param["enum"]} if "enum" in param else {}) 231 | } 232 | for param in tool_config.parameters 233 | }, 234 | "required": [ 235 | param["name"] 236 | for param in tool_config.parameters 237 | if param.get("required", False) 238 | ] 239 | } 240 | ) 241 | ) 242 | return tools 243 | 244 | @server.call_tool() 245 | async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[types.TextContent | types.ImageContent]: 246 | """Handle tool execution requests.""" 247 | try: 248 | logger.info(f"Tool call received - Name: {name}, Arguments: {arguments}") 249 | 250 | # 检查工具是否存在 251 | if name not in config.tools: 252 | return [types.TextContent( 253 | type="text", 254 | text=f"Error: Tool {name} not found" 255 | )] 256 | 257 | tool_config = config.tools[name] 258 | 259 | # 验证参数 260 | if arguments is None: 261 | arguments = {} 262 | 263 | # 检查必需参数 264 | for param in tool_config.parameters: 265 | if param.get('required', False) and param['name'] not in arguments: 266 | return [types.TextContent( 267 | type="text", 268 | text=f"Error: Missing required parameter {param['name']}" 269 | )] 270 | 271 | # 验证枚举值 272 | if 'enum' in param and param['name'] in arguments: 273 | if arguments[param['name']] not in param['enum']: 274 | return [types.TextContent( 275 | type="text", 276 | text=f"Error: Invalid value for {param['name']}" 277 | )] 278 | 279 | # 准备消息 280 | message = None 281 | if name == "set_pwm": 282 | frequency = arguments.get("frequency", 0) 283 | if not (0 <= frequency <= 100): 284 | return [types.TextContent( 285 | type="text", 286 | text="Error: Frequency must be between 0 and 100" 287 | )] 288 | message = f"PWM {frequency}" 289 | 290 | elif name == "get_pico_info": 291 | message = "INFO" 292 | 293 | elif name == "led_control": 294 | state = arguments.get("state", "").lower() 295 | if state not in ["on", "off"]: 296 | return [types.TextContent( 297 | type="text", 298 | text="Error: State must be 'on' or 'off'" 299 | )] 300 | message = f"LED {state}" 301 | 302 | else: 303 | return [types.TextContent( 304 | type="text", 305 | text=f"Error: Unknown tool {name}" 306 | )] 307 | 308 | # 发送消息并等待响应 309 | mqtt_connection = MQTTConnection(config) 310 | response = await mqtt_connection.connect_and_send( 311 | topic=tool_config.mqtt_topic, 312 | message=message, 313 | response_topic=tool_config.response_topic 314 | ) 315 | 316 | return [types.TextContent( 317 | type="text", 318 | text=response if response else f"{config.mqtt_response_start_string} {message} OK" 319 | )] 320 | 321 | except Exception as e: 322 | logger.error(f"Error handling tool call: {e}") 323 | return [types.TextContent( 324 | type="text", 325 | text=f"Error: {str(e)}" 326 | )] 327 | 328 | async def main(config_name: str = None) -> None: 329 | """Run the MCP server.""" 330 | try: 331 | # 加载配置 332 | config_path = config_name if config_name else "config.yaml" 333 | if not os.path.isfile(config_path): 334 | config_path = os.path.join(os.path.dirname(__file__), '..', '..', 'config.yaml') 335 | 336 | logger.info(f"Loading configuration from {config_path}") 337 | if not os.path.isfile(config_path): 338 | logger.error(f"Configuration file not found: {config_path}") 339 | raise FileNotFoundError(f"Configuration file not found: {config_path}") 340 | 341 | global config 342 | config = Config.load(config_path) 343 | logger.info("Configuration loaded successfully") 344 | logger.info(f"MQTT Broker: {config.mqtt_broker}") 345 | logger.info(f"MQTT Port: {config.mqtt_port}") 346 | logger.info(f"Available tools: {list(config.tools.keys())}") 347 | 348 | # 运行 MCP 服务器 349 | logger.info("Starting MCP server...") 350 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 351 | await server.run( 352 | read_stream, 353 | write_stream, 354 | InitializationOptions( 355 | server_name="mcp2mqtt", 356 | server_version="0.1.0", 357 | capabilities=server.get_capabilities( 358 | notification_options=NotificationOptions(), 359 | experimental_capabilities={}, 360 | ), 361 | ), 362 | ) 363 | 364 | except KeyboardInterrupt: 365 | logger.info("Received keyboard interrupt") 366 | except Exception as e: 367 | logger.error(f"Error in main: {e}") 368 | raise 369 | 370 | if __name__ == "__main__": 371 | import sys 372 | config_name = sys.argv[1] if len(sys.argv) > 1 else None 373 | asyncio.run(main(config_name)) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/responder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | MQTT应答程序 6 | 用于接收MQTT消息并按照配置的格式进行应答 7 | 基于配置文件自动处理三种类型的命令: 8 | PWM控制命令 9 | PICO信息查询命令 10 | LED控制命令 11 | 响应格式: 12 | PWM命令:CMD PWM {value} OK 13 | INFO命令:CMD INFO Device:Pico Status:Running OK 14 | LED命令:CMD LED {state} OK 15 | """ 16 | 17 | import paho.mqtt.client as mqtt 18 | import time 19 | import logging 20 | import yaml 21 | import os 22 | from typing import Dict, Any 23 | 24 | # 配置日志 25 | logging.basicConfig( 26 | level=logging.DEBUG, 27 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 28 | ) 29 | logger = logging.getLogger(__name__) 30 | 31 | class MQTTResponder: 32 | """MQTT应答程序类""" 33 | 34 | def __init__(self, config_path: str = "../config.yaml"): 35 | """初始化MQTT应答程序 36 | 37 | Args: 38 | config_path: 配置文件路径 39 | """ 40 | self.config = self.load_config(config_path) 41 | self.client = mqtt.Client(client_id=f"{self.config['mqtt']['client_id']}_responder") 42 | self.client.on_connect = self.on_connect 43 | self.client.on_message = self.on_message 44 | self.client.on_disconnect = self.on_disconnect 45 | self.connected = False 46 | 47 | def load_config(self, config_path: str) -> Dict[str, Any]: 48 | """加载配置文件""" 49 | try: 50 | with open(config_path, 'r', encoding='utf-8') as f: 51 | return yaml.safe_load(f) 52 | except Exception as e: 53 | logger.error(f"Error loading config: {e}") 54 | raise 55 | 56 | def connect(self) -> bool: 57 | """连接到MQTT服务器""" 58 | try: 59 | if self.config['mqtt']['username']: 60 | self.client.username_pw_set( 61 | self.config['mqtt']['username'], 62 | self.config['mqtt']['password'] 63 | ) 64 | 65 | self.client.connect( 66 | self.config['mqtt']['broker'], 67 | self.config['mqtt']['port'], 68 | self.config['mqtt']['keepalive'] 69 | ) 70 | 71 | self.client.loop_start() 72 | logger.info(f"Connected to MQTT broker at {self.config['mqtt']['broker']}") 73 | return True 74 | 75 | except Exception as e: 76 | logger.error(f"Failed to connect to MQTT broker: {e}") 77 | return False 78 | 79 | def on_connect(self, client, userdata, flags, rc): 80 | """连接回调函数""" 81 | if rc == 0: 82 | self.connected = True 83 | logger.info("Connected to MQTT broker successfully") 84 | 85 | # 订阅所有命令主题 86 | for cmd_name, cmd in self.config['commands'].items(): 87 | if 'mqtt_topic' in cmd: 88 | self.client.subscribe(cmd['mqtt_topic']) 89 | logger.info(f"Subscribed to topic: {cmd['mqtt_topic']}") 90 | else: 91 | logger.error(f"Failed to connect to MQTT broker with result code: {rc}") 92 | 93 | def on_message(self, client, userdata, msg): 94 | """消息回调函数""" 95 | try: 96 | payload = msg.payload.decode() 97 | logger.info(f"Received message on topic {msg.topic}: {payload}") 98 | 99 | # 查找对应的命令配置 100 | for cmd_name, cmd in self.config['commands'].items(): 101 | if cmd['mqtt_topic'] == msg.topic: 102 | # 生成响应 103 | response = self.generate_response(cmd_name, payload) 104 | # 发送响应 105 | if response and 'response_topic' in cmd: 106 | self.client.publish(cmd['response_topic'], response) 107 | logger.info(f"Sent response to topic {cmd['response_topic']}: {response}") 108 | break 109 | 110 | except Exception as e: 111 | logger.error(f"Error processing message: {e}") 112 | 113 | def generate_response(self, cmd_name: str, payload: str) -> str: 114 | """生成响应消息""" 115 | try: 116 | cmd_config = self.config['commands'][cmd_name] 117 | response_start = self.config['mqtt']['response_start_string'] 118 | 119 | if cmd_name == 'set_pwm': 120 | # 解析PWM值 121 | try: 122 | value = int(payload.split()[-1]) 123 | return f"{response_start} PWM {value} OK" 124 | except: 125 | return f"{response_start} PWM ERROR" 126 | 127 | elif cmd_name == 'get_pico_info': 128 | # 返回设备信息 129 | return f"{response_start} INFO Device:Pico Status:Running OK" 130 | 131 | elif cmd_name == 'led_control': 132 | # 解析LED状态 133 | state = payload.split()[-1].lower() 134 | if state in ['on', 'off']: 135 | return f"{response_start} LED {state} OK" 136 | return f"{response_start} LED ERROR" 137 | 138 | return None 139 | 140 | except Exception as e: 141 | logger.error(f"Error generating response: {e}") 142 | return None 143 | 144 | def on_disconnect(self, client, userdata, rc): 145 | """断开连接回调函数""" 146 | self.connected = False 147 | logger.warning(f"Disconnected from MQTT broker with result code: {rc}") 148 | 149 | def close(self): 150 | """关闭连接""" 151 | self.client.loop_stop() 152 | self.client.disconnect() 153 | logger.info("Disconnected from MQTT broker") 154 | 155 | def main(): 156 | """主函数""" 157 | import signal 158 | import sys 159 | 160 | def signal_handler(sig, frame): 161 | logger.info("Received signal to stop...") 162 | responder.close() 163 | sys.exit(0) 164 | 165 | signal.signal(signal.SIGINT, signal_handler) 166 | signal.signal(signal.SIGTERM, signal_handler) 167 | 168 | # 获取配置文件路径 169 | config_path = '../config.yaml' 170 | if not os.path.isfile(config_path): 171 | config_path = os.path.join(os.path.dirname(__file__), '..', '/', 'config.yaml') 172 | 173 | # 创建并启动应答程序 174 | responder = MQTTResponder(config_path) 175 | if responder.connect(): 176 | logger.info("MQTT Responder is running. Press CTRL+C to stop.") 177 | signal.pause() 178 | else: 179 | logger.error("Failed to start MQTT Responder") 180 | sys.exit(1) 181 | 182 | if __name__ == "__main__": 183 | main() 184 | -------------------------------------------------------------------------------- /tests/test_mqtt_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | MQTT测试服务器 6 | 用于测试mcp2mqtt的MQTT功能 7 | """ 8 | 9 | import paho.mqtt.client as mqtt 10 | import time 11 | import json 12 | import logging 13 | import yaml 14 | import os 15 | from typing import Dict, Any 16 | 17 | # 配置日志 18 | logging.basicConfig( 19 | level=logging.DEBUG, 20 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 21 | ) 22 | logger = logging.getLogger(__name__) 23 | 24 | class MQTTTestServer: 25 | """MQTT测试服务器类""" 26 | 27 | def __init__(self, config_path: str = "../config.yaml"): 28 | """初始化MQTT测试服务器 29 | 30 | Args: 31 | config_path: 配置文件路径 32 | """ 33 | self.config = self.load_config(config_path) 34 | self.client = mqtt.Client(client_id=f"{self.config['mqtt']['client_id']}_server") 35 | self.client.on_connect = self.on_connect 36 | self.client.on_message = self.on_message 37 | self.client.on_disconnect = self.on_disconnect 38 | self.connected = False 39 | 40 | # 存储最后收到的消息 41 | self.last_message = { 42 | 'topic': None, 43 | 'payload': None, 44 | 'timestamp': None 45 | } 46 | 47 | def load_config(self, config_path: str) -> Dict[str, Any]: 48 | """加载配置文件 49 | 50 | Args: 51 | config_path: 配置文件路径 52 | 53 | Returns: 54 | 配置字典 55 | """ 56 | try: 57 | with open(config_path, 'r', encoding='utf-8') as f: 58 | return yaml.safe_load(f) 59 | except Exception as e: 60 | logger.error(f"Error loading config: {e}") 61 | raise 62 | 63 | def connect(self) -> bool: 64 | """连接到MQTT服务器 65 | 66 | Returns: 67 | bool: 连接是否成功 68 | """ 69 | try: 70 | # 如果配置了用户名和密码,则设置认证 71 | if self.config['mqtt']['username']: 72 | self.client.username_pw_set( 73 | self.config['mqtt']['username'], 74 | self.config['mqtt']['password'] 75 | ) 76 | 77 | # 连接到服务器 78 | self.client.connect( 79 | self.config['mqtt']['broker'], 80 | self.config['mqtt']['port'], 81 | self.config['mqtt']['keepalive'] 82 | ) 83 | 84 | # 启动循环 85 | self.client.loop_start() 86 | logger.info(f"Connected to MQTT broker at {self.config['mqtt']['broker']}") 87 | return True 88 | 89 | except Exception as e: 90 | logger.error(f"Failed to connect to MQTT broker: {e}") 91 | return False 92 | 93 | def on_connect(self, client, userdata, flags, rc): 94 | """连接回调函数""" 95 | if rc == 0: 96 | self.connected = True 97 | logger.info("Connected to MQTT broker successfully") 98 | 99 | # 订阅所有命令的响应主题 100 | topics = [] 101 | for cmd_name, cmd in self.config['commands'].items(): 102 | if 'mqtt_topic' in cmd: 103 | topics.append(cmd['mqtt_topic']) 104 | if 'response_topic' in cmd: 105 | topics.append(cmd['response_topic']) 106 | 107 | for topic in topics: 108 | self.client.subscribe(topic) 109 | logger.info(f"Subscribed to topic: {topic}") 110 | else: 111 | logger.error(f"Failed to connect to MQTT broker with result code: {rc}") 112 | 113 | def on_message(self, client, userdata, msg): 114 | """消息回调函数""" 115 | try: 116 | payload = msg.payload.decode() 117 | logger.info(f"Received message on topic {msg.topic}: {payload}") 118 | 119 | # 存储接收到的消息 120 | self.last_message = { 121 | 'topic': msg.topic, 122 | 'payload': payload, 123 | 'timestamp': time.time() 124 | } 125 | 126 | # 发送响应消息 127 | response_topic = None 128 | if msg.topic.endswith('/command'): 129 | response_topic = msg.topic.replace('/command', '/response') 130 | elif msg.topic.endswith('/status'): 131 | response_topic = msg.topic.replace('/status', '/control') 132 | 133 | if response_topic: 134 | response = { 135 | 'original_topic': msg.topic, 136 | 'original_message': payload, 137 | 'status': 'received', 138 | 'timestamp': time.time() 139 | } 140 | self.client.publish(response_topic, json.dumps(response)) 141 | logger.info(f"Sent response to topic {response_topic}") 142 | 143 | except Exception as e: 144 | logger.error(f"Error processing message: {e}") 145 | 146 | def on_disconnect(self, client, userdata, rc): 147 | """断开连接回调函数""" 148 | self.connected = False 149 | logger.warning(f"Disconnected from MQTT broker with result code: {rc}") 150 | 151 | def publish_test_message(self, topic: str, message: str): 152 | """发布测试消息 153 | 154 | Args: 155 | topic: 主题 156 | message: 消息内容 157 | """ 158 | if not self.connected: 159 | logger.error("Not connected to MQTT broker") 160 | return False 161 | 162 | try: 163 | result = self.client.publish(topic, message) 164 | if result.rc == mqtt.MQTT_ERR_SUCCESS: 165 | logger.info(f"Published test message to {topic}: {message}") 166 | return True 167 | else: 168 | logger.error(f"Failed to publish message: {result.rc}") 169 | return False 170 | except Exception as e: 171 | logger.error(f"Error publishing message: {e}") 172 | return False 173 | 174 | def close(self): 175 | """关闭连接""" 176 | if self.connected: 177 | self.client.loop_stop() 178 | self.client.disconnect() 179 | logger.info("Disconnected from MQTT broker") 180 | 181 | def main(): 182 | """主函数""" 183 | # 获取配置文件的绝对路径 184 | current_dir = os.path.dirname(os.path.abspath(__file__)) 185 | config_path = os.path.join(current_dir, "..", "config.yaml") 186 | 187 | # 创建测试服务器实例 188 | server = MQTTTestServer(config_path) 189 | 190 | try: 191 | # 连接到MQTT服务器 192 | if not server.connect(): 193 | logger.error("Failed to connect to MQTT broker") 194 | return 195 | 196 | # 发送测试消息到每个命令的主题 197 | test_messages = [ 198 | { 199 | 'topic': 'mcp2mqtt/pwm', 200 | 'message': 'PWM 100' # 设置PWM到最大 201 | }, 202 | { 203 | 'topic': 'mcp2mqtt/pwm', 204 | 'message': 'PWM 0' # 关闭PWM 205 | }, 206 | { 207 | 'topic': 'mcp2mqtt/info', 208 | 'message': 'INFO' # 查询信息 209 | }, 210 | { 211 | 'topic': 'mcp2mqtt/led', 212 | 'message': 'LED on' # 打开LED 213 | }, 214 | { 215 | 'topic': 'mcp2mqtt/led', 216 | 'message': 'LED off' # 关闭LED 217 | } 218 | ] 219 | 220 | # 每隔5秒发送一条测试消息 221 | for msg in test_messages: 222 | server.publish_test_message(msg['topic'], msg['message']) 223 | logger.info(f"Published test message: {msg['message']} to topic: {msg['topic']}") 224 | time.sleep(5) 225 | 226 | # 保持运行一段时间以接收响应 227 | logger.info("Waiting for responses...") 228 | time.sleep(10) 229 | 230 | except KeyboardInterrupt: 231 | logger.info("Received keyboard interrupt") 232 | finally: 233 | server.close() 234 | 235 | if __name__ == "__main__": 236 | main() 237 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mcp2mqtt.server import create_app 3 | 4 | @pytest.fixture 5 | def app(): 6 | app = create_app() 7 | return app 8 | 9 | def test_set_pwm_endpoint(app): 10 | with app.test_client() as client: 11 | # Test valid request 12 | response = client.post('/set-pwm', json={'frequency': 50}) 13 | assert response.status_code == 200 14 | assert response.json['status'] == 'success' 15 | 16 | # Test invalid frequency 17 | response = client.post('/set-pwm', json={'frequency': 101}) 18 | assert response.status_code == 400 19 | assert 'error' in response.json 20 | 21 | # Test missing frequency parameter 22 | response = client.post('/set-pwm', json={}) 23 | assert response.status_code == 400 24 | assert 'error' in response.json 25 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.10" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.7.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 22 | ] 23 | sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 } 24 | wheels = [ 25 | { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2024.12.14" 31 | source = { registry = "https://pypi.org/simple" } 32 | sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, 35 | ] 36 | 37 | [[package]] 38 | name = "colorama" 39 | version = "0.4.6" 40 | source = { registry = "https://pypi.org/simple" } 41 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 42 | wheels = [ 43 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 44 | ] 45 | 46 | [[package]] 47 | name = "exceptiongroup" 48 | version = "1.2.2" 49 | source = { registry = "https://pypi.org/simple" } 50 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 51 | wheels = [ 52 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 53 | ] 54 | 55 | [[package]] 56 | name = "h11" 57 | version = "0.14.0" 58 | source = { registry = "https://pypi.org/simple" } 59 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 60 | wheels = [ 61 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 62 | ] 63 | 64 | [[package]] 65 | name = "httpcore" 66 | version = "1.0.7" 67 | source = { registry = "https://pypi.org/simple" } 68 | dependencies = [ 69 | { name = "certifi" }, 70 | { name = "h11" }, 71 | ] 72 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 75 | ] 76 | 77 | [[package]] 78 | name = "httpx" 79 | version = "0.28.1" 80 | source = { registry = "https://pypi.org/simple" } 81 | dependencies = [ 82 | { name = "anyio" }, 83 | { name = "certifi" }, 84 | { name = "httpcore" }, 85 | { name = "idna" }, 86 | ] 87 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 88 | wheels = [ 89 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 90 | ] 91 | 92 | [[package]] 93 | name = "httpx-sse" 94 | version = "0.4.0" 95 | source = { registry = "https://pypi.org/simple" } 96 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 97 | wheels = [ 98 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 99 | ] 100 | 101 | [[package]] 102 | name = "idna" 103 | version = "3.10" 104 | source = { registry = "https://pypi.org/simple" } 105 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 106 | wheels = [ 107 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 108 | ] 109 | 110 | [[package]] 111 | name = "iniconfig" 112 | version = "2.0.0" 113 | source = { registry = "https://pypi.org/simple" } 114 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 115 | wheels = [ 116 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 117 | ] 118 | 119 | [[package]] 120 | name = "mcp" 121 | version = "1.1.2" 122 | source = { registry = "https://pypi.org/simple" } 123 | dependencies = [ 124 | { name = "anyio" }, 125 | { name = "httpx" }, 126 | { name = "httpx-sse" }, 127 | { name = "pydantic" }, 128 | { name = "sse-starlette" }, 129 | { name = "starlette" }, 130 | ] 131 | sdist = { url = "https://files.pythonhosted.org/packages/9b/f3/5cf212e60681ea6da0dbb6e0d1bc0ab2dbf5eebc749b69663d46f114fea1/mcp-1.1.2.tar.gz", hash = "sha256:694aa9df7a8641b24953c935eb72c63136dc948981021525a0add199bdfee402", size = 57628 } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/df/40/9883eac3718b860d4006eba1920bfcb628f0a1fe37fac46a4f4e391edca6/mcp-1.1.2-py3-none-any.whl", hash = "sha256:a4d32d60fd80a1702440ba4751b847a8a88957a1f7b059880953143e9759965a", size = 36652 }, 134 | ] 135 | 136 | [[package]] 137 | name = "mcp-python" 138 | version = "0.1.2" 139 | source = { registry = "https://pypi.org/simple" } 140 | dependencies = [ 141 | { name = "mcp" }, 142 | ] 143 | sdist = { url = "https://files.pythonhosted.org/packages/52/41/40f65997280bc927479024029a34b4936e66dc0abdf9e6f8a7233c03c7eb/mcp_python-0.1.2.tar.gz", hash = "sha256:d5813b7fa6c2609307b8abc80fd1fefe1609b98938d1f88f930bf69e66b6c1c0", size = 15479 } 144 | wheels = [ 145 | { url = "https://files.pythonhosted.org/packages/12/0f/ea5a74eb6de8d0cc4ff265ae8a6746783b37bc3f4a55dc4918409fc6bd82/mcp_python-0.1.2-py3-none-any.whl", hash = "sha256:f35331775bc49f960714ef8ec1981533a93fbeccf4252e346247b61dc27e55d1", size = 2989 }, 146 | ] 147 | 148 | [[package]] 149 | name = "mcp2mqtt" 150 | version = "0.1.0" 151 | source = { editable = "." } 152 | dependencies = [ 153 | { name = "mcp-python" }, 154 | { name = "paho-mqtt" }, 155 | { name = "pyyaml" }, 156 | ] 157 | 158 | [package.optional-dependencies] 159 | dev = [ 160 | { name = "pytest" }, 161 | ] 162 | 163 | [package.metadata] 164 | requires-dist = [ 165 | { name = "mcp-python", specifier = ">=0.1.0" }, 166 | { name = "paho-mqtt", specifier = ">=1.6.1" }, 167 | { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.3" }, 168 | { name = "pyyaml", specifier = ">=6.0.1" }, 169 | ] 170 | 171 | [[package]] 172 | name = "packaging" 173 | version = "24.2" 174 | source = { registry = "https://pypi.org/simple" } 175 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 176 | wheels = [ 177 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 178 | ] 179 | 180 | [[package]] 181 | name = "paho-mqtt" 182 | version = "2.1.0" 183 | source = { registry = "https://pypi.org/simple" } 184 | sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848 } 185 | wheels = [ 186 | { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 }, 187 | ] 188 | 189 | [[package]] 190 | name = "pluggy" 191 | version = "1.5.0" 192 | source = { registry = "https://pypi.org/simple" } 193 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 196 | ] 197 | 198 | [[package]] 199 | name = "pydantic" 200 | version = "2.10.4" 201 | source = { registry = "https://pypi.org/simple" } 202 | dependencies = [ 203 | { name = "annotated-types" }, 204 | { name = "pydantic-core" }, 205 | { name = "typing-extensions" }, 206 | ] 207 | sdist = { url = "https://files.pythonhosted.org/packages/70/7e/fb60e6fee04d0ef8f15e4e01ff187a196fa976eb0f0ab524af4599e5754c/pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06", size = 762094 } 208 | wheels = [ 209 | { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765 }, 210 | ] 211 | 212 | [[package]] 213 | name = "pydantic-core" 214 | version = "2.27.2" 215 | source = { registry = "https://pypi.org/simple" } 216 | dependencies = [ 217 | { name = "typing-extensions" }, 218 | ] 219 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 220 | wheels = [ 221 | { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, 222 | { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, 223 | { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, 224 | { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, 225 | { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, 226 | { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, 227 | { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, 228 | { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, 229 | { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, 230 | { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, 231 | { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, 232 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 233 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 234 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 235 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 236 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 237 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 238 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 239 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 240 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 241 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 242 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 243 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 244 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 245 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 246 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 247 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 248 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 249 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 250 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 251 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 252 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 253 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 254 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 255 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 256 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 257 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 258 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 259 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 260 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 261 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 262 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 263 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 264 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 265 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 266 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 267 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 268 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 269 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 270 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 271 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 272 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 273 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 274 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 275 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 276 | { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, 277 | { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, 278 | { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, 279 | { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, 280 | { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, 281 | { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, 282 | { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, 283 | { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, 284 | { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, 285 | ] 286 | 287 | [[package]] 288 | name = "pytest" 289 | version = "8.3.4" 290 | source = { registry = "https://pypi.org/simple" } 291 | dependencies = [ 292 | { name = "colorama", marker = "sys_platform == 'win32'" }, 293 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 294 | { name = "iniconfig" }, 295 | { name = "packaging" }, 296 | { name = "pluggy" }, 297 | { name = "tomli", marker = "python_full_version < '3.11'" }, 298 | ] 299 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } 300 | wheels = [ 301 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, 302 | ] 303 | 304 | [[package]] 305 | name = "pyyaml" 306 | version = "6.0.2" 307 | source = { registry = "https://pypi.org/simple" } 308 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 309 | wheels = [ 310 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, 311 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, 312 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, 313 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, 314 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, 315 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, 316 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, 317 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, 318 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, 319 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 320 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 321 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 322 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 323 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 324 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 325 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 326 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 327 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 328 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 329 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 330 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 331 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 332 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 333 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 334 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 335 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 336 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 337 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 338 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 339 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 340 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 341 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 342 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 343 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 344 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 345 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 346 | ] 347 | 348 | [[package]] 349 | name = "sniffio" 350 | version = "1.3.1" 351 | source = { registry = "https://pypi.org/simple" } 352 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 353 | wheels = [ 354 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 355 | ] 356 | 357 | [[package]] 358 | name = "sse-starlette" 359 | version = "2.2.1" 360 | source = { registry = "https://pypi.org/simple" } 361 | dependencies = [ 362 | { name = "anyio" }, 363 | { name = "starlette" }, 364 | ] 365 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 366 | wheels = [ 367 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 368 | ] 369 | 370 | [[package]] 371 | name = "starlette" 372 | version = "0.44.0" 373 | source = { registry = "https://pypi.org/simple" } 374 | dependencies = [ 375 | { name = "anyio" }, 376 | ] 377 | sdist = { url = "https://files.pythonhosted.org/packages/8d/b4/910f693584958b687b8f9c628f8217cfef19a42b64d2de7840814937365c/starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715", size = 2575579 } 378 | wheels = [ 379 | { url = "https://files.pythonhosted.org/packages/b6/c5/7ae467eeddb57260c8ce17a3a09f9f5edba35820fc022d7c55b7decd5d3a/starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea", size = 73412 }, 380 | ] 381 | 382 | [[package]] 383 | name = "tomli" 384 | version = "2.2.1" 385 | source = { registry = "https://pypi.org/simple" } 386 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 389 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 390 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 391 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 392 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 393 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 394 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 395 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 396 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 397 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 398 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 399 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 400 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 401 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 402 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 403 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 404 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 405 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 406 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 407 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 408 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 409 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 410 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 411 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 412 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 413 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 414 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 415 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 416 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 417 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 418 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 419 | ] 420 | 421 | [[package]] 422 | name = "typing-extensions" 423 | version = "4.12.2" 424 | source = { registry = "https://pypi.org/simple" } 425 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 428 | ] 429 | --------------------------------------------------------------------------------