Add cp210x-mcp FastMCP server for device customization

MCP Tools:
- list_devices: List connected CP210x devices
- get_device_info: Full device details (part number, VID/PID, strings)
- set_product_string: Change USB device name (max 126 chars)
- set_manufacturer_string: Change manufacturer (max 45 chars)
- set_serial_number: Change serial number (max 63 chars)
- set_max_power: Set USB power draw (0-500mA)
- set_self_powered: Toggle self/bus powered
- reset_device: USB re-enumeration
- lock_device: PERMANENTLY lock configuration

Requires libcp210xmanufacturing.so (see aur/cp210xmanufacturing)

Usage:
  claude mcp add cp210x -- uvx cp210x-mcp
This commit is contained in:
Ryan Malloy 2026-01-30 10:37:08 -07:00
parent 7c04e0cb6a
commit fc12d6a2d7
6 changed files with 2758 additions and 0 deletions

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# CP210x MCP Server
MCP server for customizing Silicon Labs CP210x USB-UART bridge devices. Allows reading and writing USB descriptor strings, power configuration, and more.
## Features
- List connected CP210x devices
- Read/write USB product string (device name)
- Read/write manufacturer string
- Read/write serial number
- Configure max power draw
- Set self-powered/bus-powered mode
- Reset device (USB re-enumeration)
- Lock device (permanent - prevents further changes)
## Requirements
- Linux x86_64
- `libcp210xmanufacturing.so` - Install via AUR package or build from source
## Installation
### Install the library (Arch Linux)
```bash
cd aur/cp210xmanufacturing
makepkg -si
```
Or build from source:
```bash
cd AN721SW/Linux/LibrarySourcePackages/cp210xmanufacturing
make LIB_ARCH=64
sudo make install
sudo ldconfig
```
### Install the MCP server
```bash
# With uv (recommended)
uv tool install .
# Or with pip
pip install .
```
## Usage
### Add to Claude Code
```bash
claude mcp add cp210x -- uvx cp210x-mcp
```
### Available Tools
| Tool | Description |
|------|-------------|
| `list_devices` | List connected CP210x devices |
| `get_device_info` | Get detailed device information |
| `set_product_string` | Set USB product string (device name) |
| `set_manufacturer_string` | Set USB manufacturer string |
| `set_serial_number` | Set USB serial number |
| `set_max_power` | Set max USB power draw (mA) |
| `set_self_powered` | Set self-powered vs bus-powered |
| `reset_device` | Reset device (USB re-enumeration) |
| `lock_device` | PERMANENTLY lock device config |
### Example
```bash
# In Claude Code conversation:
> What CP210x devices are connected?
> Change the product name of device 0 to "My Custom Device"
```
## Relationship to mcserial
This MCP server complements [mcserial](https://github.com/ryanmalloy/mcserial) which handles serial port communication. Use this server for **device customization** (changing USB descriptors) and mcserial for **serial communication** (sending/receiving data over UART).
## License
MIT

53
pyproject.toml Normal file
View File

@ -0,0 +1,53 @@
[project]
name = "cp210x-mcp"
version = "0.1.0"
description = "MCP server for CP210x USB-UART bridge customization"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{name = "Ryan Malloy", email = "ryan@supported.systems"}
]
keywords = ["mcp", "cp210x", "usb", "uart", "serial", "silicon-labs"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: System :: Hardware :: Hardware Drivers",
]
dependencies = [
"fastmcp>=2.0.0",
]
[project.optional-dependencies]
dev = [
"ruff",
"pytest",
]
[project.scripts]
cp210x-mcp = "cp210x_mcp:main"
[project.urls]
Homepage = "https://github.com/ryanmalloy/cp210x-mcp"
Repository = "https://github.com/ryanmalloy/cp210x-mcp"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/cp210x_mcp"]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
ignore = ["E501"]

View File

@ -0,0 +1,5 @@
"""CP210x MCP Server - Device customization for Silicon Labs USB-UART bridges."""
from .server import mcp, main
__all__ = ["mcp", "main"]

441
src/cp210x_mcp/bindings.py Normal file
View File

@ -0,0 +1,441 @@
"""Low-level ctypes bindings for libcp210xmanufacturing."""
import ctypes
from ctypes import c_int, c_uint, c_ushort, c_ubyte, c_char_p, c_void_p, POINTER, byref
from pathlib import Path
from typing import Optional
# Type aliases matching the C library
CP210x_STATUS = c_int
HANDLE = c_void_p
DWORD = c_uint
WORD = c_ushort
BYTE = c_ubyte
BOOL = c_int
# Return codes
CP210x_SUCCESS = 0x00
CP210x_DEVICE_NOT_FOUND = 0xFF
CP210x_INVALID_HANDLE = 0x01
CP210x_INVALID_PARAMETER = 0x02
CP210x_DEVICE_IO_FAILED = 0x03
CP210x_FUNCTION_NOT_SUPPORTED = 0x04
CP210x_GLOBAL_DATA_ERROR = 0x05
CP210x_FILE_ERROR = 0x06
CP210x_COMMAND_FAILED = 0x08
CP210x_INVALID_ACCESS_TYPE = 0x09
# Part numbers
PART_NUMBERS = {
0x01: "CP2101",
0x02: "CP2102",
0x03: "CP2103",
0x04: "CP2104",
0x05: "CP2105",
0x08: "CP2108",
0x09: "CP2109",
0x20: "CP2102N-QFN28",
0x21: "CP2102N-QFN24",
0x22: "CP2102N-QFN20",
}
# Buffer sizes
MAX_DEVICE_STRLEN = 256
MAX_MANUFACTURER_STRLEN = 45
MAX_PRODUCT_STRLEN = 126
MAX_SERIAL_STRLEN = 63
# GetProductString flags
RETURN_SERIAL_NUMBER = 0x00
RETURN_DESCRIPTION = 0x01
RETURN_FULL_PATH = 0x02
class CP210xError(Exception):
"""Exception raised for CP210x library errors."""
ERROR_MESSAGES = {
CP210x_DEVICE_NOT_FOUND: "Device not found",
CP210x_INVALID_HANDLE: "Invalid handle",
CP210x_INVALID_PARAMETER: "Invalid parameter",
CP210x_DEVICE_IO_FAILED: "Device I/O failed",
CP210x_FUNCTION_NOT_SUPPORTED: "Function not supported",
CP210x_GLOBAL_DATA_ERROR: "Global data error",
CP210x_FILE_ERROR: "File error",
CP210x_COMMAND_FAILED: "Command failed",
CP210x_INVALID_ACCESS_TYPE: "Invalid access type",
}
def __init__(self, status: int, context: str = ""):
self.status = status
msg = self.ERROR_MESSAGES.get(status, f"Unknown error 0x{status:02X}")
if context:
msg = f"{context}: {msg}"
super().__init__(msg)
class CP210xLibrary:
"""Wrapper for the CP210x manufacturing library."""
def __init__(self, lib_path: Optional[str] = None):
"""Load the CP210x manufacturing library.
Args:
lib_path: Path to libcp210xmanufacturing.so, or None to search default paths.
"""
if lib_path:
self._lib = ctypes.CDLL(lib_path)
else:
# Search common locations
search_paths = [
"/usr/lib/libcp210xmanufacturing.so",
"/usr/local/lib/libcp210xmanufacturing.so",
str(Path(__file__).parent.parent.parent / "AN721SW/Linux/LibrarySourcePackages/cp210xmanufacturing/build/lib/x86_64/libcp210xmanufacturing.so"),
]
for path in search_paths:
if Path(path).exists():
self._lib = ctypes.CDLL(path)
break
else:
raise FileNotFoundError(
"libcp210xmanufacturing.so not found. Install with: "
"cd aur/cp210xmanufacturing && makepkg -si"
)
self._setup_functions()
def _setup_functions(self):
"""Set up function prototypes."""
# CP210x_GetNumDevices
self._lib.CP210x_GetNumDevices.argtypes = [POINTER(DWORD)]
self._lib.CP210x_GetNumDevices.restype = CP210x_STATUS
# CP210x_GetProductString
self._lib.CP210x_GetProductString.argtypes = [DWORD, c_void_p, DWORD]
self._lib.CP210x_GetProductString.restype = CP210x_STATUS
# CP210x_Open
self._lib.CP210x_Open.argtypes = [DWORD, POINTER(HANDLE)]
self._lib.CP210x_Open.restype = CP210x_STATUS
# CP210x_Close
self._lib.CP210x_Close.argtypes = [HANDLE]
self._lib.CP210x_Close.restype = CP210x_STATUS
# CP210x_GetPartNumber
self._lib.CP210x_GetPartNumber.argtypes = [HANDLE, POINTER(BYTE)]
self._lib.CP210x_GetPartNumber.restype = CP210x_STATUS
# CP210x_GetDeviceVid
self._lib.CP210x_GetDeviceVid.argtypes = [HANDLE, POINTER(WORD)]
self._lib.CP210x_GetDeviceVid.restype = CP210x_STATUS
# CP210x_GetDevicePid
self._lib.CP210x_GetDevicePid.argtypes = [HANDLE, POINTER(WORD)]
self._lib.CP210x_GetDevicePid.restype = CP210x_STATUS
# CP210x_GetDeviceManufacturerString
self._lib.CP210x_GetDeviceManufacturerString.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
self._lib.CP210x_GetDeviceManufacturerString.restype = CP210x_STATUS
# CP210x_GetDeviceProductString
self._lib.CP210x_GetDeviceProductString.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
self._lib.CP210x_GetDeviceProductString.restype = CP210x_STATUS
# CP210x_GetDeviceSerialNumber
self._lib.CP210x_GetDeviceSerialNumber.argtypes = [HANDLE, c_void_p, POINTER(BYTE), BOOL]
self._lib.CP210x_GetDeviceSerialNumber.restype = CP210x_STATUS
# CP210x_SetManufacturerString
self._lib.CP210x_SetManufacturerString.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
self._lib.CP210x_SetManufacturerString.restype = CP210x_STATUS
# CP210x_SetProductString
self._lib.CP210x_SetProductString.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
self._lib.CP210x_SetProductString.restype = CP210x_STATUS
# CP210x_SetSerialNumber
self._lib.CP210x_SetSerialNumber.argtypes = [HANDLE, c_void_p, BYTE, BOOL]
self._lib.CP210x_SetSerialNumber.restype = CP210x_STATUS
# CP210x_GetMaxPower
self._lib.CP210x_GetMaxPower.argtypes = [HANDLE, POINTER(BYTE)]
self._lib.CP210x_GetMaxPower.restype = CP210x_STATUS
# CP210x_SetMaxPower
self._lib.CP210x_SetMaxPower.argtypes = [HANDLE, BYTE]
self._lib.CP210x_SetMaxPower.restype = CP210x_STATUS
# CP210x_GetSelfPower
self._lib.CP210x_GetSelfPower.argtypes = [HANDLE, POINTER(BOOL)]
self._lib.CP210x_GetSelfPower.restype = CP210x_STATUS
# CP210x_SetSelfPower
self._lib.CP210x_SetSelfPower.argtypes = [HANDLE, BOOL]
self._lib.CP210x_SetSelfPower.restype = CP210x_STATUS
# CP210x_GetDeviceVersion
self._lib.CP210x_GetDeviceVersion.argtypes = [HANDLE, POINTER(WORD)]
self._lib.CP210x_GetDeviceVersion.restype = CP210x_STATUS
# CP210x_SetDeviceVersion
self._lib.CP210x_SetDeviceVersion.argtypes = [HANDLE, WORD]
self._lib.CP210x_SetDeviceVersion.restype = CP210x_STATUS
# CP210x_GetLockValue
self._lib.CP210x_GetLockValue.argtypes = [HANDLE, POINTER(BYTE)]
self._lib.CP210x_GetLockValue.restype = CP210x_STATUS
# CP210x_SetLockValue
self._lib.CP210x_SetLockValue.argtypes = [HANDLE]
self._lib.CP210x_SetLockValue.restype = CP210x_STATUS
# CP210x_Reset
self._lib.CP210x_Reset.argtypes = [HANDLE]
self._lib.CP210x_Reset.restype = CP210x_STATUS
def _check_status(self, status: int, context: str = ""):
"""Raise exception if status indicates error."""
if status != CP210x_SUCCESS:
raise CP210xError(status, context)
def get_num_devices(self) -> int:
"""Get the number of connected CP210x devices."""
num = DWORD()
status = self._lib.CP210x_GetNumDevices(byref(num))
self._check_status(status, "GetNumDevices")
return num.value
def get_product_string(self, device_index: int, flag: int = RETURN_DESCRIPTION) -> str:
"""Get device string without opening the device.
Args:
device_index: Zero-based device index
flag: What to return - RETURN_SERIAL_NUMBER, RETURN_DESCRIPTION, or RETURN_FULL_PATH
"""
buf = ctypes.create_string_buffer(MAX_DEVICE_STRLEN)
status = self._lib.CP210x_GetProductString(DWORD(device_index), buf, DWORD(flag))
self._check_status(status, "GetProductString")
return buf.value.decode('utf-8', errors='replace')
def open(self, device_index: int) -> HANDLE:
"""Open a device by index."""
handle = HANDLE()
status = self._lib.CP210x_Open(DWORD(device_index), byref(handle))
self._check_status(status, f"Open device {device_index}")
return handle
def close(self, handle: HANDLE):
"""Close an open device."""
status = self._lib.CP210x_Close(handle)
self._check_status(status, "Close")
def get_part_number(self, handle: HANDLE) -> tuple[int, str]:
"""Get the part number of an open device.
Returns:
Tuple of (part_number_code, part_name)
"""
part = BYTE()
status = self._lib.CP210x_GetPartNumber(handle, byref(part))
self._check_status(status, "GetPartNumber")
return part.value, PART_NUMBERS.get(part.value, f"Unknown (0x{part.value:02X})")
def get_device_vid(self, handle: HANDLE) -> int:
"""Get the USB Vendor ID."""
vid = WORD()
status = self._lib.CP210x_GetDeviceVid(handle, byref(vid))
self._check_status(status, "GetDeviceVid")
return vid.value
def get_device_pid(self, handle: HANDLE) -> int:
"""Get the USB Product ID."""
pid = WORD()
status = self._lib.CP210x_GetDevicePid(handle, byref(pid))
self._check_status(status, "GetDevicePid")
return pid.value
def get_manufacturer_string(self, handle: HANDLE) -> str:
"""Get the manufacturer string."""
buf = ctypes.create_string_buffer(MAX_MANUFACTURER_STRLEN)
length = BYTE()
status = self._lib.CP210x_GetDeviceManufacturerString(handle, buf, byref(length), BOOL(1))
self._check_status(status, "GetDeviceManufacturerString")
return buf.value.decode('utf-8', errors='replace')
def get_product_string_device(self, handle: HANDLE) -> str:
"""Get the product string from an open device."""
buf = ctypes.create_string_buffer(MAX_PRODUCT_STRLEN)
length = BYTE()
status = self._lib.CP210x_GetDeviceProductString(handle, buf, byref(length), BOOL(1))
self._check_status(status, "GetDeviceProductString")
return buf.value.decode('utf-8', errors='replace')
def get_serial_number(self, handle: HANDLE) -> str:
"""Get the serial number."""
buf = ctypes.create_string_buffer(MAX_SERIAL_STRLEN)
length = BYTE()
status = self._lib.CP210x_GetDeviceSerialNumber(handle, buf, byref(length), BOOL(1))
self._check_status(status, "GetDeviceSerialNumber")
return buf.value.decode('utf-8', errors='replace')
def set_manufacturer_string(self, handle: HANDLE, manufacturer: str):
"""Set the manufacturer string (max 45 chars)."""
data = manufacturer.encode('utf-8')[:MAX_MANUFACTURER_STRLEN]
status = self._lib.CP210x_SetManufacturerString(handle, data, BYTE(len(data)), BOOL(1))
self._check_status(status, "SetManufacturerString")
def set_product_string(self, handle: HANDLE, product: str):
"""Set the product string (max 126 chars)."""
data = product.encode('utf-8')[:MAX_PRODUCT_STRLEN]
status = self._lib.CP210x_SetProductString(handle, data, BYTE(len(data)), BOOL(1))
self._check_status(status, "SetProductString")
def set_serial_number(self, handle: HANDLE, serial: str):
"""Set the serial number (max 63 chars)."""
data = serial.encode('utf-8')[:MAX_SERIAL_STRLEN]
status = self._lib.CP210x_SetSerialNumber(handle, data, BYTE(len(data)), BOOL(1))
self._check_status(status, "SetSerialNumber")
def get_max_power(self, handle: HANDLE) -> int:
"""Get max power in units of 2mA (multiply by 2 for mA)."""
power = BYTE()
status = self._lib.CP210x_GetMaxPower(handle, byref(power))
self._check_status(status, "GetMaxPower")
return power.value
def set_max_power(self, handle: HANDLE, power_2ma: int):
"""Set max power in units of 2mA (e.g., 50 = 100mA)."""
if power_2ma > 250:
raise ValueError("Max power cannot exceed 250 (500mA)")
status = self._lib.CP210x_SetMaxPower(handle, BYTE(power_2ma))
self._check_status(status, "SetMaxPower")
def get_self_power(self, handle: HANDLE) -> bool:
"""Check if device is self-powered."""
self_power = BOOL()
status = self._lib.CP210x_GetSelfPower(handle, byref(self_power))
self._check_status(status, "GetSelfPower")
return bool(self_power.value)
def set_self_power(self, handle: HANDLE, self_powered: bool):
"""Set whether device is self-powered."""
status = self._lib.CP210x_SetSelfPower(handle, BOOL(1 if self_powered else 0))
self._check_status(status, "SetSelfPower")
def get_device_version(self, handle: HANDLE) -> int:
"""Get device version (bcdDevice)."""
version = WORD()
status = self._lib.CP210x_GetDeviceVersion(handle, byref(version))
self._check_status(status, "GetDeviceVersion")
return version.value
def set_device_version(self, handle: HANDLE, version: int):
"""Set device version (bcdDevice)."""
status = self._lib.CP210x_SetDeviceVersion(handle, WORD(version))
self._check_status(status, "SetDeviceVersion")
def get_lock_value(self, handle: HANDLE) -> int:
"""Get lock value (0 = unlocked, 1-255 = locked)."""
lock = BYTE()
status = self._lib.CP210x_GetLockValue(handle, byref(lock))
self._check_status(status, "GetLockValue")
return lock.value
def lock_device(self, handle: HANDLE):
"""Lock the device (PERMANENT - cannot be undone!)."""
status = self._lib.CP210x_SetLockValue(handle)
self._check_status(status, "SetLockValue")
def reset(self, handle: HANDLE):
"""Reset the device (USB disconnect/reconnect)."""
status = self._lib.CP210x_Reset(handle)
self._check_status(status, "Reset")
# Convenience context manager for device access
class CP210xDevice:
"""Context manager for safe device access."""
def __init__(self, lib: CP210xLibrary, device_index: int):
self.lib = lib
self.device_index = device_index
self.handle = None
def __enter__(self):
self.handle = self.lib.open(self.device_index)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.handle:
self.lib.close(self.handle)
self.handle = None
return False
@property
def part_number(self) -> tuple[int, str]:
return self.lib.get_part_number(self.handle)
@property
def vid(self) -> int:
return self.lib.get_device_vid(self.handle)
@property
def pid(self) -> int:
return self.lib.get_device_pid(self.handle)
@property
def manufacturer(self) -> str:
return self.lib.get_manufacturer_string(self.handle)
@manufacturer.setter
def manufacturer(self, value: str):
self.lib.set_manufacturer_string(self.handle, value)
@property
def product(self) -> str:
return self.lib.get_product_string_device(self.handle)
@product.setter
def product(self, value: str):
self.lib.set_product_string(self.handle, value)
@property
def serial_number(self) -> str:
return self.lib.get_serial_number(self.handle)
@serial_number.setter
def serial_number(self, value: str):
self.lib.set_serial_number(self.handle, value)
@property
def max_power_ma(self) -> int:
"""Max power in milliamps."""
return self.lib.get_max_power(self.handle) * 2
@max_power_ma.setter
def max_power_ma(self, value: int):
"""Set max power in milliamps (will be rounded down to nearest 2mA)."""
self.lib.set_max_power(self.handle, value // 2)
@property
def self_powered(self) -> bool:
return self.lib.get_self_power(self.handle)
@self_powered.setter
def self_powered(self, value: bool):
self.lib.set_self_power(self.handle, value)
@property
def device_version(self) -> str:
"""Device version as BCD string (e.g., '1.00')."""
v = self.lib.get_device_version(self.handle)
return f"{(v >> 8) & 0xFF}.{v & 0xFF:02d}"
@property
def is_locked(self) -> bool:
return self.lib.get_lock_value(self.handle) != 0
def reset(self):
"""Reset the device."""
self.lib.reset(self.handle)

290
src/cp210x_mcp/server.py Normal file
View File

@ -0,0 +1,290 @@
"""FastMCP server for CP210x device customization."""
from typing import Optional
from fastmcp import FastMCP
from .bindings import CP210xLibrary, CP210xDevice, CP210xError, PART_NUMBERS
mcp = FastMCP(
"cp210x",
instructions="CP210x USB-UART bridge customization - read/write product strings, serial numbers, and device configuration",
)
# Lazy-loaded library instance
_lib: Optional[CP210xLibrary] = None
def get_lib() -> CP210xLibrary:
"""Get or create the library instance."""
global _lib
if _lib is None:
_lib = CP210xLibrary()
return _lib
@mcp.tool()
def list_devices() -> list[dict]:
"""List all connected CP210x devices.
Returns a list of devices with their index, description, and serial number.
Use the index to reference a specific device in other tools.
"""
lib = get_lib()
num_devices = lib.get_num_devices()
devices = []
for i in range(num_devices):
try:
desc = lib.get_product_string(i, flag=1) # RETURN_DESCRIPTION
serial = lib.get_product_string(i, flag=0) # RETURN_SERIAL_NUMBER
devices.append({
"index": i,
"description": desc,
"serial_number": serial,
})
except CP210xError as e:
devices.append({
"index": i,
"error": str(e),
})
return devices
@mcp.tool()
def get_device_info(device_index: int = 0) -> dict:
"""Get detailed information about a CP210x device.
Args:
device_index: Zero-based index of the device (default: 0 for first device)
Returns full device details including part number, VID/PID, strings, and power settings.
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
part_code, part_name = dev.part_number
return {
"part_number": part_name,
"part_code": f"0x{part_code:02X}",
"vid": f"0x{dev.vid:04X}",
"pid": f"0x{dev.pid:04X}",
"manufacturer": dev.manufacturer,
"product": dev.product,
"serial_number": dev.serial_number,
"device_version": dev.device_version,
"max_power_ma": dev.max_power_ma,
"self_powered": dev.self_powered,
"is_locked": dev.is_locked,
}
@mcp.tool()
def set_product_string(product: str, device_index: int = 0) -> dict:
"""Set the USB product string (device name shown to host).
Args:
product: New product string (max 126 characters)
device_index: Zero-based device index (default: 0)
Returns the updated device info. Device may need to be re-plugged for
changes to appear on the USB host.
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is locked and cannot be modified"}
old_product = dev.product
dev.product = product
return {
"success": True,
"old_value": old_product,
"new_value": product[:126],
"note": "Re-plug device for changes to take effect on USB host",
}
@mcp.tool()
def set_manufacturer_string(manufacturer: str, device_index: int = 0) -> dict:
"""Set the USB manufacturer string.
Args:
manufacturer: New manufacturer string (max 45 characters)
device_index: Zero-based device index (default: 0)
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is locked and cannot be modified"}
old_mfr = dev.manufacturer
dev.manufacturer = manufacturer
return {
"success": True,
"old_value": old_mfr,
"new_value": manufacturer[:45],
"note": "Re-plug device for changes to take effect on USB host",
}
@mcp.tool()
def set_serial_number(serial_number: str, device_index: int = 0) -> dict:
"""Set the USB serial number.
Args:
serial_number: New serial number (max 63 characters)
device_index: Zero-based device index (default: 0)
Note: Changing serial number may affect udev rules that match on serial.
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is locked and cannot be modified"}
old_serial = dev.serial_number
dev.serial_number = serial_number
return {
"success": True,
"old_value": old_serial,
"new_value": serial_number[:63],
"note": "Re-plug device for changes to take effect on USB host",
}
@mcp.tool()
def set_max_power(max_power_ma: int, device_index: int = 0) -> dict:
"""Set the maximum USB power draw in milliamps.
Args:
max_power_ma: Max power in mA (0-500, will be rounded to nearest 2mA)
device_index: Zero-based device index (default: 0)
"""
lib = get_lib()
if max_power_ma < 0 or max_power_ma > 500:
return {"error": "max_power_ma must be 0-500"}
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is locked and cannot be modified"}
old_power = dev.max_power_ma
dev.max_power_ma = max_power_ma
return {
"success": True,
"old_value_ma": old_power,
"new_value_ma": (max_power_ma // 2) * 2, # Actual value after rounding
}
@mcp.tool()
def set_self_powered(self_powered: bool, device_index: int = 0) -> dict:
"""Set whether device reports as self-powered or bus-powered.
Args:
self_powered: True for self-powered, False for bus-powered
device_index: Zero-based device index (default: 0)
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is locked and cannot be modified"}
old_value = dev.self_powered
dev.self_powered = self_powered
return {
"success": True,
"old_value": old_value,
"new_value": self_powered,
}
@mcp.tool()
def reset_device(device_index: int = 0) -> dict:
"""Reset the CP210x device (USB disconnect/reconnect).
Args:
device_index: Zero-based device index (default: 0)
This triggers a USB re-enumeration, which will make any programmed
changes visible to the USB host.
"""
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
dev.reset()
return {
"success": True,
"note": "Device has been reset. It may take a moment to re-enumerate.",
}
@mcp.tool()
def lock_device(device_index: int = 0, confirm: bool = False) -> dict:
"""PERMANENTLY lock the device to prevent further customization.
Args:
device_index: Zero-based device index (default: 0)
confirm: Must be True to actually lock (safety check)
WARNING: This is PERMANENT and IRREVERSIBLE! Once locked, the device's
configuration cannot be changed. The device will still function normally,
but strings, power settings, etc. cannot be modified.
"""
if not confirm:
return {
"error": "Safety check failed",
"message": "Set confirm=True to actually lock the device. THIS IS PERMANENT!",
}
lib = get_lib()
with CP210xDevice(lib, device_index) as dev:
if dev.is_locked:
return {"error": "Device is already locked"}
lib.lock_device(dev.handle)
return {
"success": True,
"warning": "Device is now PERMANENTLY locked. Configuration cannot be changed.",
}
def main():
"""Entry point for the MCP server."""
try:
from importlib.metadata import version
package_version = version("cp210x-mcp")
except Exception:
package_version = "0.1.0"
print(f"🔌 CP210x MCP Server v{package_version}")
try:
lib = get_lib()
num_devices = lib.get_num_devices()
print(f" Found {num_devices} CP210x device(s)")
except FileNotFoundError as e:
print(f"⚠️ Library not found: {e}")
print(" Server will start but tools will fail until library is installed.")
except Exception as e:
print(f"⚠️ Error initializing: {e}")
mcp.run()
if __name__ == "__main__":
main()

1884
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff