serial-mcp-server 0.1.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.
Files changed (44) hide show
  1. serial_mcp_server-0.1.0/.github/workflows/cicd.yml +157 -0
  2. serial_mcp_server-0.1.0/.gitignore +17 -0
  3. serial_mcp_server-0.1.0/.pre-commit-config.yaml +7 -0
  4. serial_mcp_server-0.1.0/CHANGELOG.md +52 -0
  5. serial_mcp_server-0.1.0/CONTRIBUTING.md +95 -0
  6. serial_mcp_server-0.1.0/LICENSE +21 -0
  7. serial_mcp_server-0.1.0/PKG-INFO +294 -0
  8. serial_mcp_server-0.1.0/README.md +270 -0
  9. serial_mcp_server-0.1.0/docs/assets/scan.gif +0 -0
  10. serial_mcp_server-0.1.0/docs/concepts.md +238 -0
  11. serial_mcp_server-0.1.0/docs/tools.md +407 -0
  12. serial_mcp_server-0.1.0/examples/demo-device/README.md +62 -0
  13. serial_mcp_server-0.1.0/examples/demo-device/demo-device-spec.md +272 -0
  14. serial_mcp_server-0.1.0/examples/demo-device/demo_device_plugin.py +308 -0
  15. serial_mcp_server-0.1.0/examples/demo-device/pty_server.py +435 -0
  16. serial_mcp_server-0.1.0/examples/demo-device/uart_server.py +415 -0
  17. serial_mcp_server-0.1.0/pyproject.toml +81 -0
  18. serial_mcp_server-0.1.0/serial_mcp_server/__init__.py +1 -0
  19. serial_mcp_server-0.1.0/serial_mcp_server/__main__.py +5 -0
  20. serial_mcp_server-0.1.0/serial_mcp_server/_version.py +34 -0
  21. serial_mcp_server-0.1.0/serial_mcp_server/handlers_introspection.py +63 -0
  22. serial_mcp_server-0.1.0/serial_mcp_server/handlers_plugin.py +254 -0
  23. serial_mcp_server-0.1.0/serial_mcp_server/handlers_serial.py +726 -0
  24. serial_mcp_server-0.1.0/serial_mcp_server/handlers_spec.py +215 -0
  25. serial_mcp_server-0.1.0/serial_mcp_server/handlers_trace.py +67 -0
  26. serial_mcp_server-0.1.0/serial_mcp_server/helpers.py +46 -0
  27. serial_mcp_server-0.1.0/serial_mcp_server/mirror.py +348 -0
  28. serial_mcp_server-0.1.0/serial_mcp_server/plugins.py +306 -0
  29. serial_mcp_server-0.1.0/serial_mcp_server/server.py +205 -0
  30. serial_mcp_server-0.1.0/serial_mcp_server/specs.py +391 -0
  31. serial_mcp_server-0.1.0/serial_mcp_server/state.py +118 -0
  32. serial_mcp_server-0.1.0/serial_mcp_server/trace.py +122 -0
  33. serial_mcp_server-0.1.0/server.json +50 -0
  34. serial_mcp_server-0.1.0/tests/__init__.py +0 -0
  35. serial_mcp_server-0.1.0/tests/conftest.py +63 -0
  36. serial_mcp_server-0.1.0/tests/test_handlers_introspection.py +36 -0
  37. serial_mcp_server-0.1.0/tests/test_handlers_serial.py +352 -0
  38. serial_mcp_server-0.1.0/tests/test_mirror.py +335 -0
  39. serial_mcp_server-0.1.0/tests/test_plugins.py +679 -0
  40. serial_mcp_server-0.1.0/tests/test_server.py +41 -0
  41. serial_mcp_server-0.1.0/tests/test_specs.py +661 -0
  42. serial_mcp_server-0.1.0/tests/test_state.py +166 -0
  43. serial_mcp_server-0.1.0/tests/test_trace.py +210 -0
  44. serial_mcp_server-0.1.0/tools/serial_cli.py +597 -0
@@ -0,0 +1,157 @@
1
+ name: CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: write
12
+ id-token: write # trusted publishing to PyPI
13
+
14
+ jobs:
15
+ lint:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.11"
22
+ cache: pip
23
+ - run: pip install ruff
24
+ - run: ruff check .
25
+ - run: ruff format --check .
26
+
27
+ test:
28
+ runs-on: ubuntu-latest
29
+ strategy:
30
+ matrix:
31
+ python-version: ["3.11", "3.12", "3.13"]
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: ${{ matrix.python-version }}
37
+ cache: pip
38
+ - run: pip install -e ".[test]"
39
+ - run: python -m pytest tests/ -v --cov --cov-report=term-missing --cov-report="markdown:coverage.md"
40
+ - uses: actions/upload-artifact@v4
41
+ if: matrix.python-version == '3.13'
42
+ with:
43
+ name: coverage-report
44
+ path: coverage.md
45
+
46
+ build:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ with:
51
+ fetch-depth: 0 # full history for hatch-vcs versioning
52
+ - uses: actions/setup-python@v5
53
+ with:
54
+ python-version: "3.11"
55
+ cache: pip
56
+ - run: pip install build twine
57
+ - run: python -m build
58
+ - run: twine check dist/*
59
+ - uses: actions/upload-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+
64
+ security:
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ - uses: actions/setup-python@v5
69
+ with:
70
+ python-version: "3.11"
71
+ cache: pip
72
+ - run: pip install bandit pip-audit
73
+ - name: Bandit (static security analysis)
74
+ run: bandit -r serial_mcp_server/ -c pyproject.toml
75
+ - name: pip-audit (dependency vulnerabilities)
76
+ continue-on-error: true
77
+ run: |
78
+ pip install -e .
79
+ pip-audit --strict --desc | tee pip-audit-report.txt
80
+ - uses: actions/upload-artifact@v4
81
+ if: always()
82
+ with:
83
+ name: pip-audit-report
84
+ path: pip-audit-report.txt
85
+ - name: Summary
86
+ if: always()
87
+ run: |
88
+ echo "## Security scan results" >> "$GITHUB_STEP_SUMMARY"
89
+ echo "### Bandit (static analysis)" >> "$GITHUB_STEP_SUMMARY"
90
+ echo "Passed" >> "$GITHUB_STEP_SUMMARY"
91
+ echo "### pip-audit (dependency vulnerabilities)" >> "$GITHUB_STEP_SUMMARY"
92
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
93
+ if [ -f pip-audit-report.txt ]; then
94
+ cat pip-audit-report.txt >> "$GITHUB_STEP_SUMMARY"
95
+ else
96
+ echo "pip-audit did not run" >> "$GITHUB_STEP_SUMMARY"
97
+ fi
98
+ echo '```' >> "$GITHUB_STEP_SUMMARY"
99
+
100
+ release:
101
+ if: startsWith(github.ref, 'refs/tags/v')
102
+ needs: [lint, test, build, security]
103
+ runs-on: ubuntu-latest
104
+ steps:
105
+ - uses: actions/checkout@v4
106
+ - uses: actions/download-artifact@v4
107
+ with:
108
+ name: dist
109
+ path: dist/
110
+ - name: Extract changelog
111
+ if: ${{ !contains(github.ref_name, 'rc') }}
112
+ run: |
113
+ # Extract the section for this version (e.g. "## 0.1.0" from tag "v0.1.0")
114
+ VERSION="${GITHUB_REF_NAME#v}"
115
+ awk "/^## ${VERSION}$/,/^## /" CHANGELOG.md | head -n -1 > release-notes.md
116
+ - name: Create GitHub Release
117
+ env:
118
+ GH_TOKEN: ${{ github.token }}
119
+ run: |
120
+ if [[ "${{ github.ref_name }}" == *rc* ]]; then
121
+ gh release create "${{ github.ref_name }}" \
122
+ --title "${{ github.ref_name }}" \
123
+ --generate-notes \
124
+ --prerelease \
125
+ dist/*
126
+ else
127
+ gh release create "${{ github.ref_name }}" \
128
+ --title "${{ github.ref_name }}" \
129
+ --notes-file release-notes.md \
130
+ dist/*
131
+ fi
132
+
133
+ publish-testpypi:
134
+ if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, 'rc')
135
+ needs: release
136
+ runs-on: ubuntu-latest
137
+ environment: testpypi
138
+ steps:
139
+ - uses: actions/download-artifact@v4
140
+ with:
141
+ name: dist
142
+ path: dist/
143
+ - uses: pypa/gh-action-pypi-publish@release/v1
144
+ with:
145
+ repository-url: https://test.pypi.org/legacy/
146
+
147
+ publish-pypi:
148
+ if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, 'rc')
149
+ needs: release
150
+ runs-on: ubuntu-latest
151
+ environment: pypi
152
+ steps:
153
+ - uses: actions/download-artifact@v4
154
+ with:
155
+ name: dist
156
+ path: dist/
157
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .pytest_cache/
9
+ .venv/
10
+ serial_mcp_server/_version.py
11
+ .serial_mcp/index.json
12
+ .serial_mcp/traces/
13
+ coverage.md
14
+ .coverage
15
+ .DS_Store
16
+ .mcpregistry_*
17
+ .ble_mcp/
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.9.7
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ ### Serial Core
8
+ - List available serial ports with device metadata (VID, PID, manufacturer, serial number)
9
+ - Open connections with configurable baud rate, byte size, parity, stop bits, timeout, and encoding
10
+ - Close connections, query connection status
11
+ - Read data with configurable byte count and timeout override
12
+ - Write data in text, hex, or base64 format, with optional newline append
13
+ - Line-oriented I/O: `serial.readline` and `serial.read_until` with custom delimiters
14
+ - Flush input/output buffers (or both)
15
+ - Control lines: set and pulse DTR/RTS (useful for hardware reset and boot mode entry)
16
+ - Duplicate port detection (rejects opening the same port twice)
17
+ - Graceful shutdown (closes all serial ports on exit)
18
+
19
+ ### PTY Mirror
20
+ - Virtual clone ports via pseudo-terminals (`SERIAL_MCP_MIRROR=ro` or `rw`)
21
+ - External tools (screen, picocom, etc.) can attach to the same serial session
22
+ - Default symlink at `/tmp/serial-mcp0`, configurable via `SERIAL_MCP_MIRROR_LINK`
23
+ - macOS and Linux only
24
+
25
+ ### Introspection
26
+ - `serial.connections.list` for recovering connection IDs and inspecting state
27
+
28
+ ### Protocol Specs
29
+ - Markdown specs with YAML front-matter (`kind: serial-protocol`, `name`)
30
+ - Template generation, registration, indexing
31
+ - Attach specs to connections for agent reference
32
+ - Full-text search over spec content
33
+
34
+ ### Tracing
35
+ - JSONL tracing of every tool call (in-memory ring buffer + file sink)
36
+ - Configurable payload logging with truncation
37
+ - `serial.trace.status` and `serial.trace.tail` for inspection
38
+
39
+ ### Plugins
40
+ - User plugins in `.serial_mcp/plugins/` (single files or packages)
41
+ - Plugin contract: `TOOLS`, `HANDLERS`, optional `META` for device matching
42
+ - `SERIAL_MCP_PLUGINS` env var: `all` or comma-separated allowlist
43
+ - `serial.plugin.template` for generating plugin skeletons
44
+ - `serial.plugin.list` with metadata, `serial.plugin.load`, `serial.plugin.reload`
45
+ - Hot-reload without server restart
46
+
47
+ ### Security
48
+ - Plugin path containment: `serial.plugin.load` rejects paths outside `.serial_mcp/plugins/`
49
+ - Spec path containment: `serial.spec.register` rejects paths outside the project directory
50
+ - Trace file always writes to `.serial_mcp/traces/trace.jsonl` (no configurable path)
51
+ - Symlink check on trace file path
52
+ - Input validation for hex/base64 write payloads
@@ -0,0 +1,95 @@
1
+ # Contributing
2
+
3
+ ## Dev setup
4
+
5
+ ```bash
6
+ # Clone and install in editable mode with test dependencies
7
+ git clone https://github.com/es617/serial-mcp-server.git
8
+ cd serial-mcp-server
9
+ pip install -e ".[test]"
10
+
11
+ # Run tests (no serial hardware needed)
12
+ python -m pytest tests/ -v
13
+ ```
14
+
15
+ ## How tools are registered
16
+
17
+ Each `handlers_*.py` file exports:
18
+
19
+ ```python
20
+ TOOLS: list[Tool] = [...] # Tool definitions with names, descriptions, schemas
21
+ HANDLERS: dict[str, Callable] = { # Maps tool name → async handler function
22
+ "serial.tool_name": handle_fn,
23
+ }
24
+ ```
25
+
26
+ In `server.py`, these are merged inside `build_server()`:
27
+
28
+ ```python
29
+ tools = handlers_serial.TOOLS + handlers_introspection.TOOLS + handlers_spec.TOOLS + handlers_trace.TOOLS + handlers_plugin.TOOLS
30
+ handlers = {**handlers_serial.HANDLERS, **handlers_introspection.HANDLERS, **handlers_spec.HANDLERS, **handlers_trace.HANDLERS}
31
+ ```
32
+
33
+ Plugin handlers are added via `handlers_plugin.make_handlers()`, which returns closures that capture the `PluginManager` and `Server` instances.
34
+
35
+ ## Handler pattern
36
+
37
+ Every handler has the same signature:
38
+
39
+ ```python
40
+ async def handle_something(state: SerialState, args: dict[str, Any]) -> dict[str, Any]:
41
+ ```
42
+
43
+ - `state` — shared serial state (connections)
44
+ - `args` — parsed tool arguments from the MCP client
45
+ - Returns `_ok(key=value)` on success or `_err(code, message)` on failure
46
+
47
+ The dispatcher in `server.py` catches common exceptions (KeyError, SerialException, TimeoutError, etc.) and converts them to error responses automatically.
48
+
49
+ ## Adding a new tool
50
+
51
+ 1. Add the `Tool(...)` definition to the appropriate `handlers_*.py` `TOOLS` list
52
+ 2. Write the handler function following the signature above
53
+ 3. Add the mapping to the `HANDLERS` dict
54
+ 4. Add tests in the corresponding `test_*.py`
55
+
56
+ Tool names follow the convention `serial.<action>` for core tools (e.g., `serial.read`, `serial.open`) and `serial.<category>.<action>` for subsystems (e.g., `serial.spec.read`, `serial.plugin.reload`).
57
+
58
+ ## Plugin system internals
59
+
60
+ **Path containment:** `PluginManager.load()` resolves the path and verifies it is inside `plugins_dir` before loading. Paths outside `.serial_mcp/plugins/` are rejected with `ValueError`.
61
+
62
+ **Loading:** `load_plugin()` uses `importlib` to load a `.py` file or package `__init__.py`. It validates `TOOLS`, `HANDLERS`, and optional `META` exports, and registers the module in `sys.modules` with a unique key (`serial_mcp_plugin__{name}__{hash}`).
63
+
64
+ **Name collisions:** If a plugin tool name collides with any existing tool (core or other plugin), loading fails with `ValueError`.
65
+
66
+ **Policy:** `SERIAL_MCP_PLUGINS` env var is parsed into `(enabled, allowlist)`. The `PluginManager` checks this before every `load()` call. `load_all()` skips entirely when disabled.
67
+
68
+ **Hot reload:** `reload(name)` calls `unload(name)` then `load(path)`. Unload filters the TOOLS list in-place and pops handler keys. The old module is deleted from `sys.modules`.
69
+
70
+ **Limitation:** MCP clients may not refresh their tool list mid-session. Newly loaded plugins may require a client restart to call their tools. Hot-reload of existing plugins works without restart.
71
+
72
+ ## MCP Inspector
73
+
74
+ The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) lets you call tools without an agent:
75
+
76
+ ```bash
77
+ npx @modelcontextprotocol/inspector python -m serial_mcp_server
78
+ ```
79
+
80
+ Open the URL with the auth token printed in the terminal. Use the **Tools** tab to call any tool interactively.
81
+
82
+ ## Tests
83
+
84
+ All tests run without serial hardware. They use `MagicMock` for pyserial objects, `tmp_path` fixtures for filesystem isolation, and `monkeypatch` for environment variables.
85
+
86
+ ```bash
87
+ # Run all tests
88
+ python -m pytest tests/ -v
89
+
90
+ # Run a specific test file
91
+ python -m pytest tests/test_plugins.py -v
92
+
93
+ # Run a specific test
94
+ python -m pytest tests/test_plugins.py::TestPluginManager::test_load_adds_tools_and_handlers -v
95
+ ```
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Enrico Santagati and 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.
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.4
2
+ Name: serial-mcp-server
3
+ Version: 0.1.0
4
+ Summary: Serial port Model Context Protocol server for AI agents and developer tooling
5
+ Project-URL: Homepage, https://github.com/es617/serial-mcp-server
6
+ Project-URL: Repository, https://github.com/es617/serial-mcp-server
7
+ Project-URL: Issues, https://github.com/es617/serial-mcp-server/issues
8
+ Project-URL: Changelog, https://github.com/es617/serial-mcp-server/blob/main/CHANGELOG.md
9
+ Author: Enrico Santagati
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: mcp<2,>=1.0
14
+ Requires-Dist: pyserial<4,>=3.5
15
+ Requires-Dist: pyyaml<7,>=6.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
18
+ Requires-Dist: ruff>=0.9; extra == 'dev'
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
21
+ Requires-Dist: pytest-cov>=5.0; extra == 'test'
22
+ Requires-Dist: pytest>=8.0; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Serial MCP Server
26
+
27
+ <!-- mcp-name: io.github.es617/serial-mcp-server -->
28
+
29
+ ![MCP](https://img.shields.io/badge/MCP-compatible-blue)
30
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
31
+ ![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)
32
+ ![Serial](https://img.shields.io/badge/Serial-RS232%2FUART-green)
33
+
34
+ A stateful serial port Model Context Protocol (MCP) server for developer tooling and AI agents.
35
+ Works out of the box with Claude Code and any MCP-compatible runtime. Communicates over **stdio** and uses [pyserial](https://github.com/pyserial/pyserial) for cross-platform serial on macOS, Windows, and Linux.
36
+
37
+ > **Example:** Let Claude Code list available serial ports, connect to your microcontroller, reset it via DTR, and read the boot banner from your hardware.
38
+
39
+ ### Demo
40
+
41
+ [Video walkthrough](https://www.youtube.com/watch?v=FdFdXjoyyAM) — connecting to a serial device, sending commands, reading responses, and creating plugins.
42
+
43
+ ---
44
+
45
+ ## Why this exists
46
+ If you’ve ever copy-pasted commands into `screen` or `minicom`, guessed baud rates, toggled DTR to kick a bootloader, and re-run the same test sequence 20 times — this is for you.
47
+
48
+
49
+ You have a serial device. You want an AI agent to talk to it — open a port, send commands, read responses, debug protocols. This server makes that possible.
50
+
51
+ It gives any MCP-compatible agent a full set of serial tools: listing ports, opening connections, reading, writing, line-oriented I/O, control line manipulation — plus protocol specs and device plugins, so the agent can reason about higher-level device behavior instead of just raw bytes.
52
+
53
+ The agent calls these tools, gets structured JSON back, and reasons about what to do next — without you manually typing commands into a terminal for every step.
54
+
55
+ **What agents can do with it:**
56
+
57
+ - **Develop and debug** — connect to your device, send commands, read responses, and diagnose issues conversationally (boot banners, prompts, error codes).
58
+ - **Iterate on new firmware** — attach a protocol spec so the agent understands your command set, boot modes, and output format as they evolve.
59
+ - **Automate test flows** — reset device via DTR, wait for prompt, run a command sequence, validate output.
60
+ - **Explore unknown devices** — probe command sets, discover prompts, infer message formats.
61
+ - **Build serial automation** — long-running test rigs, manufacturing bring-up, CI hardware smoke tests.
62
+
63
+ ---
64
+
65
+ ## Who is this for?
66
+
67
+ - **Embedded engineers** — faster iteration on serial protocols, conversational debugging, automated test sequences
68
+ - **Hobbyists and makers** — interact with serial devices without writing boilerplate; let the agent help reverse-engineer simple protocols
69
+ - **QA and test engineers** — build repeatable serial test suites with plugin tools
70
+ - **Support and field engineers** — diagnose serial device issues interactively without specialized tooling
71
+ - **Researchers** — automate data collection from serial devices, explore device capabilities systematically
72
+
73
+ ---
74
+
75
+ ## Quickstart (Claude Code)
76
+
77
+ ```bash
78
+ pip install serial-mcp-server
79
+
80
+ # Register the MCP server with Claude Code
81
+ claude mcp add serial -- serial_mcp
82
+ ```
83
+
84
+ Then in Claude Code, try:
85
+
86
+ > "List available serial ports and connect to the one on /dev/ttyUSB0 at 115200 baud."
87
+
88
+ <p align="center"><img src="https://raw.githubusercontent.com/es617/serial-mcp-server/main/docs/assets/scan.gif" alt="Scanning serial ports" width="600"></p>
89
+
90
+ ---
91
+
92
+ ## What the agent can do
93
+
94
+ Once connected, the agent has full serial capabilities:
95
+
96
+ - **List ports** to find available serial devices
97
+ - **Open and close** connections with configurable baud rate, parity, stop bits, and encoding
98
+ - **Read and write** data in text, hex, or base64 format
99
+ - **Line-oriented I/O** — readline and read-until-delimiter for text protocols
100
+ - **Control lines** — set or pulse DTR and RTS for hardware reset and boot mode entry
101
+ - **Flush** input and output buffers
102
+ - **Attach protocol specs** to understand device-specific commands and data formats
103
+ - **Use plugins** for high-level device operations instead of raw reads/writes
104
+ - **Create specs and plugins** for new devices so future sessions start "knowing" your protocol
105
+ - **PTY mirroring** — attach screen, minicom, or custom scripts to the same serial session the agent is using
106
+
107
+ The agent can coordinate multi-step flows automatically — e.g., toggle reset, wait for prompt, send init sequence, stream output.
108
+
109
+ At a high level:
110
+
111
+ **Raw Serial → Protocol Spec → Plugin**
112
+
113
+ You can start with raw serial tools, then move up the stack as your device protocol becomes understood and repeatable.
114
+
115
+ ---
116
+
117
+ ## Install (development)
118
+
119
+ ```bash
120
+ # Editable install from repo root
121
+ pip install -e .
122
+
123
+ # Or with uv
124
+ uv pip install -e .
125
+ ```
126
+
127
+ ## Add to Claude Code
128
+
129
+ ```bash
130
+ # Standard setup
131
+ claude mcp add serial -- serial_mcp
132
+
133
+ # Or run as a module
134
+ claude mcp add serial -- python -m serial_mcp_server
135
+
136
+ # Enable all plugins
137
+ claude mcp add serial -e SERIAL_MCP_PLUGINS=all -- serial_mcp
138
+
139
+ # Enable specific plugins only
140
+ claude mcp add serial -e SERIAL_MCP_PLUGINS=mydevice,ota -- serial_mcp
141
+
142
+ # Debug logging
143
+ claude mcp add serial -e SERIAL_MCP_LOG_LEVEL=DEBUG -- serial_mcp
144
+ ```
145
+
146
+ > MCP is a protocol. Claude Code is one MCP client; other agent runtimes can also connect to this server.
147
+
148
+ ## Environment variables
149
+
150
+ | Variable | Default | Description |
151
+ |---|---|---|
152
+ | `SERIAL_MCP_MAX_CONNECTIONS` | `10` | Maximum simultaneous open serial connections. |
153
+ | `SERIAL_MCP_PLUGINS` | disabled | Plugin policy: `all` to allow all, or `name1,name2` to allow specific plugins. Unset = disabled. |
154
+ | `SERIAL_MCP_MIRROR` | `off` | PTY mirror mode: `off`, `ro` (read-only), or `rw` (read-write). macOS and Linux only. |
155
+ | `SERIAL_MCP_MIRROR_LINK` | `/tmp/serial-mcp` | Base path for PTY symlinks. Connections get numbered: `/tmp/serial-mcp0`, `/tmp/serial-mcp1`, etc. |
156
+ | `SERIAL_MCP_LOG_LEVEL` | `WARNING` | Python log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Logs go to stderr. |
157
+ | `SERIAL_MCP_TRACE` | enabled | JSONL tracing of every tool call. Set to `0`, `false`, or `no` to disable. |
158
+ | `SERIAL_MCP_TRACE_PAYLOADS` | disabled | Include write `data` in traced args (stripped by default). |
159
+ | `SERIAL_MCP_TRACE_MAX_BYTES` | `16384` | Max payload chars before truncation (only applies when `TRACE_PAYLOADS` is on). |
160
+
161
+ ---
162
+
163
+ ## Tools
164
+
165
+ | Category | Tools |
166
+ |---|---|
167
+ | **Serial Core** | `serial.list_ports`, `serial.open`, `serial.close`, `serial.connection_status`, `serial.read`, `serial.write`, `serial.readline`, `serial.read_until`, `serial.flush`, `serial.set_dtr`, `serial.set_rts`, `serial.pulse_dtr`, `serial.pulse_rts` |
168
+ | **Introspection** | `serial.connections.list` |
169
+ | **Protocol Specs** | `serial.spec.template`, `serial.spec.register`, `serial.spec.list`, `serial.spec.attach`, `serial.spec.get`, `serial.spec.read`, `serial.spec.search` |
170
+ | **Tracing** | `serial.trace.status`, `serial.trace.tail` |
171
+ | **Plugins** | `serial.plugin.template`, `serial.plugin.list`, `serial.plugin.reload`, `serial.plugin.load` |
172
+
173
+ ---
174
+
175
+ ## Protocol Specs
176
+
177
+ Specs are markdown files that describe a serial device's protocol — connection settings, message format, commands, and multi-step flows. They live in `.serial_mcp/specs/` and teach the agent what the byte stream means.
178
+
179
+ Without a spec, the agent can still open a port and exchange data. With a spec, it knows what commands to send, what responses to expect, and what the data means.
180
+
181
+ You can create specs by telling the agent about your device — paste a datasheet, describe the protocol, or just let it explore and document what it finds. The agent generates the spec file, registers it, and references it in future sessions. You can also write specs by hand.
182
+
183
+ ---
184
+
185
+ ## Plugins
186
+
187
+ Plugins add device-specific shortcut tools to the server. Instead of the agent composing raw read/write sequences, a plugin provides high-level operations like `mydevice.read_temp` or `ota.upload_firmware`.
188
+
189
+ The agent can also **generate** Python plugins (with your approval). It explores a device, writes a plugin based on what it learns, and future sessions get shortcut tools — no manual coding required.
190
+
191
+ To enable plugins:
192
+
193
+ ```bash
194
+ # Enable all plugins
195
+ claude mcp add serial -e SERIAL_MCP_PLUGINS=all -- serial_mcp
196
+
197
+ # Enable specific plugins only
198
+ claude mcp add serial -e SERIAL_MCP_PLUGINS=mydevice,ota -- serial_mcp
199
+ ```
200
+
201
+ Editing an already-loaded plugin only requires `serial.plugin.reload` — no restart needed.
202
+
203
+ ---
204
+
205
+ ## Tracing
206
+
207
+ Every tool call is traced to `.serial_mcp/traces/trace.jsonl` and an in-memory ring buffer (last 2000 events). Tracing is **on by default** — set `SERIAL_MCP_TRACE=0` to disable.
208
+
209
+ ### Event format
210
+
211
+ Two events per tool call:
212
+
213
+ ```jsonl
214
+ {"ts":"2025-01-01T00:00:00.000Z","event":"tool_call_start","tool":"serial.read","args":{"connection_id":"s1"},"connection_id":"s1"}
215
+ {"ts":"2025-01-01T00:00:00.050Z","event":"tool_call_end","tool":"serial.read","ok":true,"error_code":null,"duration_ms":50,"connection_id":"s1"}
216
+ ```
217
+
218
+ - `connection_id` is extracted from args when present
219
+ - Write `data` is stripped from traced args by default (enable with `SERIAL_MCP_TRACE_PAYLOADS=1`)
220
+
221
+ ### Inspecting the trace
222
+
223
+ Use `serial.trace.status` to check config and event count, and `serial.trace.tail` to retrieve recent events — no need to read the file directly.
224
+
225
+ ---
226
+
227
+ ## PTY Mirror
228
+
229
+ When the MCP server owns a serial port, most OSes prevent any other process from opening it. PTY mirroring creates a virtual clone port that external tools (screen, minicom, logic analyzers, custom scripts) can connect to simultaneously.
230
+
231
+ ```bash
232
+ # Enable read-only mirror
233
+ claude mcp add serial \
234
+ -e SERIAL_MCP_MIRROR=ro \
235
+ -- serial_mcp
236
+
237
+ # After opening a connection, the response includes the mirror path:
238
+ # { "mirror": { "pty_path": "/dev/ttys004", "link": "/tmp/serial-mcp0", "mode": "ro" } }
239
+
240
+ # In another terminal:
241
+ screen /tmp/serial-mcp0 115200
242
+ ```
243
+
244
+ | Mode | Behavior |
245
+ |---|---|
246
+ | `off` | No mirror (default). Only the MCP server can access the port. |
247
+ | `ro` | External tools see all serial data but cannot write to the device. |
248
+ | `rw` | External tools can both see data and write to the device. |
249
+
250
+ **Platform:** macOS and Linux only. On Windows, setting `SERIAL_MCP_MIRROR` to `ro`/`rw` logs a warning and is silently ignored.
251
+
252
+ ---
253
+
254
+ ## Try without an agent
255
+
256
+ You can test the server interactively using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) — no Claude or other agent needed:
257
+
258
+ ```bash
259
+ npx @modelcontextprotocol/inspector python -m serial_mcp_server
260
+ ```
261
+
262
+ Open the URL with the auth token from the terminal output. The Inspector gives you a web UI to call any tool and see responses in real time.
263
+
264
+
265
+ ---
266
+
267
+ ## Known limitations
268
+
269
+ - **Single-client only.** The server handles one MCP session at a time (stdio transport). Multi-client transports (HTTP/SSE) may be added later.
270
+ - **Exclusive access.** Without PTY mirroring, the MCP server must own the serial port exclusively.
271
+
272
+ ---
273
+
274
+ ## Safety
275
+
276
+ This server connects an AI agent to real hardware. That's the point — and it means the stakes are higher than pure-software tools.
277
+
278
+ **Plugins execute arbitrary code.** When plugins are enabled, the agent can create and run Python code on your machine with full server privileges. Review agent-generated plugins before loading them. Use `SERIAL_MCP_PLUGINS=name1,name2` to allow only specific plugins rather than `all`.
279
+
280
+ **Writes affect real devices.** A bad command sent to a serial device can trigger unintended behavior, disrupt other connected systems, or cause hardware damage (e.g., wiping flash, entering bootloader mode, triggering actuators). Consider what the agent can reach.
281
+
282
+ **Use tool approval deliberately.** When your MCP client prompts you to approve a tool call, consider whether you want to allow it once or always. "Always allow" is convenient but means the agent can repeat that action without further confirmation.
283
+
284
+ This software is provided as-is under the MIT License. You are responsible for what the agent does with your hardware.
285
+
286
+ ---
287
+
288
+ ## License
289
+
290
+ This project is licensed under the MIT License — see [LICENSE](https://github.com/es617/serial-mcp-server/blob/main/LICENSE) for details.
291
+
292
+ ## Acknowledgements
293
+
294
+ This project is built on top of the excellent [pyserial](https://github.com/pyserial/pyserial) library for cross-platform serial communication in Python.