Loopback Testing
pyserial provides a built-in loop:// URL scheme that creates a virtual serial port. Everything written to it is immediately available to read back — no cables, no adapters, no hardware at all. This makes it a great way to verify that mcserial is working in your MCP client, practice serial workflows, validate encodings, and run automated checks in CI/CD.
Why use loopback
Section titled “Why use loopback”- No hardware required — test serial workflows on any machine, including headless CI runners
- Deterministic — what you write is exactly what you read, making assertions straightforward
- Fast — no real baud rate delays; data transfers are instantaneous in memory
- Safe — no risk of misconfiguring or locking a real device
Basic round-trip with transact
Section titled “Basic round-trip with transact”The simplest test: send a command and read the response in a single call using transact.
-
Open the loopback port
open_serial_port(port="loop://", baudrate=9600){"success": true,"port": "loop://","mode": "rs232","baudrate": 9600,"bytesize": 8,"parity": "N","stopbits": 1,"resource_uri": "serial://loop:///data","url_scheme": "loop"}The
baudrateparameter is accepted but has no effect on loopback — data moves at memory speed regardless of the value. Setting it still validates that the parameter is handled correctly throughout the tool chain. -
Send and receive in one call
transactwrites your data, flushes, and reads back the response:transact(port="loop://", data="AT+VERSION\r\n", response_timeout=0.5){"success": true,"port": "loop://","bytes_sent": 12,"data_sent": "AT+VERSION\r\n","response": "AT+VERSION\r\n","response_bytes": 12,"response_hex": "41542b56455253494f4e0d0a"}The
response_hexfield confirms the exact bytes — useful for verifying line endings and control characters. -
Test echo stripping
Many devices echo back the command before the response.
strip_echoremoves it:transact(port="loop://", data="AT\r\n", strip_echo=true, response_timeout=0.5){"success": true,"response": "","response_bytes": 0}On a loopback port the entire response is the echo, so stripping it returns empty. On a real device that echoes
AT\r\nfollowed byOK\r\n,strip_echowould return justOK\r\n. -
Close the port
close_serial_port(port="loop://")
Default line endings
Section titled “Default line endings”Most serial devices expect a line ending after each command. Instead of appending \r\n to every call, set it once with configure_serial:
-
Open and configure
open_serial_port(port="loop://", baudrate=9600)configure_serial(port="loop://", line_ending="crlf"){"success": true,"port": "loop://","baudrate": 9600,"line_ending": "\r\n"}Now every
write_serialandtransactcall auto-appends\r\n. -
Write without manual line ending
write_serial(port="loop://", data="AT"){"success": true,"bytes_written": 4,"port": "loop://"}4 bytes:
AT(2) +\r\n(2). The line ending was appended automatically. -
Verify with read
read_serial(port="loop://"){"success": true,"data": "AT\r\n","bytes_read": 4,"raw_hex": "41540d0a"} -
Override for a single transact call
Pass
line_endingdirectly to override the port default:transact(port="loop://", data="RAW", line_ending="none", response_timeout=0.5)This sends
RAWwithout any line ending, regardless of the port default. -
Clear the default
configure_serial(port="loop://", line_ending="none") -
Close the port
close_serial_port(port="loop://")
Testing line-oriented reads
Section titled “Testing line-oriented reads”Many serial devices send responses as lines terminated with \r\n or \n. Use read_serial_line and read_serial_lines to handle these.
-
Open and write multi-line data
Ask the assistant to open a loopback port and send multi-line data:
open_serial_port(port="loop://", baudrate=115200)write_serial(port="loop://", data="OK\r\nERROR 42\r\nDONE\r\n") -
Read one line at a time
The assistant calls
read_serial_lineto get a single line:read_serial_line(port="loop://"){"success": true,"line": "OK","bytes_read": 4,"port": "loop://"}The
linefield strips trailing\r\nautomatically. Thebytes_readcount includes the line ending bytes. -
Drain remaining lines in one call
Ask the assistant to read all remaining lines:
read_serial_lines(port="loop://", max_lines=10){"success": true,"lines": ["ERROR 42", "DONE"],"count": 2,"bytes_read": 16,"port": "loop://"}read_serial_linesstops early when no more data is available, so settingmax_lineshigher than expected is fine. -
Close the port
close_serial_port(port="loop://")
Testing custom terminators
Section titled “Testing custom terminators”Not every protocol uses newlines. Some devices use > as a prompt, \x00 as a null terminator, or multi-byte sequences. Use read_until for these.
-
Open and write prompt-terminated data
Ask the assistant to open a loopback port and write data with a
>prompt at the end:open_serial_port(port="loop://", baudrate=9600)write_serial(port="loop://", data="ELM327 v1.5>") -
Read until the
>promptThe assistant calls
read_untilwith a custom terminator:read_until(port="loop://", terminator=">"){"success": true,"data": "ELM327 v1.5>","bytes_read": 12,"raw_hex": "454c4d33323720312e353e","port": "loop://","terminator_found": true}The
terminator_foundfield confirms whether the terminator was actually received or whether the read timed out first. -
Close the port
close_serial_port(port="loop://")
Testing encodings
Section titled “Testing encodings”Serial devices don’t always speak UTF-8. Industrial equipment, legacy systems, and some sensors use Latin-1 or other single-byte encodings. mcserial’s encoding parameter handles the translation. Tell the assistant which encoding to use, and it passes the value through to the read/write calls.
-
Open the port
open_serial_port(port="loop://", baudrate=9600) -
Write with Latin-1 encoding
Latin-1 maps bytes 0x00—0xFF directly, which makes it useful for binary-safe text. The assistant calls:
write_serial(port="loop://", data="Temp: 23\xb0C", encoding="latin-1")The
\xb0byte is the degree symbol in Latin-1. -
Read with matching encoding
read_serial(port="loop://", encoding="latin-1"){"success": true,"data": "Temp: 23\u00b0C","bytes_read": 10,"raw_hex": "54656d703a203233b043","port": "loop://"}The
b0byte inraw_hexconfirms the degree symbol was sent correctly. -
Close the port
close_serial_port(port="loop://")
Testing raw byte writes
Section titled “Testing raw byte writes”For binary protocols (Modbus RTU, custom framing, firmware uploads), the assistant uses write_serial_bytes to send exact byte sequences:
-
Open the port
open_serial_port(port="loop://", baudrate=9600) -
Write raw bytes
Send a Modbus-style query (address 0x01, function 0x03, register 0x0000, count 0x0001):
write_serial_bytes(port="loop://", data=[1, 3, 0, 0, 0, 1]){"success": true,"bytes_written": 6,"port": "loop://"} -
Read back and verify hex
read_serial(port="loop://", encoding="latin-1"){"success": true,"data": "\u0001\u0003\u0000\u0000\u0000\u0001","bytes_read": 6,"raw_hex": "010300000001","port": "loop://"}The
raw_hexfield is the most reliable way to verify binary data — it shows the exact bytes without encoding ambiguity. -
Close the port
close_serial_port(port="loop://")
Testing port configuration changes
Section titled “Testing port configuration changes”The assistant can reconfigure an open port without closing and reopening it. This is useful for testing parameter changes or simulating baud rate switching:
-
Open at 9600 baud
open_serial_port(port="loop://", baudrate=9600) -
Change to 115200 baud with hardware flow control
configure_serial(port="loop://", baudrate=115200, rtscts=true){"success": true,"port": "loop://","baudrate": 115200,"timeout": 1.0,"write_timeout": null,"inter_byte_timeout": null,"xonxoff": false,"rtscts": true,"dsrdtr": false,"rts": true,"dtr": true}The response shows the full port state after the change, making it easy to confirm the new settings.
-
Verify the connection status
get_connection_status()This returns all open connections with their current settings, including the updated baud rate and flow control flags.
-
Close the port
close_serial_port(port="loop://")
Testing buffer management
Section titled “Testing buffer management”When working with protocol handlers, you sometimes need to clear stale data before sending a new command. Ask the assistant to flush the buffers:
flush_serial(port="loop://", input_buffer=true, output_buffer=true)This discards any unread data in the input buffer and any unsent data in the output buffer. Call it before sending a command when you want a clean slate.
Using loopback in CI/CD
Section titled “Using loopback in CI/CD”Since loop:// requires no hardware or special permissions, it works in any CI environment. A typical test pattern:
- Start mcserial as a subprocess
- Connect via MCP client
- Open
loop:// - Write known data, read it back, assert equality
- Test edge cases: empty writes, large payloads, encoding boundaries
- Close the port
This validates that your MCP tool chain works end-to-end without needing a physical device in the test runner.