pidp10-mcp
An MCP server for driving an ITS session on a PiDP-10 emulator over raw TCP, supporting persistent connections, raw control-byte transmission, and escape syntax for DDT commands. It enables interaction with ITS systems through natural language via MCP tools like open, send, read, and status.
README
pidp10-mcp
An MCP server for driving an ITS session on a PiDP-10 (simh KA10) emulator over its raw TCP terminal line.
It exists because generic telnet MCP servers do not work against this target: they cannot transmit raw control bytes, they tie the TCP connection's lifetime to tool-call cadence, and they reconnect dead sessions in the background — which, on a port that maps to a single terminal line, produces zombie connections fighting each other for it.
What it does differently
- Raw control bytes get through. A
~-escape syntax insendputs exact bytes on the wire:~zfor the Ctrl-Z that calls ITS,~efor the ESC that DDT prints as$,~xNNfor anything else. - The connection belongs to the server process, not to tool calls. A background reader drains the socket continuously into a 256 KB scrollback. Nothing cares how long the client spends thinking between calls; a three-minute gap is invisible to the session.
- It never reconnects on its own. If the socket dies, the session is marked dead and the next tool response says so. Reopening is an explicit decision.
- Closes hard.
close()setsSO_LINGERto zero so the socket is reset rather than left in a half-closed state that keeps the line marked busy, and the same teardown runs fromatexitplus SIGTERM/SIGHUP handlers. - Terse responses. Only output produced since the last call, VT52 noise stripped, followed by one trailer line. No banners, no echoed inputs, no re-dumping the session log.
Install
Requires Python 3.11+ and the official mcp SDK 2.x.
uv sync # or: pip install -e .
uv run pidp10-mcp # stdio transport (default)
Register it with an MCP client — for Claude Code:
claude mcp add pidp10 -- uv --directory /path/to/pidp10/mcp run pidp10-mcp
or by hand, in an mcpServers config block:
{
"mcpServers": {
"pidp10": {
"command": "uv",
"args": ["--directory", "/path/to/pidp10/mcp", "run", "pidp10-mcp"],
"env": { "PIDP10_HOST": "pidp10.local", "PIDP10_PORT": "10018" }
}
}
}
Streamable HTTP
uv run pidp10-mcp --http --http-host 127.0.0.1 --http-port 8010
Configuration
| Env var | CLI flag | Default | Meaning |
|---|---|---|---|
PIDP10_HOST |
--host |
pidp10.local |
Emulator host |
PIDP10_PORT |
--port |
10018 |
Emulator TCP port (one line) |
PIDP10_MCP_HOST |
--http-host |
127.0.0.1 |
Bind address for --http |
PIDP10_MCP_PORT |
--http-port |
8010 |
Bind port for --http |
open(host, port) can override the host and port per call.
Escape syntax
Escapes are expanded in send's input. An unknown escape is an error rather
than being passed through as text — silently sending ~q to DDT is worse than
a rejection.
| Escape | Byte | Meaning |
|---|---|---|
~z |
0x1A |
Ctrl-Z — calls ITS; a fresh line ignores all other input |
~e |
0x1B |
ESC / altmode — DDT's $ |
~c |
0x03 |
Ctrl-C |
~g |
0x07 |
Ctrl-G |
~d |
0x7F |
Rubout |
~s |
0x13 |
Ctrl-S |
~o |
0x0F |
Ctrl-O |
~r |
0x0D |
CR, when an explicit one is wanted mid-line |
~n |
0x0A |
LF — a DDT command (examine next location), not a newline |
~t |
0x09 |
Tab |
~xNN |
0xNN |
Any byte, two hex digits |
~~ |
~ |
Literal tilde |
~- |
— | At the very end: do not append the automatic CR |
Line endings
send appends a CR (0x0D) automatically, because that is what the line
editor wants, and normalises any literal LF or CRLF in the input to CR. A bare
LF is not a newline on this system — it is a DDT command. If you genuinely
want to send one, ~n is exempt from normalisation.
raw=true sends the expanded bytes verbatim: no CR appended, no normalisation.
Tools
| Tool | Purpose |
|---|---|
open(host?, port?) |
Connect. Idempotent — reports status if already open. Returns any greeting bytes. |
send(input, expect?, timeout_ms=10000, quiet_ms=700, auto_more=true, raw=false) |
Send input, return the output it produced. |
read(timeout_ms=2000, expect?, quiet_ms=700, auto_more=true) |
Collect more output without sending anything. |
peek(last_n_chars=2000) |
Re-show recent scrollback without moving the read cursor. |
status() |
Connected?, host:port, uptime, bytes, pending output, death reason. |
close() |
Hard-close the socket so the emulator frees the line. |
How send and read decide to return
They return on whichever comes first:
expect(a regex) matches the new output → reasonmatched- the line has been silent for
quiet_ms→ reasonquiet timeout_mselapses → reasontimeout
The quiet timer only starts after the first byte arrives, so a program that
takes five seconds to say anything is not cut off at 700 ms. A call that sees
no output at all runs to timeout_ms and returns timeout.
With auto_more (default on), a trailing --More-- (Space=yes, Rubout=no)
prompt is answered with a space and collection continues, up to 20 pages; the
answered prompts are removed from the returned text. Hitting the cap returns
reason more_limit, and read continues from there. To flush a pager instead
of paging through it, send ~d (rubout).
send and read return only output produced since the last call. peek
does not move that cursor.
Output filtering
Raw output carries VT52 escape sequences, NUL padding and occasional telnet
IAC bytes. The filter drops NULs, ESC+letter sequences, ESC Y <row> <col>
cursor addressing, IAC negotiation (without implementing any telnet stack), and
other nonprinting bytes; it keeps text, tabs and newlines. CR, LF and CRLF all
become a single \n. Trailing whitespace and runs of blank lines are collapsed
in returned text to save tokens; the scrollback keeps the unabridged text.
Typical session
open()
send("~z", expect="Happy hacking|ITS") # ^Z calls ITS -> DDT banner
send(":login rms")
send(":listf") # pages collected automatically
send("foo~ej") # f-o-o ESC j CR (DDT's foo$j)
close()
A detached but logged-in ITS session gets auto-logged-out after about five minutes; ITS itself never times out an idle line, so a held-open session is stable indefinitely.
Tests
uv run pytest # offline: filter, escapes, session, tools
PIDP10_LIVE=1 uv run pytest -m live # acceptance tests on a real emulator
PIDP10_LIVE=1 PIDP10_LIVE_SLOW=1 uv run pytest -m live # ...including the 3-minute idle test
The offline tests run the whole stack — including the MCP tool layer, via an in-process client — against a fake TCP line, so no emulator is needed.
Live tests are skipped unless PIDP10_LIVE=1; they take the single terminal
line for their duration. They honour PIDP10_HOST / PIDP10_PORT, plus
PIDP10_USER (default guest) and PIDP10_LISTF_DIR (default sys;, which
needs to be a directory big enough to make the pager appear).
The test_three_minute_gap_does_not_drop_the_session case sits idle for 190
seconds on purpose — it is the regression test for the failure that motivated
this server — so it needs PIDP10_LIVE_SLOW=1 as well.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.