pyserial-mcp 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install ruff
18
+ - run: ruff check serial_mcp/ tests/
19
+ - run: ruff format --check serial_mcp/ tests/
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ - run: pip install -e ".[dev]"
32
+ - run: pytest -v
@@ -0,0 +1,130 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ verify:
10
+ name: Verify tag matches version files
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - name: Check tag agrees with pyproject.toml and manifest.json
18
+ run: |
19
+ python3 - <<'PY'
20
+ import json, os, sys, tomllib
21
+ tag = os.environ["GITHUB_REF_NAME"]
22
+ if not tag.startswith("v"):
23
+ sys.exit(f"tag {tag!r} must start with 'v'")
24
+ tag_version = tag[1:]
25
+ with open("pyproject.toml", "rb") as f:
26
+ pyproject_version = tomllib.load(f)["project"]["version"]
27
+ with open("manifest.json") as f:
28
+ manifest_version = json.load(f)["version"]
29
+ mismatches = []
30
+ if pyproject_version != tag_version:
31
+ mismatches.append(f"pyproject.toml has {pyproject_version}")
32
+ if manifest_version != tag_version:
33
+ mismatches.append(f"manifest.json has {manifest_version}")
34
+ if mismatches:
35
+ sys.exit(f"Version mismatch — tag is v{tag_version}; " + "; ".join(mismatches))
36
+ print(f"OK: tag, pyproject.toml, and manifest.json all agree on {tag_version}")
37
+ PY
38
+
39
+ lint:
40
+ needs: verify
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+ - run: pip install ruff
48
+ - run: ruff check serial_mcp/ tests/
49
+ - run: ruff format --check serial_mcp/ tests/
50
+
51
+ test:
52
+ needs: verify
53
+ runs-on: ubuntu-latest
54
+ strategy:
55
+ matrix:
56
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-python@v5
60
+ with:
61
+ python-version: ${{ matrix.python-version }}
62
+ - run: pip install -e ".[dev]"
63
+ - run: pytest -v
64
+
65
+ build:
66
+ needs: [lint, test]
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - uses: actions/setup-python@v5
71
+ with:
72
+ python-version: "3.12"
73
+ - uses: actions/setup-node@v4
74
+ with:
75
+ node-version: "20"
76
+ - name: Build sdist and wheel
77
+ run: |
78
+ pip install build
79
+ python -m build
80
+ - name: Build MCPB bundle
81
+ run: bash scripts/build-mcpb.sh
82
+ - name: Stage MCPB artifact
83
+ run: |
84
+ mkdir -p artifacts
85
+ cp build/mcpb/*.mcpb artifacts/
86
+ ls -la artifacts/ dist/
87
+ - name: Upload Python distributions
88
+ uses: actions/upload-artifact@v4
89
+ with:
90
+ name: python-dist
91
+ path: dist/
92
+ - name: Upload MCPB
93
+ uses: actions/upload-artifact@v4
94
+ with:
95
+ name: mcpb
96
+ path: artifacts/*.mcpb
97
+
98
+ publish-pypi:
99
+ needs: build
100
+ runs-on: ubuntu-latest
101
+ environment:
102
+ name: pypi
103
+ url: https://pypi.org/p/pyserial-mcp
104
+ permissions:
105
+ id-token: write
106
+ steps:
107
+ - name: Download Python distributions
108
+ uses: actions/download-artifact@v4
109
+ with:
110
+ name: python-dist
111
+ path: dist/
112
+ - name: Publish to PyPI
113
+ uses: pypa/gh-action-pypi-publish@release/v1
114
+
115
+ github-release:
116
+ needs: publish-pypi
117
+ runs-on: ubuntu-latest
118
+ permissions:
119
+ contents: write
120
+ steps:
121
+ - name: Download MCPB
122
+ uses: actions/download-artifact@v4
123
+ with:
124
+ name: mcpb
125
+ path: artifacts/
126
+ - name: Create GitHub Release
127
+ uses: softprops/action-gh-release@v2
128
+ with:
129
+ generate_release_notes: true
130
+ files: artifacts/*.mcpb
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.mcpb
10
+ .remember/
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ serial-mcp is an MCP (Model Context Protocol) server that enables LLMs to communicate with serial devices (microcontrollers, routers, modems, embedded Linux). Python 3.10+, MIT licensed.
8
+
9
+ ## Build & Run Commands
10
+
11
+ ```bash
12
+ # Install (editable, with dev dependencies)
13
+ uv pip install -e ".[dev]"
14
+
15
+ # Run the MCP server
16
+ python3 -m serial_mcp.server
17
+
18
+ # Test with MCP Inspector
19
+ DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector -- python3 -m serial_mcp.server
20
+
21
+ # Install dependencies only
22
+ uv pip install -r requirements.txt
23
+
24
+ # Run tests
25
+ pytest -v
26
+
27
+ # Lint
28
+ ruff check serial_mcp/ tests/
29
+ ruff format --check serial_mcp/ tests/
30
+
31
+ # Build MCPB bundle
32
+ ./scripts/build-mcpb.sh
33
+ ```
34
+
35
+ ## Architecture
36
+
37
+ Three-file architecture in `serial_mcp/`:
38
+
39
+ - **server.py** — FastMCP server exposing ~22 async tools (all prefixed `serial_*`) and 3 prompts. Maintains a global `_sessions` dict keyed by port name with atexit cleanup. `_resolve_session()` auto-selects when only one session is open. All tools include MCP annotations (readOnlyHint, destructiveHint, etc.). Blocking serial I/O is wrapped in `asyncio.to_thread()`. Tools are grouped: port discovery, connection management, text read/write, binary/hex read/write, hardware signal control, session utilities, XMODEM file transfer, and logging. Supports elicitation for interactive port and baud selection.
40
+
41
+ - **session.py** — `SerialSession` class managing individual serial connections. Runs a daemon background reader thread that stores data in a timestamped ring buffer (10MB default cap). Supports both destructive reads (`read_buffer`) and non-destructive historical reads (`read_since`). Thread safety via `threading.Lock` for history and `threading.Event` for data availability and shutdown signaling.
42
+
43
+ - **xmodem.py** — Pure-Python XMODEM send/receive. Takes read/write callables for testability. Supports checksum and CRC-16 modes.
44
+
45
+ Entry point: `serial_mcp.server:main()` (registered as `serial-mcp` console script via pyproject.toml/Hatchling).
46
+
47
+ ## Key Design Decisions
48
+
49
+ - **Timestamped ring buffer**: All received data is stored with timestamps, enabling `read_since()` for history replay without consuming the buffer. Automatic trimming adjusts the read cursor.
50
+ - **Pattern matching**: `serial_command()` waits for a regex match OR 300ms of silence. `serial_wait_for()` blocks until a pattern appears or timeout.
51
+ - **Hardware signals**: Full DTR/RTS control and CTS/DSR/RI/CD readback for reset sequences and bootloader entry.
52
+ - **Baud detection**: Tries 8 common rates, scores readability by printable ASCII ratio, optional `\r\n` probing.
53
+ - **XMODEM file transfer**: Pure-Python implementation with reader thread pause/resume. Uses callable abstraction for testability.
54
+
55
+ ## Testing
56
+
57
+ 27 unit tests using pytest with a `MockSerial` fixture (no real hardware needed). Tests cover session buffer management, pattern matching, history trimming, logging, XMODEM protocol, and server tool resolution.
58
+
59
+ Run: `pytest -v` (requires dev dependencies: `uv pip install -e ".[dev]"`)
60
+
61
+ ## Gotchas
62
+
63
+ - **XMODEM pauses the reader thread**: During `serial_xmodem_send`/`serial_xmodem_receive`, the background reader is stopped for exclusive port access. Logging and `read_buffer` won't capture data during transfers.
64
+ - **Elicitation fallback**: `serial_open` (portless) and `serial_detect_baud` use elicitation when supported. If the host doesn't support it, they return data for the LLM to relay — not an error.
65
+
66
+ ## Dependencies
67
+
68
+ Only two runtime deps: `mcp >= 1.0.0` and `pyserial >= 3.5`. Dev: `ruff`, `pytest`, `pytest-asyncio`.
69
+
70
+ ## Releasing
71
+
72
+ Releases are automated by `.github/workflows/release.yml` on tag push (`v*.*.*`). To cut a release: bump `version` in **both** `pyproject.toml` and `manifest.json`, commit, then `git tag vX.Y.Z && git push origin vX.Y.Z`. The workflow verifies the tag matches both files, runs lint + tests, builds sdist/wheel/`.mcpb`, publishes to PyPI via Trusted Publishing (gated by a manual approval), and creates a GitHub Release with the `.mcpb` attached. See [RELEASING.md](RELEASING.md) for full procedure and recovery steps.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alex Gompper
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.
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyserial-mcp
3
+ Version: 0.4.0
4
+ Summary: MCP server for serial device communication
5
+ Project-URL: Homepage, https://github.com/alxgmpr/serial-mcp
6
+ Project-URL: Repository, https://github.com/alxgmpr/serial-mcp
7
+ Project-URL: Issues, https://github.com/alxgmpr/serial-mcp/issues
8
+ Author: Alex Gompper
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: embedded,iot,mcp,pyserial,serial,uart
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications
21
+ Classifier: Topic :: System :: Hardware
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: mcp>=1.0.0
24
+ Requires-Dist: pyserial>=3.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # serial-mcp
32
+
33
+ MCP server for serial port communication. Lets LLMs talk to hardware — microcontrollers, routers, modems, embedded Linux, anything with a UART.
34
+
35
+ Why use this and not any of the many others out there?
36
+
37
+ 1. Real useful tools that the LLM can use like waiting/expecting data.
38
+ 2. Better test suite, and I test the commands myself.
39
+ 3. I actually use this day to day for real hardware hacking.
40
+
41
+ <img width="1456" height="1132" alt="image" src="https://github.com/user-attachments/assets/17e948ae-4888-4748-8694-77c1e257e329" />
42
+
43
+
44
+ ## What it does
45
+
46
+ Exposes serial ports as MCP tools so an AI assistant can:
47
+
48
+ - **Discover** connected USB-serial adapters and identify them by VID/PID
49
+ - **Connect** to devices with configurable baud rate, data bits, stop bits, parity
50
+ - **Send commands** and wait for responses (with regex-based expect patterns)
51
+ - **Read/write raw hex** for binary protocols (Modbus, bootloader commands, etc.)
52
+ - **Control hardware signals** (DTR/RTS) — reset Arduinos, enter ESP32 bootloader mode
53
+ - **Auto-detect baud rate** by trying common rates and scoring readability
54
+ - **Transfer files** with XMODEM (checksum or CRC-16)
55
+ - **Log received data** to a file for capture / postmortem analysis
56
+ - **Manage multiple sessions** simultaneously across different ports
57
+
58
+ ## Install
59
+
60
+ ### With `uv` (recommended)
61
+
62
+ Install globally so the `serial-mcp` command is available everywhere:
63
+
64
+ ```sh
65
+ uv tool install serial-mcp
66
+ ```
67
+
68
+ Or from a local clone:
69
+
70
+ ```sh
71
+ uv tool install /path/to/serial-mcp
72
+ ```
73
+
74
+ ### With pip
75
+
76
+ ```sh
77
+ pip install serial-mcp
78
+ ```
79
+
80
+ ### From source (editable)
81
+
82
+ ```sh
83
+ git clone https://github.com/alxgmpr/serial-mcp.git
84
+ cd serial-mcp
85
+ uv pip install -e .
86
+ ```
87
+
88
+ ## Configure
89
+
90
+ ### Claude Code
91
+
92
+ ```sh
93
+ claude mcp add serial-mcp -- serial-mcp
94
+ ```
95
+
96
+ That's it. Verify with `claude mcp list`.
97
+
98
+ If you installed from source instead of globally, use the full path:
99
+
100
+ ```sh
101
+ claude mcp add serial-mcp -- python3 -m serial_mcp.server
102
+ ```
103
+
104
+ ### Claude Desktop (`claude_desktop_config.json`)
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "serial": {
110
+ "command": "serial-mcp"
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### With `uvx` (no install)
117
+
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "serial": {
122
+ "command": "uvx",
123
+ "args": ["serial-mcp"]
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Tools
130
+
131
+ All tools are prefixed with `serial_` to avoid name collisions with other MCP servers. Each tool includes MCP annotations (`readOnlyHint`, `destructiveHint`, etc.).
132
+
133
+ ### Port discovery
134
+
135
+ | Tool | Description |
136
+ |---|---|
137
+ | `list_serial_ports` | List available serial ports with USB metadata (VID/PID, manufacturer) |
138
+ | `serial_detect_baud` | Auto-detect baud rate by trying common rates and scoring ASCII readability |
139
+
140
+ ### Connection management
141
+
142
+ | Tool | Description |
143
+ |---|---|
144
+ | `serial_open` | Open a serial connection (baud, data bits, stop bits, parity, timeout) |
145
+ | `serial_close` | Close a connection |
146
+ | `serial_change_settings` | Change baud/parity/etc. on a live connection without closing it |
147
+ | `serial_list_sessions` | List all open sessions |
148
+ | `serial_status` | Detailed connection health, byte counts, uptime |
149
+
150
+ ### Read / write (text)
151
+
152
+ | Tool | Description |
153
+ |---|---|
154
+ | `serial_command` | Send a string, wait for response. Supports `expect` regex for prompt detection |
155
+ | `serial_write` | Fire-and-forget text write |
156
+ | `serial_read` | Read buffered data (advances cursor) |
157
+ | `serial_read_since` | Read historical data since a timestamp (non-destructive) |
158
+ | `serial_wait_for` | Block until a regex pattern appears in incoming data |
159
+
160
+ ### Read / write (binary)
161
+
162
+ | Tool | Description |
163
+ |---|---|
164
+ | `serial_write_hex` | Write raw bytes as hex (`"AA 55 01 03"`) |
165
+ | `serial_read_hex` | Read buffered data as hex string |
166
+
167
+ ### Hardware signals
168
+
169
+ | Tool | Description |
170
+ |---|---|
171
+ | `serial_set_signals` | Control DTR/RTS (reset micros, enter bootloader, etc.) |
172
+ | `serial_get_signals` | Read DTR, RTS, CTS, DSR, RI, CD |
173
+ | `serial_send_break` | Send a serial break (interrupt U-Boot, Cisco ROMMON, etc.) |
174
+
175
+ ### Session utilities
176
+
177
+ | Tool | Description |
178
+ |---|---|
179
+ | `serial_clear_history` | Flush the receive buffer and free memory |
180
+
181
+ ### Logging
182
+
183
+ | Tool | Description |
184
+ |---|---|
185
+ | `serial_log_start` | Capture all received data to a file (like minicom's capture) |
186
+ | `serial_log_stop` | Stop logging and return file path, byte count, and duration |
187
+
188
+ ### File transfer
189
+
190
+ | Tool | Description |
191
+ |---|---|
192
+ | `serial_xmodem_send` | Send a file via XMODEM (checksum or CRC-16 mode) |
193
+ | `serial_xmodem_receive` | Receive a file via XMODEM (checksum or CRC-16 mode) |
194
+
195
+ The reader thread is paused for the duration of an XMODEM transfer so the protocol has exclusive port access — `serial_read` and logging won't capture anything during the transfer.
196
+
197
+ ## Prompts
198
+
199
+ Three prompts are registered to guide common workflows:
200
+
201
+ | Prompt | Description |
202
+ |---|---|
203
+ | `scan_devices` | Walk through identifying all connected serial devices by VID/PID |
204
+ | `detect_baud_rate` | Run baud detection on a port and interpret the results |
205
+ | `interactive_shell` | Open a connection and probe for the device prompt |
206
+
207
+ ## Usage examples
208
+
209
+ ### Interactive shell on a Linux device
210
+
211
+ ```
212
+ 1. list_serial_ports() → find /dev/ttyUSB0
213
+ 2. serial_open(port="/dev/ttyUSB0") → connect at 115200 8N1
214
+ 3. serial_command(data="", expect="[$#]") → get the shell prompt
215
+ 4. serial_command(data="uname -a", expect="\\$")
216
+ ```
217
+
218
+ ### Arduino / microcontroller
219
+
220
+ ```
221
+ 1. list_serial_ports() → find /dev/ttyACM0
222
+ 2. serial_open(port="/dev/ttyACM0", baud_rate=9600)
223
+ 3. serial_command(data="STATUS", timeout=2)
224
+ 4. serial_set_signals(dtr=False) → reset the board
225
+ 5. serial_set_signals(dtr=True)
226
+ 6. serial_wait_for(pattern="Ready", timeout=5)
227
+ ```
228
+
229
+ ### Unknown baud rate
230
+
231
+ ```
232
+ 1. serial_detect_baud(port="/dev/ttyUSB0") → recommends 9600
233
+ 2. serial_open(port="/dev/ttyUSB0", baud_rate=9600)
234
+ ```
235
+
236
+ ### Binary protocol (Modbus, etc.)
237
+
238
+ ```
239
+ 1. serial_open(port="/dev/ttyUSB0", baud_rate=9600)
240
+ 2. serial_write_hex(hex_string="01 03 00 00 00 0A C5 CD")
241
+ 3. serial_read_hex(timeout=2)
242
+ ```
243
+
244
+ ### ESP32 bootloader entry
245
+
246
+ ```
247
+ 1. serial_open(port="/dev/ttyUSB0", baud_rate=115200)
248
+ 2. serial_set_signals(dtr=False, rts=True)
249
+ 3. serial_set_signals(dtr=True, rts=False)
250
+ 4. serial_set_signals(dtr=False)
251
+ 5. serial_wait_for(pattern="waiting for download", timeout=3)
252
+ ```
253
+
254
+ ## How it works
255
+
256
+ Each `serial_open()` call creates a `SerialSession` with a background thread that continuously reads from the port into a timestamped ring buffer (default 10MB cap). This means:
257
+
258
+ - **No data loss** — bytes are captured even between tool calls
259
+ - **Non-destructive reads** — `serial_read_since()` can replay history without advancing the cursor
260
+ - **Pattern matching** — `serial_command()` and `serial_wait_for()` scan the buffer for regex matches in real-time
261
+ - **Multiple sessions** — each port gets its own thread and buffer
262
+
263
+ All tools are async. Blocking serial I/O runs in `asyncio.to_thread()` so the event loop stays free.
264
+
265
+ ## Testing
266
+
267
+ Run the unit tests (no hardware required — they use a `MockSerial` fixture):
268
+
269
+ ```sh
270
+ uv pip install -e ".[dev]"
271
+ pytest -v
272
+ ```
273
+
274
+ Smoke-test the live server with the MCP Inspector:
275
+
276
+ ```sh
277
+ DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector -- python3 -m serial_mcp.server
278
+ ```
279
+
280
+ Set command to `python3` and args to `-m serial_mcp.server` in the inspector UI, then connect.
281
+
282
+ ## Requirements
283
+
284
+ - Python >= 3.10
285
+ - [pyserial](https://pyserial.readthedocs.io/) >= 3.5
286
+ - [mcp](https://github.com/modelcontextprotocol/python-sdk) >= 1.0.0
287
+
288
+ ## License
289
+
290
+ MIT