ADR 0001: Runtime, SDK, and SSH library¶
- Status: Accepted
- Date: 2026-05-19
Context¶
relay-shell must speak the Model Context Protocol reliably, run arbitrary local
commands and interactive PTYs, and perform full SSH operations (exec, PTY,
SFTP, port forwarding, jump hosts). The choice affects protocol correctness,
type safety, and how much transport/auth code we own versus delegate.
Decision¶
- Python 3.12+ and the official
mcpSDK (FastMCP), initially pinned tomcp==1.27.1(validated by the test suite), with the surrounding stack pinned inrequirements.txt. FastMCP provides the protocol, stdio and streamable-HTTP transports, and an OAuth 2.1 hook. The pin moves on a deliberate, tested bump; the current pins are the ones inrequirements.txt(tested) andpyproject.toml(supported range), and each bump is listed under Consequences below, not on this line. asyncsshfor all SSH. It is pure-Python, asyncio-native, and covers the entire required surface (exec, interactive PTY, SFTP, local/remote/ dynamic forwarding, agent,ProxyJump/tunnels,known_hosts, keepalive) with one dependency and no systemsshshelling.- Local execution uses
asynciosubprocesses and a non-blocking PTY master driven by the event loop (executor fallback, removed in 0.4.0 as dead code, A14-X3) - stdlib only.
Consequences¶
- One protocol dependency, one SSH dependency; both typed.
ssh_configsemantics (includingProxyJump) come free via asyncssh's config support. - Pinning a known-good SDK version trades newest features for reliability; upgrades are deliberate and tested.
- No reliance on a system
sshbinary or its version quirks; behaviour is reproducible across hosts. - Pin movement (one line per validated bump; the runbook §8.9 trigger):
mcp1.27.1 → 1.27.2 (PR #66, 2026-06-04). Validated by the full test suite plus the ADR 0005 step-3 upstream-symbol check (FastMCP/Contextkwargs and the nine OAuth provider methods resolve unchanged); recorded in the ADR 0005 2026-06-12 outcome.mcp1.27.2 → 1.28.1 andasyncssh2.23.1 → 2.24.0 (shipped in v0.3.0, 2026-07-15;pyproject.toml/requirements.txt). The living docs (README status line and compatibility matrix,docs/architecture.md) were reconciled to the same pins (CHANGELOG DOC-1); the release pipeline runs the full test suite as its build gate.mcp1.28.1 → the v2 line, moving off the exact pin tomcp>=2,<3(PR #164/#165, 2026-08-12).mcp.server.fastmcp.FastMCPis nowmcp.server.mcpserver.MCPServer;host/port/stateless_http/json_responsemoved from the constructor to per-transportrun()kwargs (http_transport_kwargs()inserver.py), andcall_toolnow returns aCallToolResultinstead of a(content, structured)tuple. No change topolicy,redaction,audit,patterns,broker, or theRelay.runbody; the trust boundary is untouched. Validated by the full test suite (450 passed),ruff check/ruff format --check, andmypy --strictacross 20 source files; the living docs (README status line and compatibility matrix,docs/architecture.md) were reconciled to the same range.- Tested pins
mcp2.2.0 → 2.3.0 andasyncssh2.24.0 → 2.24.1 (shipped in v0.4.0, 2026-10-15;requirements.txt, thedevextra and the pre-commit hook in lockstep). The supported ranges staymcp>=2,<3andasyncssh>=2.23.0,<3.
Rejected¶
- TypeScript MCP SDK: diverges from the Python execution/SSH ecosystem here.
- Shelling out to system
ssh/scp: brittle parsing, version-dependent behaviour, weaker error semantics thanasyncssh. paramiko: synchronous core; bridging to async for PTY/forwarding is more code and more failure modes thanasyncssh.