Initial MCP serial server implementation
FastMCP server for serial port access via Model Context Protocol: - 10 tools: list, open, close, read, write, configure, flush, status - 4 dynamic resources: ports list, data, status, raw hex - USB device filtering (hides phantom ttyS ports by default) - Full pyserial support with configurable baudrate/parity/etc
This commit is contained in:
commit
1a26109fc1
13
.env.example
Normal file
13
.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# MCP Serial Server Configuration
|
||||||
|
|
||||||
|
# Default baud rate for serial connections
|
||||||
|
MCSERIAL_DEFAULT_BAUDRATE=9600
|
||||||
|
|
||||||
|
# Default timeout in seconds for read operations
|
||||||
|
MCSERIAL_DEFAULT_TIMEOUT=1.0
|
||||||
|
|
||||||
|
# Maximum number of concurrent serial port connections
|
||||||
|
MCSERIAL_MAX_CONNECTIONS=10
|
||||||
|
|
||||||
|
# Log level (DEBUG, INFO, WARNING, ERROR)
|
||||||
|
MCSERIAL_LOG_LEVEL=INFO
|
||||||
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
|
||||||
|
# uv
|
||||||
|
.uv/
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# MCP Serial Server
|
||||||
|
|
||||||
|
FastMCP server for serial port access via Model Context Protocol.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Tools** for serial port control (open, close, write, configure)
|
||||||
|
- **Dynamic Resources** for reading data (`serial://{port}/data`)
|
||||||
|
- Full pyserial support (baudrate, parity, stop bits, etc.)
|
||||||
|
- Multiple concurrent port connections
|
||||||
|
- Raw byte and text modes
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With uvx (recommended)
|
||||||
|
uvx mcserial
|
||||||
|
|
||||||
|
# Or install directly
|
||||||
|
uv pip install mcserial
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage with Claude Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to Claude Code
|
||||||
|
claude mcp add mcserial "uvx mcserial"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_serial_ports` | Discover available serial ports |
|
||||||
|
| `open_serial_port` | Open a connection with config |
|
||||||
|
| `close_serial_port` | Close a connection |
|
||||||
|
| `write_serial` | Send text data |
|
||||||
|
| `write_serial_bytes` | Send raw bytes |
|
||||||
|
| `read_serial` | Read available data |
|
||||||
|
| `read_serial_line` | Read until newline |
|
||||||
|
| `configure_serial` | Change port settings |
|
||||||
|
| `flush_serial` | Clear buffers |
|
||||||
|
| `get_connection_status` | List open connections |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
| URI | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `serial://ports` | List available ports |
|
||||||
|
| `serial://{port}/data` | Read data from open port |
|
||||||
|
| `serial://{port}/status` | Port configuration info |
|
||||||
|
| `serial://{port}/raw` | Read as hex dump |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `MCSERIAL_DEFAULT_BAUDRATE` | 9600 | Default baud rate |
|
||||||
|
| `MCSERIAL_DEFAULT_TIMEOUT` | 1.0 | Read timeout (seconds) |
|
||||||
|
| `MCSERIAL_MAX_CONNECTIONS` | 10 | Max concurrent ports |
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. list_serial_ports → find /dev/ttyUSB0
|
||||||
|
2. open_serial_port(port="/dev/ttyUSB0", baudrate=115200)
|
||||||
|
3. write_serial(port="/dev/ttyUSB0", data="AT\r\n")
|
||||||
|
4. Read resource: serial:///dev/ttyUSB0/data
|
||||||
|
5. close_serial_port(port="/dev/ttyUSB0")
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
[project]
|
||||||
|
name = "mcserial"
|
||||||
|
version = "2025.01.27"
|
||||||
|
description = "MCP Serial Port Server - FastMCP server for serial port access"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [{ name = "Ryan Malloy", email = "ryan@supported.systems" }]
|
||||||
|
keywords = ["mcp", "serial", "fastmcp", "pyserial", "uart", "rs232"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Topic :: Communications",
|
||||||
|
"Topic :: System :: Hardware",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"fastmcp>=2.14.4",
|
||||||
|
"pyserial>=3.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"ruff>=0.9.0",
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mcserial = "mcserial:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/mcserial"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||||
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["mcserial"]
|
||||||
5
src/mcserial/__init__.py
Normal file
5
src/mcserial/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""MCP Serial Port Server - FastMCP server for serial port access."""
|
||||||
|
|
||||||
|
from mcserial.server import main, mcp
|
||||||
|
|
||||||
|
__all__ = ["main", "mcp"]
|
||||||
489
src/mcserial/server.py
Normal file
489
src/mcserial/server.py
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
"""FastMCP Serial Port Server - Tools and Resources for serial communication."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
# Configuration from environment
|
||||||
|
DEFAULT_BAUDRATE = int(os.getenv("MCSERIAL_DEFAULT_BAUDRATE", "9600"))
|
||||||
|
DEFAULT_TIMEOUT = float(os.getenv("MCSERIAL_DEFAULT_TIMEOUT", "1.0"))
|
||||||
|
MAX_CONNECTIONS = int(os.getenv("MCSERIAL_MAX_CONNECTIONS", "10"))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SerialConnection:
|
||||||
|
"""Tracks an open serial port connection."""
|
||||||
|
|
||||||
|
port: str
|
||||||
|
connection: serial.Serial
|
||||||
|
buffer: bytes = field(default_factory=bytes)
|
||||||
|
|
||||||
|
|
||||||
|
# Active connections registry
|
||||||
|
_connections: dict[str, SerialConnection] = {}
|
||||||
|
|
||||||
|
mcp = FastMCP(
|
||||||
|
name="mcserial",
|
||||||
|
instructions="""Serial port MCP server. Use tools to open/close/write to serial ports.
|
||||||
|
Use resources to read data from open ports (serial://{port}/data).
|
||||||
|
Always list_serial_ports first to discover available ports.""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TOOLS - Actions that modify state
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_serial_ports(usb_only: bool = True) -> list[dict]:
|
||||||
|
"""List available serial ports on the system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
usb_only: If True (default), only show USB serial devices.
|
||||||
|
Set False to include legacy/phantom ttyS ports.
|
||||||
|
|
||||||
|
Returns a list of ports with their device path, description, and hardware ID.
|
||||||
|
Call this first to discover what ports are available before opening one.
|
||||||
|
"""
|
||||||
|
ports = []
|
||||||
|
for port in serial.tools.list_ports.comports():
|
||||||
|
# Filter out phantom/legacy ports if usb_only is True
|
||||||
|
if usb_only and port.hwid == "n/a":
|
||||||
|
continue
|
||||||
|
ports.append({
|
||||||
|
"device": port.device,
|
||||||
|
"description": port.description,
|
||||||
|
"hwid": port.hwid,
|
||||||
|
"manufacturer": port.manufacturer,
|
||||||
|
"product": port.product,
|
||||||
|
"serial_number": port.serial_number,
|
||||||
|
"is_open": port.device in _connections,
|
||||||
|
})
|
||||||
|
# Sort USB ports first (ttyUSB, ttyACM), then others
|
||||||
|
ports.sort(key=lambda p: (0 if "USB" in p["device"] or "ACM" in p["device"] else 1, p["device"]))
|
||||||
|
return ports
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def open_serial_port(
|
||||||
|
port: str,
|
||||||
|
baudrate: int = DEFAULT_BAUDRATE,
|
||||||
|
bytesize: Literal[5, 6, 7, 8] = 8,
|
||||||
|
parity: Literal["N", "E", "O", "M", "S"] = "N",
|
||||||
|
stopbits: Literal[1, 1.5, 2] = 1,
|
||||||
|
timeout: float = DEFAULT_TIMEOUT,
|
||||||
|
) -> dict:
|
||||||
|
"""Open a serial port connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path (e.g., '/dev/ttyUSB0', 'COM3')
|
||||||
|
baudrate: Baud rate (default from env or 9600)
|
||||||
|
bytesize: Data bits (5, 6, 7, or 8)
|
||||||
|
parity: Parity checking (N=None, E=Even, O=Odd, M=Mark, S=Space)
|
||||||
|
stopbits: Stop bits (1, 1.5, or 2)
|
||||||
|
timeout: Read timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Connection status and details
|
||||||
|
"""
|
||||||
|
if port in _connections:
|
||||||
|
return {"error": f"Port {port} is already open", "success": False}
|
||||||
|
|
||||||
|
if len(_connections) >= MAX_CONNECTIONS:
|
||||||
|
return {"error": f"Maximum connections ({MAX_CONNECTIONS}) reached", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=bytesize,
|
||||||
|
parity=parity,
|
||||||
|
stopbits=stopbits,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
_connections[port] = SerialConnection(port=port, connection=conn)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"port": port,
|
||||||
|
"baudrate": baudrate,
|
||||||
|
"bytesize": bytesize,
|
||||||
|
"parity": parity,
|
||||||
|
"stopbits": stopbits,
|
||||||
|
"resource_uri": f"serial://{port}/data",
|
||||||
|
}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def close_serial_port(port: str) -> dict:
|
||||||
|
"""Close an open serial port connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to close
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status of the close operation
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
_connections[port].connection.close()
|
||||||
|
del _connections[port]
|
||||||
|
return {"success": True, "port": port, "message": "Port closed"}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def write_serial(port: str, data: str, encoding: str = "utf-8") -> dict:
|
||||||
|
"""Write data to an open serial port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to write to
|
||||||
|
data: String data to send
|
||||||
|
encoding: Character encoding (default: utf-8)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of bytes written
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
encoded = data.encode(encoding)
|
||||||
|
bytes_written = conn.write(encoded)
|
||||||
|
conn.flush()
|
||||||
|
return {"success": True, "bytes_written": bytes_written, "port": port}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def write_serial_bytes(port: str, data: list[int]) -> dict:
|
||||||
|
"""Write raw bytes to an open serial port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to write to
|
||||||
|
data: List of byte values (0-255)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of bytes written
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
raw_bytes = bytes(data)
|
||||||
|
bytes_written = conn.write(raw_bytes)
|
||||||
|
conn.flush()
|
||||||
|
return {"success": True, "bytes_written": bytes_written, "port": port}
|
||||||
|
except (serial.SerialException, ValueError) as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def read_serial(
|
||||||
|
port: str,
|
||||||
|
size: int | None = None,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
timeout: float | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Read data from an open serial port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to read from
|
||||||
|
size: Maximum bytes to read (None = read all available)
|
||||||
|
encoding: Character encoding for decoding (default: utf-8)
|
||||||
|
timeout: Override timeout for this read (None = use port default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data read from the port
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
|
||||||
|
# Temporarily override timeout if specified
|
||||||
|
original_timeout = conn.timeout
|
||||||
|
if timeout is not None:
|
||||||
|
conn.timeout = timeout
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = conn.read(size) if size is not None else conn.read(conn.in_waiting or 1)
|
||||||
|
finally:
|
||||||
|
if timeout is not None:
|
||||||
|
conn.timeout = original_timeout
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": raw.decode(encoding, errors="replace"),
|
||||||
|
"bytes_read": len(raw),
|
||||||
|
"raw_hex": raw.hex(),
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def read_serial_line(port: str, encoding: str = "utf-8") -> dict:
|
||||||
|
"""Read a single line (until newline) from an open serial port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to read from
|
||||||
|
encoding: Character encoding for decoding (default: utf-8)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Line of data read from the port
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
raw = conn.readline()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"line": raw.decode(encoding, errors="replace").rstrip("\r\n"),
|
||||||
|
"bytes_read": len(raw),
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def configure_serial(
|
||||||
|
port: str,
|
||||||
|
baudrate: int | None = None,
|
||||||
|
timeout: float | None = None,
|
||||||
|
rts: bool | None = None,
|
||||||
|
dtr: bool | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Configure settings on an open serial port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to configure
|
||||||
|
baudrate: New baud rate (None = no change)
|
||||||
|
timeout: New read timeout in seconds (None = no change)
|
||||||
|
rts: Set RTS line state (None = no change)
|
||||||
|
dtr: Set DTR line state (None = no change)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated port configuration
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
|
||||||
|
if baudrate is not None:
|
||||||
|
conn.baudrate = baudrate
|
||||||
|
if timeout is not None:
|
||||||
|
conn.timeout = timeout
|
||||||
|
if rts is not None:
|
||||||
|
conn.rts = rts
|
||||||
|
if dtr is not None:
|
||||||
|
conn.dtr = dtr
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"port": port,
|
||||||
|
"baudrate": conn.baudrate,
|
||||||
|
"timeout": conn.timeout,
|
||||||
|
"rts": conn.rts,
|
||||||
|
"dtr": conn.dtr,
|
||||||
|
}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_connection_status() -> dict:
|
||||||
|
"""Get status of all open serial connections.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of open connections with their settings
|
||||||
|
"""
|
||||||
|
status = {}
|
||||||
|
for port, sc in _connections.items():
|
||||||
|
conn = sc.connection
|
||||||
|
try:
|
||||||
|
status[port] = {
|
||||||
|
"is_open": conn.is_open,
|
||||||
|
"baudrate": conn.baudrate,
|
||||||
|
"bytesize": conn.bytesize,
|
||||||
|
"parity": conn.parity,
|
||||||
|
"stopbits": conn.stopbits,
|
||||||
|
"timeout": conn.timeout,
|
||||||
|
"in_waiting": conn.in_waiting,
|
||||||
|
"out_waiting": conn.out_waiting,
|
||||||
|
"cts": conn.cts,
|
||||||
|
"dsr": conn.dsr,
|
||||||
|
"rts": conn.rts,
|
||||||
|
"dtr": conn.dtr,
|
||||||
|
"resource_uri": f"serial://{port}/data",
|
||||||
|
}
|
||||||
|
except serial.SerialException:
|
||||||
|
status[port] = {"is_open": False, "error": "Port disconnected"}
|
||||||
|
return {"connections": status, "count": len(_connections)}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def flush_serial(port: str, input_buffer: bool = True, output_buffer: bool = True) -> dict:
|
||||||
|
"""Flush serial port buffers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Device path of the port to flush
|
||||||
|
input_buffer: Clear input/receive buffer
|
||||||
|
output_buffer: Clear output/transmit buffer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Flush operation status
|
||||||
|
"""
|
||||||
|
if port not in _connections:
|
||||||
|
return {"error": f"Port {port} is not open", "success": False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
if input_buffer:
|
||||||
|
conn.reset_input_buffer()
|
||||||
|
if output_buffer:
|
||||||
|
conn.reset_output_buffer()
|
||||||
|
return {"success": True, "port": port, "flushed_input": input_buffer, "flushed_output": output_buffer}
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return {"error": str(e), "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RESOURCES - Dynamic data access via URIs
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("serial://ports")
|
||||||
|
def resource_list_ports() -> str:
|
||||||
|
"""List available serial ports as a resource."""
|
||||||
|
ports = list_serial_ports()
|
||||||
|
lines = ["# Available Serial Ports\n"]
|
||||||
|
for p in ports:
|
||||||
|
status = "[OPEN]" if p["is_open"] else "[closed]"
|
||||||
|
lines.append(f"- {p['device']} {status}: {p['description']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("serial://{port}/data")
|
||||||
|
def resource_read_data(port: str) -> str:
|
||||||
|
"""Read available data from an open serial port.
|
||||||
|
|
||||||
|
The {port} parameter should be URL-encoded if it contains special characters.
|
||||||
|
For example: serial:///dev/ttyUSB0/data
|
||||||
|
"""
|
||||||
|
# Handle URL-encoded port paths
|
||||||
|
import urllib.parse
|
||||||
|
port = urllib.parse.unquote(port)
|
||||||
|
|
||||||
|
if port not in _connections:
|
||||||
|
return f"Error: Port {port} is not open. Use open_serial_port tool first."
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
available = conn.in_waiting
|
||||||
|
if available == 0:
|
||||||
|
return f"[No data available on {port}]"
|
||||||
|
|
||||||
|
raw = conn.read(available)
|
||||||
|
text = raw.decode("utf-8", errors="replace")
|
||||||
|
return f"[{len(raw)} bytes from {port}]\n{text}"
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return f"Error reading from {port}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("serial://{port}/status")
|
||||||
|
def resource_port_status(port: str) -> str:
|
||||||
|
"""Get status information for a serial port."""
|
||||||
|
import urllib.parse
|
||||||
|
port = urllib.parse.unquote(port)
|
||||||
|
|
||||||
|
if port not in _connections:
|
||||||
|
return f"Port {port} is not open."
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
lines = [
|
||||||
|
f"# Serial Port Status: {port}",
|
||||||
|
f"- Open: {conn.is_open}",
|
||||||
|
f"- Baudrate: {conn.baudrate}",
|
||||||
|
f"- Bytesize: {conn.bytesize}",
|
||||||
|
f"- Parity: {conn.parity}",
|
||||||
|
f"- Stopbits: {conn.stopbits}",
|
||||||
|
f"- Timeout: {conn.timeout}s",
|
||||||
|
f"- Bytes waiting (in): {conn.in_waiting}",
|
||||||
|
f"- Bytes waiting (out): {conn.out_waiting}",
|
||||||
|
f"- CTS: {conn.cts}",
|
||||||
|
f"- DSR: {conn.dsr}",
|
||||||
|
f"- RTS: {conn.rts}",
|
||||||
|
f"- DTR: {conn.dtr}",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return f"Error getting status for {port}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("serial://{port}/raw")
|
||||||
|
def resource_read_raw(port: str) -> str:
|
||||||
|
"""Read available data as hex dump from an open serial port."""
|
||||||
|
import urllib.parse
|
||||||
|
port = urllib.parse.unquote(port)
|
||||||
|
|
||||||
|
if port not in _connections:
|
||||||
|
return f"Error: Port {port} is not open."
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = _connections[port].connection
|
||||||
|
available = conn.in_waiting
|
||||||
|
if available == 0:
|
||||||
|
return f"[No data available on {port}]"
|
||||||
|
|
||||||
|
raw = conn.read(available)
|
||||||
|
# Format as hex dump
|
||||||
|
hex_str = " ".join(f"{b:02x}" for b in raw)
|
||||||
|
return f"[{len(raw)} bytes from {port}]\n{hex_str}"
|
||||||
|
except serial.SerialException as e:
|
||||||
|
return f"Error reading from {port}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ENTRY POINT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run the MCP serial server."""
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
package_version = version("mcserial")
|
||||||
|
except Exception:
|
||||||
|
package_version = "dev"
|
||||||
|
|
||||||
|
print(f"🔌 MCP Serial Server v{package_version}")
|
||||||
|
print(f" Default baudrate: {DEFAULT_BAUDRATE}")
|
||||||
|
print(f" Max connections: {MAX_CONNECTIONS}")
|
||||||
|
mcp.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user