Serencp

by abda11ah

Not rated
GitHub

About

VM serial console viewer Browse Serencp MCP Server for Claude, Cursor, VS Code, and other AI agents.

Details

Author
abda11ah
Categories
Developer Tools, Other, Infrastructure

Setup

Install Serencp in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/abda11ah/serencp

Follow the installation instructions in the repository README, then restart your MCP client.

(tested with QEMU/KVM/virt-manager and OpenCode)

Theserencp.plscript provides a standard MCP (Model Context Protocol) server for bidirectional communication with VM serial consoles via an internal Perl-based socket bridge.

It usesIO::Ptyto create a pseudo-terminal (PTY) for the VM serial console. It manages multiple VMs by assigning unique TCP ports for communication and provides a high-performance multiplexed event loop.

- Operating System: Strictly requires a *nix-like system (Linux, macOS, BSD, etc.). Windows is NOT supported (unless via WSL).
- Perl Modules: The following non-core modules are required:

- IO::Pty- Pseudo-terminal creation

- --socket <path>: Run in Unix socket client mode (connect to existing bridge) (don't care about it unless in non-MCP environement)
- --terminal <name>: Specify preferred terminal emulator to use (bypasses auto-detection)

The script automatically detects and supports the following terminal emulators (tested for availability at runtime):

- Ghostty.app(GPU-accelerated)
- WezTerm.app
- iTerm.app/iTerm2.app
- Terminal.app(built-in macOS terminal)

- wezterm(cross-platform)
- kitty(GPU-accelerated, tested)
- alacritty(GPU-accelerated)
- ghostty(modern GPU-accelerated)
- foot(Wayland terminal)

- konsole(KDE)
- gnome-terminal(GNOME)
- tilix(GTK3 tiling terminal)
- terminator(advanced tiling terminal)
- xfce4-terminal(XFCE desktop)

- xterm(classic X11 terminal)
- urxvt(Unicode rxvt)

The script uses a priority-based detection system:
- First honors user's explicit--terminaloption
- Then checksTERM_PROGRAMenvironment variable (macOS/VSCode/Warp/Hyper)
- Then checksTERMINALenvironment variable
- Finally tries terminals in priority order until one works

The detection now performs atest launchto verify the terminal can actually spawn before selecting it, ensuring more reliable terminal spawning. If no terminal is detected, it provides fallback mechanisms and error notifications. It always prioritize best graphical terminal emulators.

- Default VM Port: 4555
- Ring Buffer Size: 1000 lines
- Max Buffer Bytes: 10MB per VM
- Console History Lines: 60 lines (sent to new clients)
- Read Timeout: 2 seconds (internal, for legacyreadtool)
- Restart Backoff: Initial 1s, Maximum 60s (exponential backoff)
- Write Buffer: 1MB max per destination
- Protocol Version: 2025-06-18

Make sure the MCP server is configured inopencode.jsonc:

"mcp": { "serencp": { "type": "local", "command": ["perl", "/path/to/serencp.pl"], "enabled": true } }

You can also specify a terminal explicitly:

"mcp": { "serencp": { "type": "local", "command": ["perl", "/path/to/serencp.pl","--terminal","wezterm"], "enabled": true } }

HTTP Experimental Server (http_experimental.pl)

http_experimental.plis an experimentalStreamable HTTPMCP server (v2.0) exposing the same 5 tools (start,stop,status,read,write) over HTTP instead of stdio. It listens by default onhttp://127.0.0.1:8080/mcp, uses Server-Sent Events (SSE) to stream live VM output notifications (but still need 'read' tool to get after each call to 'write' tool (current limitation, needs more investigation)), and supports session-based resource subscriptions with CORS enabled. Launch it with:

Then register it as aremoteMCP server:

Opencode example (~/.opencode/opencode.jsonc)

"mcp": { "serencp": { "type": "remote", "url": "http://127.0.0.1:8080/mcp", "enabled": true } }

Make sure your guest OS is configured to use the serial console.

GRUB_CMDLINE_LINUX_DEFAULT="console=ttyS0,115200n8"
T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100
systemctl enable serial-getty@ttyS0.service systemctl start serial-getty@ttyS0.service
<serial type="tcp"> <source mode="bind" host="127.0.0.1" service="4555" tls="no"/> <protocol type="raw"/> <target type="isa-serial" port="0"> <model name="isa-serial"/> </target> <alias name="serial0"/> </serial>

- Request:{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
- Response: List of tools with their input schemas.

- Request Format:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tool_name", "arguments": { ... } } }

The server supports real-time VM output streaming through MCP protocol notifications and resource subscriptions. This provides immediate feedback without requiring polling.

VM output is automatically streamed as JSON-RPC 2.0 notifications using the MCP resources pattern:

{ "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { "uri": "vm://<vm_name>/output", "content": "output data here", "stream": "stdout" } }

- uri: Resource URI in the formatvm://<vm_name>/output
- content: The actual output data (UTF-8 safe)
- stream: "stdout" or "stderr"

- Real-time Feedback: VM output appears immediately without polling
- Efficient: Push-based model reduces overhead compared to polling
- UTF-8 Safe: Binary data is converted to UTF-8 with escaped representations for non-printable characters
- Backward Compatible: Existingreadtool continues to work for pull-based access
- MCP Resources Pattern: Uses standardizednotifications/resources/updatedfor VM output
- URI-based Access: VM output accessible viavm://<vm_name>/outputresource URI

The server supports configurable log levels:debug,info, anderror. By default, debug logging is enabled.

All tools include enhanced MCP annotations for better UI integration (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint).

Starts the bridge for a specific VM. If a bridge already exists, it is restarted to ensure aclean slatewith fresh exponential backoff state.New behavior: Automatically spawns a graphical terminal window linked to the session using the internal client ofserencp.pl. The PID of this terminal is stored to avoid duplicate windows.

- Arguments:{"vm_name": "string", "port": "number"}(port is optional, default: 4555)
- Returns:{"success": true, "message": "...", "port": 4555, "socket_in": "/tmp/serial_VM_NAME.in", "socket_out": "/tmp/serial_VM_NAME.out", "session_id": "session_...", "terminal_pid": 1234}
- Example:tools/call {"name": "start", "arguments": {"vm_name": "MYVM", "port": 4555}}
- Annotations: Non-destructive, idempotent, open world

- Arguments:{"vm_name": "string"}
- Returns:{"running": true/false, "vm_name": "...", "port": ..., "buffer_size": ...}
- Annotations: Read-only, closed world

Reads all available output from the VM serial console's dedicated output Unix socket with a 2-second timeout. Live output is also streamed via notifications.

- Arguments:{"vm_name": "string"}
- Returns:{"success": true, "output": "..."}
- Annotations: Read-only, open world

Sends a command to the VM serial console via its dedicated input Unix socket.

- Arguments:{"vm_name": "string", "text": "command"}
- Returns:{"success": true/false, "message": "..."}
- Example:tools/call {"name": "write", "arguments": {"vm_name": "MYVM", "text": "ls -l /"}}
- Annotations: Non-destructive, non-idempotent, open world

Stops the bridge for a specific VM, cleaning up all PTYs, child processes, and temporary Unix sockets.

- Arguments:{"vm_name": "string"}
- Returns:{"success": true/false, "message": "..."}
- Annotations: Destructive (stops bridge), idempotent, closed world

The script connects to the VM serial console as a client and provides two Unix socket servers: one for input at/tmp/serial_${VM_NAME}.inand one for output at/tmp/serial_${VM_NAME}.out. It supports both an internal Unix socket client mode and automatic terminal spawning. The MCP server handles JSON-RPC commands and replies via MCP-compliant notifications.

When a VM disconnects, the bridge now uses exponential backoff to prevent reconnection storms:

- Initial backoff: 1 second
- Maximum backoff: 60 seconds
- Per-VM state tracking: Each VM maintains its own backoff timer
- Backoff resets on successful connection

Version 1.1 introduces a truly non-blocking write system with three modes:

- Mode 0 (Pure Non-Blocking): Returns immediately if would block
- Mode 1 (Buffered): Queues data if can't write immediately
- Mode 2 (Legacy): Retry with timeout (default for backward compatibility)
- Maximum write buffer: 1MB per destination
- Automatic buffer flushing in the event loop

- Binary data from VM is converted to UTF-8 safely
- Non-printable characters are escaped for JSON transport
- Preserves data integrity while ensuring JSON compatibility

The script can be run in client mode to connect to an existing bridge:

./serencp.pl --socket /tmp/serial_${VM_NAME}.out

This mode provides direct terminal access to the VM serial console through the Unix socket interface.

You can explicitly specify which terminal to use:

This bypasses automatic detection and uses the specified terminal.

graph TD VM["VM Serial Console (TCP:127.0.0.1:4555+)"] Bridge["Perl Bridge Child (Forked)"] PTY["Pseudo-Terminal (PTY Master)"] MCP["serencp MCP Server (Main Event Loop)"] Client["MCP Client (LLM / Opencode)"] UnixIn["Unix Input Socket (/tmp/serial_VM_NAME.in)"] UnixOut["Unix Output Socket (/tmp/serial_VM_NAME.out)"] ScriptClient["serencp.pl --socket /tmp/serial_VM_NAME.out"] ExtClients["External Clients (optional)"] LiveTerminal["Live Terminal View (Auto-Spawned)"] VM <--> Bridge Bridge <--> PTY PTY <--> MCP MCP <--> Client MCP -.-> UnixIn MCP -.-> UnixOut UnixOut <--> ScriptClient UnixOut <--> LiveTerminal UnixOut -.-> ExtClients ScriptClient --> UnixIn LiveTerminal --> UnixIn ExtClients --> UnixIn

The parent MCP server usesIO::Selectto multiplex:
- STDIN: JSON-RPC commands from the LLM or Opencode.
- PTY Master: Real-time data from/to the VM via the child bridge.
- Unix Input/Output Sockets: Listeners for external terminal connections.
- Unix Clients: Active terminal sessions connected to the Unix sockets.

When the VM disconnects, the parent detects the PTY closure and automatically restarts the bridge child to maintain persistence.

sequenceDiagram autonumber participant VM as VM (serial console) participant TCP as IO::Socket::INET (TCP 127.0.0.1:port) participant Bridge as Bridge process (child) participant PTY as IO::Pty (master/slave) participant MCP as MCP server (parent) participant USockIn as IO::Socket::UNIX (/tmp/serial_<vm>.in) participant USockOut as IO::Socket::UNIX (/tmp/serial_<vm>.out) participant Term as Terminal client %% Initial connection MCP->>Bridge: fork() + PTY creation Bridge->>TCP: TCP connection to the VM's serial port TCP-->>Bridge: OK (socket connected) Bridge-->>MCP: READY via pipe %% VM -> user flow VM-->>TCP: Serial output (bytes) TCP-->>Bridge: Raw data Bridge-->>PTY: Write into PTY slave PTY-->>MCP: Data read from PTY master MCP->>MCP: Buffer ring + JSON stdout notification MCP-->>USockOut: Make data available to output clients Term-->>USockOut: Unix output socket connection (read-only) USockOut-->>MCP: New connection accepted() MCP-->>Term: History + live stream %% User -> VM flow Term->>USockIn: Unix input socket connection (write-only) USockIn-->>MCP: New connection accepted() Term->>USockIn: Keyboard input (command) USockIn->>MCP: Client data MCP-->>PTY: Write into PTY master PTY-->>Bridge: Data read from PTY slave Bridge-->>TCP: Write to TCP socket TCP-->>VM: Command received on the serial console

For direct interaction outside of the MCP environment, you can use the script itself as a client by specifying the output socket:

./serencp.pl --socket /tmp/serial_${VM_NAME}.out

New output connections automatically receive the last 60 lines of history. Live output notifications are sent automatically when VM data is received, providing real-time streaming without polling. To send input, the client automatically opens and writes to the corresponding input socket (/tmp/serial_${VM_NAME}.in).

-

No terminal window is opened: If the automatic terminal spawning fails, first check if graphical apps can be launched from a root terminal (e.g :pluma) Then check the following environment variables are properly set:

- XAUTHORITY: Required for X11 authentication (e.g.,/root/.Xauthority)
- XDG_RUNTIME_DIR: Should be set to the user's runtime directory (e.g.,/run/user/0)
- DBUS_SESSION_BUS_ADDRESS: Required for D-Bus session communication

To fix these issues, run the script from a proper X11 session where these variables are automatically set, or export them manually:

export XAUTHORITY=/root/.Xauthority export XDG_RUNTIME_DIR=/run/user/0

Terminal detection fails: Use the--terminaloption to explicitly specify your terminal emulator:

Failed to get tools: Ensure the script is run in an environment where standard input/output is captured. Usetools/listto verify connectivity.

Bridge not running: Callstartbefore attempting to read or write.

Socket Permission: Ensure/tmpis writable by the user running the MCP server.

Syntax Check: Runperl -c serencp.plto verify script integrity.

The MCP server failed to start: Check that all required Perl modules are loaded. Runperl -c serencp.plto verify syntax and module loading. If you see "Can't locate ... in @INC", install the missing module (e.g.,cpan IO::Ptyfor non-core modules).

Write buffer full: If you see "Write buffer full" warnings, the destination is not keeping up with data. This is normal during high-throughput scenarios and data will be dropped.

Exponential backoff active: If the bridge keeps restarting, you'll see increasing delays between reconnection attempts (1s, 2s, 4s... up to 60s). This is intentional to prevent connection storms.

- seren- Serenity / Serial
- cp- MCP (Model Context Protocol)

Since it's a complex script, your help / pull requests are much appreciated !

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.