---
title: Network Serial Ports
description: Connecting to serial devices over TCP, RFC 2217, and other network protocols
---

import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';

Serial devices are not always physically attached to the machine running mcserial. Serial device servers, terminal servers, and networked test equipment expose serial ports over TCP. mcserial supports these through URL schemes passed to `open_serial_port` -- the same read, write, configure, and file transfer tools work regardless of how the port is connected.

All of the operations described in this guide are performed through your MCP client. Ask the assistant to open a network serial port by providing the URL scheme, and all subsequent tool calls work identically to a local port.

## Why network serial

- **Serial device servers** (Digi, Moxa, Lantronix) bridge RS-232/RS-485 ports to your LAN
- **Remote equipment** in server rooms, factory floors, or field sites
- **Terminal servers** providing shared console access to routers, switches, and PDUs
- **Virtual serial ports** for testing and development without physical hardware

## URL schemes

All URL schemes are passed directly to `open_serial_port` in the `port` parameter. Once opened, the connection is used identically to a local serial port.

### Raw TCP socket -- `socket://`

The simplest network serial option. Bytes are forwarded as-is between the TCP socket and your tools, with no protocol framing, baud rate control, or flow control signals.

### RFC 2217 -- `rfc2217://`

Full serial port emulation over a Telnet connection (RFC 2217 / Telnet COM Port Control). The remote device server translates baud rate changes, flow control, and modem line signals into commands that control the physical serial port.

<Tabs>
<TabItem label="Raw TCP">

```json
// Connect to a Digi PortServer on port 4001
// open_serial_port(
//   port="socket://192.168.1.100:4001",
//   baudrate=115200
// )
{
  "success": true,
  "port": "socket://192.168.1.100:4001",
  "mode": "rs232",
  "baudrate": 115200,
  "url_scheme": "socket",
  "hint": "Opened via URL handler. Some features (exclusive, auto-baud) are not available."
}
```

**Characteristics:**
- No baud rate negotiation -- the device server must be pre-configured to match
- No flow control or modem line signals
- Lowest overhead; good for simple byte-stream forwarding
- Works with any serial-to-ethernet bridge

</TabItem>
<TabItem label="RFC 2217">

```json
// Connect to a device server with full serial emulation
// open_serial_port(
//   port="rfc2217://192.168.1.100:2217",
//   baudrate=9600
// )
{
  "success": true,
  "port": "rfc2217://192.168.1.100:2217",
  "mode": "rs232",
  "baudrate": 9600,
  "url_scheme": "rfc2217",
  "hint": "Opened via URL handler. Some features (exclusive, auto-baud) are not available."
}
```

**Characteristics:**
- Baud rate, data bits, parity, and stop bits can be changed remotely via `configure_serial`
- Flow control (RTS/CTS, XON/XOFF) is supported and forwarded to the physical port
- Modem line states (DTR, RTS, CTS, DSR) can be read and set
- Higher overhead than raw TCP due to Telnet negotiation
- Requires a device server that implements RFC 2217

</TabItem>
</Tabs>

After opening, all tools work the same way:

```json
// write_serial(port="socket://192.168.1.100:4001", data="AT\r\n")
{
  "success": true,
  "bytes_written": 4,
  "port": "socket://192.168.1.100:4001"
}

// read_serial_lines(port="socket://192.168.1.100:4001", max_lines=5)
{
  "success": true,
  "lines": ["OK"],
  "count": 1,
  "port": "socket://192.168.1.100:4001"
}
```

## Debug wrapper -- `spy://`

<Aside type="tip" title="Debugging misbehaving devices">
Wrap any connection with `spy://` to log all transmitted and received bytes to stderr. This is invaluable when a device is not responding as expected and you need to see the raw traffic. The spy wrapper adds zero latency to the connection -- it simply logs a copy of every byte that passes through.
</Aside>

The `spy://` scheme wraps another serial connection and logs all traffic. It works with local ports and other URL schemes.

```json
// Debug a local serial port
// open_serial_port(port="spy:///dev/ttyUSB0", baudrate=9600)
{
  "success": true,
  "port": "spy:///dev/ttyUSB0",
  "baudrate": 9600,
  "url_scheme": "spy"
}

// Debug a network connection
// open_serial_port(port="spy://socket://192.168.1.100:4001", baudrate=115200)
```

All reads and writes are logged to stderr in the mcserial server process. Check the server's stderr output to see the traffic.

## HID-to-UART -- `cp2110://`

Silicon Labs CP2110 chips present as USB HID devices rather than standard serial ports. They do not appear as `/dev/ttyUSB*` devices, so normal serial access does not work.

```json
// open_serial_port(port="cp2110://", baudrate=9600)
{
  "success": true,
  "port": "cp2110://",
  "baudrate": 9600,
  "url_scheme": "cp2110"
}
```

This requires the `hidapi` package. Install it with the optional extra:

```bash
uv pip install mcserial[cp2110]
```

If `hidapi` is not installed and you try to open a `cp2110://` port, mcserial returns a clear error:

```json
{
  "error": "cp2110:// requires the hidapi package. Install with: pip install mcserial[cp2110]",
  "success": false
}
```

## Hardware grep -- `hwgrep://`

`hwgrep://` opens the first serial port whose hardware information matches a pattern. This is useful when device paths change between reboots or when multiple adapters are connected.

```json
// Find and open an FTDI device by USB VID:PID
// open_serial_port(port="hwgrep://VID:PID=0403:6001", baudrate=115200)
{
  "success": true,
  "port": "hwgrep://VID:PID=0403:6001",
  "baudrate": 115200,
  "url_scheme": "hwgrep"
}

// Find by description string
// open_serial_port(port="hwgrep://FTDI", baudrate=9600)

// Find by manufacturer
// open_serial_port(port="hwgrep://Silicon Labs", baudrate=9600)
```

The pattern is matched against the port's device path, description, hardware ID, manufacturer, product, and serial number. The first matching port is opened.

<Aside type="note">
`hwgrep://` uses pyserial's built-in hardware grep URL handler, which is separate from the `grep` parameter on `list_serial_ports`. Both search the same hardware fields, but `hwgrep://` opens the first match directly while `list_serial_ports(grep=...)` returns all matches for you to choose from.
</Aside>

## Loopback -- `loop://`

The `loop://` scheme creates a virtual loopback port where everything written is immediately available to read. No hardware is needed. This is useful for testing and development.

```json
// open_serial_port(port="loop://", baudrate=9600)
// write_serial(port="loop://", data="hello")
// read_serial(port="loop://")
{
  "success": true,
  "data": "hello",
  "bytes_read": 5,
  "port": "loop://"
}
```

## Limitations of URL-opened ports

URL-opened ports differ from local device ports in a few ways:

| Feature | Local port | URL port |
|---------|-----------|----------|
| Auto-baud detection | Yes | No (not applicable) |
| Exclusive access lock | Yes | No |
| Low-latency mode | Yes (Linux) | No |
| `set_rs485_mode` hardware ioctl | Yes | No |
| Read/write/configure | Yes | Yes |
| File transfers | Yes | Yes |

Baud rate auto-detection is skipped for URL ports because it requires reopening the port at different speeds, which does not apply to TCP or virtual connections. For `socket://`, the baud rate parameter is informational only -- the device server must be configured separately.

For `rfc2217://`, baud rate and serial parameters are sent to the remote device server, which applies them to the physical port.
