pidp10-mcp

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.

Category
访问服务器

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 in send puts exact bytes on the wire: ~z for the Ctrl-Z that calls ITS, ~e for the ESC that DDT prints as $, ~xNN for 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() sets SO_LINGER to 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 from atexit plus 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 → reason matched
  • the line has been silent for quiet_ms → reason quiet
  • timeout_ms elapses → reason timeout

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选