├── .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 |

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 |

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 |

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 |

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 |

35 |
mcp2mqtt System Architecture
36 |
37 |
38 | ## Workflow
39 |
40 |
41 |

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 |

276 |
Example in Claude
277 |
278 |
279 |

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 |
--------------------------------------------------------------------------------