runpod-mcp

runpod-mcp

Enables managing RunPod compute for the Learning-to-Swim replication through natural language, including pod lifecycle, SSH/rsync file transfer, detached training jobs, and supervised runs with cost guardrails.

Category
访问服务器

README

runpod-mcp — custom MCP server for the Learning-to-Swim replication

Standalone note: this server was extracted (full history) from the learning-to-swim-replication project. Relative links like ../runbook/RUNBOOK.md refer to that parent project and only resolve when this repo sits inside it (or is symlinked there); the server itself runs standalone.

Task-shaped tools (14) mirroring the parent project's runbook/RUNBOOK.md instead of ~50 generic API mirrors. Custom because no RunPod API executes commands on a pod — the official MCP covers only the control plane; running pod_setup.sh, the axis sanity sweep, and training needs SSH + rsync, encoded here with cost guardrails in code.

Architecture

.mcp.json → run.sh (venv bootstrap) → server.py (FastMCP, stdio; thin)
                                        └── runpod_mcp/
                                            config.py     Keychain key fetch + rpa_ scrubber
                                            api.py        REST v1 (pods/volumes/billing) + unauth GraphQL gpuTypes
                                            guardrails.py one-pod-per-vehicle (unknown refused) · 4090-only · no spot · volume required · confirm gate
                                            ssh.py        hardened ssh/scp/rsync; known_hosts_runpod; 60s conn cache
                                            jobs.py       detached jobs: /workspace/jobs/<id>/{cmd.sh,pid,out.log,exit_code,meta.json}
                                            training.py   DR tables (RUNBOOK/yaml-cross-checked) + verbatim train cmd
                                            supervise.py  Mac-side background CLI: launch→poll→pull→sync→spend→stop (reuses tools.*)
                                            watch.py      Mac-side ADVISORY observation CLI: discover job→tail out.log→parse metrics→page on plateau/failure/stall (read-only; never stops pods)
                                            remote/       job_wrapper.sh · idle_watchdog.sh · apply_bluerov2_patch.py
                                            deadman.py    Mac-side stop-pod fuse: arm --vehicle → sleep → stop with retries (per-vehicle pid/summaries)
supervise.sh → caffeinate -i wrapper around  python -m runpod_mcp.supervise
watch.sh     → caffeinate -i wrapper around  python -m runpod_mcp.watch   (live-pod behavior UNVERIFIED — fixture/mock-verified only; see CLAUDE.md §D)
deadman.sh   → caffeinate -i wrapper around  python -m runpod_mcp.deadman (arm/cancel REQUIRE --vehicle; bare status reports all vehicles)
  • Stateless & per-vehicle: "the pod" = whatever GET /pods returns matching the selected vehicle's configured name (hippocampus → lts-replication, bluerov2 → lts-replication-bluerov2; every tool's vehicle param defaults to hippocampus, stop_pod/terminate_pod require it explicitly); console and MCP always agree. Only local state: a 60-second (host, port) cache per vehicle Runtime.
  • Async jobs: one SSH call runs setsid bash job_wrapper.sh <dir> <pod_id> <ceiling> <auto_stop>; state lives on the network volume, so it survives MCP restarts, Mac sleep, and pod stop. timeout --kill-after enforces wall-clock ceilings (exit 124); the auto-stop suffix runs AFTER exit_code is written, so a timeout can never defeat it. Pod id is argv-injected (container env vars are unreliable in detached BatchMode shells); /etc/rp_environment is sourced for runpodctl credentials; arming auto_stop probes runpodctl synchronously and fails loudly if it can't work. The probe (2026-08-09) is a three-way diagnostic: the bare-shell checks decide nothing (they answer H1-vs-H2 and capture the bare PATH), /etc/rp_environment is then sourced unconditionally, and the SOURCED pair carries the verdict — NO_RUNPODCTL (binary absent even after sourcing, exit 90), NO_RUNPODCTL_AUTH_SOURCED (still refused after sourcing, exit 91), PROBE_OK (sourced success only; NO_RUNPODCTL_AUTH_BARE is the mid-stream diagnostic that continues).
  • Idle watchdog: reinstalled on every transition-to-running — the container-disk wipe removes runtime-installed material (idle_watchdog.sh itself, the apt X11/GL libs, rsync), which is why install-on-every- transition stays; runpodctl is IMAGE-SHIPPED and back on every boot (a wipe restores the disk from the image, it does not empty it — corrected 2026-08-09). Every 5 min: no live job pid + no sshd session + /workspace/.keepalive older than 60 min → runpodctl stop pod. touch /workspace/.keepalive is the manual-session escape hatch. A successful install reports armed (stop path unverified) — the probe certifies READ (get pod), the watchdog needs WRITE (stop pod); the first real confirmation is a successful-stop entry in /workspace/.idle_watchdog.log. Status (2026-08-09): the install probe has failed on every recorded bring-up (opaque rc=91 pre-fix) — the watchdog has never yet armed; defect 2 ships DIAGNOSED, not CLOSED, and the next bring-up's sentinel settles it. idle_watchdog: FAILED ⇒ arm the Mac-side deadman before any job.
  • Guardrails are code: one pod per declared vehicle (any other pod name on the account is refused), RTX 4090 ×1, SECURE, interruptible forced false, network volume required, terminate_pod needs an explicit vehicle plus the verbatim string terminate <that vehicle's pod_name> (e.g. terminate lts-replication), one job at a time per pod absent force.

Install / registration

Register the server in a project's .mcp.json (Claude Code) with an absolute path to run.sh — run.sh bootstraps its own .venv on first launch:

{
  "mcpServers": {
    "runpod": {
      "command": "bash",
      "args": ["/path/to/runpod-mcp/run.sh"]
    }
  }
}

Setup

  1. API key (never on disk/git/argv — macOS Keychain only; the server reads it via security find-generic-password and scrubs rpa_ values from every error and log):

    security add-generic-password -a kyle -s runpod-api-key -w '<KEY>'
    

    (The lookup account name is currently hardcoded to kyle in runpod_mcp/config.py — adjust both together if your macOS account differs.)

  2. SSH key: ~/.ssh/id_ed25519(.pub) must exist; the .pub is injected at pod-create via the PUBLIC_KEY env var (what runpod/pytorch images actually honor — live-verified; SSH_PUBLIC_KEY also set as belt-and-braces). Direct SSH to root@publicIp:portMappings["22"]; RunPod's proxy SSH is unused (no scp). Host keys land in a dedicated ~/.ssh/known_hosts_runpod, truncated on every pod start (the container disk wipe regenerates host keys, stale entries only cause false MITM failures).

  3. Nothing else — run.sh creates .venv/ and installs requirements.txt on first launch (stamp-gated).

Testing

runpod-mcp/.venv/bin/python -m pytest runpod-mcp/tests -q          # offline (default)
RUNPOD_MCP_LIVE=1 runpod-mcp/.venv/bin/python -m pytest \
    runpod-mcp/tests/test_live.py -q                               # live $0 read-only

Offline tests use httpx.MockTransport + duck-typed fake SSH — no network, no key. Live tests are read-only GETs + an MCP stdio handshake through run.sh (asserts all 14 tools register). DR tables are cross-checked by parsing BLUEROV2/config/bluerov2_heavy.yaml, RUNBOOK.md and APPLY.md; the patch script is exercised against committed fixture excerpts of the pinned 7c5ebe7 sources (plus a SHA-gated test against the real reference clone when present — read-only, tmp copies).

test_supervise.py drives the supervise CLI's core with injected fakes + a fake clock (no real waiting), covering every safety branch: normal completion, job failure, max-wait force-stop, pod-not-running refusal, launch refusal, transient poll errors, capture-failure-still-stops, --no-stop, and terminate_pod is asserted never-called in every case.

Root-repo pytest -q ignores this folder (conftest.py collect_ignore) — the lean root venv has no mcp/httpx.

Supervised runs (supervise.sh)

One command that chains an entire run — verify-pod-running → dry-run-derive a finite wall-clock cap → launch(auto_stop=false) → poll job_status → unconditionally pull /workspace/jobs/<job_id>/ + sync_logs + spend_report → stop_pod → durable JSON summary — so the agent fires it once as a background task and is notified on completion. It reuses runpod_mcp.tools.* (no logic duplication, all guardrails inherited) and never calls terminate_pod. This is a Mac-side CLI, not a 15th MCP tool: a poll-for-minutes tool would block the stdio server.

# training run (background task)
supervise.sh --training curee --dr DR_0 --seed 1 \
    [--interval 45] [--max-wait N] [--backstop 300] [--no-stop] \
    [--sync-subdir rsl_rl/warpauv_direct] [--summary-path PATH]

# generic job — --sync-subdir REQUIRED (pass 'none' to skip the analysis sync;
# the job-dir pull always happens); --vehicle routes the pod (default
# hippocampus; --training mode derives it from the training vehicle instead)
supervise.sh --job-name eval --command "…" --workdir /workspace \
    --sync-subdir <dir|none> [--max-runtime-sec N] [--vehicle bluerov2]

Money-safety: the poll loop has exactly two exits — normal completion → stop_pod; or --max-wait (always finite) elapsed while still running → force-stop + non-zero exit + force_stopped summary flag. A launch refusal → no stop (fix and retry), exit 2. The supervise-<job_id>.json summary in the vehicle's log dir (logs/pod/ hippocampus, logs/pod/bluerov2/ bluerov2) is the recovery contract (a later session reconciles stop state from it). Liveness caveats: caffeinate -i guards idle sleep but not lid-close; run_in_background survival across WarmLifecycle reaping is unverified — the job's timeout ceiling is the guaranteed backstop; the pod-side idle watchdog would back it up but has never yet armed on a recorded bring-up (DIAGNOSED, not CLOSED — see the Idle-watchdog bullet), so arm the Mac-side deadman when ensure_pod reports idle_watchdog: FAILED.

Campaign chains (CUREE/chains/)

One bash script per campaign (named by campaign ID, e.g. chain-011-CUREE_Adaptive-weights.sh): the campaign's whole pod-side job sequence — patches, gates, trainings, evals, syncs — as ordered, sha-pinned links. Chains are launched through supervise.sh (which owns capture-and-stop), never hand-driven; they are the durable record of exactly what a campaign executed.

Dry runs

ensure_pod, run_pod_setup, run_job, launch_training, apply_bluerov_patches all take dry_run=true and return the exact would-be payloads/edits/commands without mutating anything ($0). supervise uses this dry-run path to derive its finite --max-wait before the real launch.

NGC fallback image (manual swap — read first)

nvcr.io/nvidia/isaac-sim:4.5.0 (RUNBOOK Day-1 fallback) has no sshd — it breaks this server's entire SSH story. Switching requires a docker-start command that installs/launches sshd (not a one-line change): flag to Kyle before ever swapping image_name in pod_defaults.yaml.

Known risks (accepted at plan time)

  • The IsaacSim 4.5.0 download URL in pod_setup.sh may 404 — surfaces in job_status log tail; the fix is a runbook edit, not an MCP change.
  • 4090 stock fluctuates per DC; the network volume pins one DC. gpu_availability(data_center_id=...) + ensure_pod's no-GPU recovery recipe cover it; worst case, create a second volume in another DC.

License

MIT — see LICENSE.

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选