Whitelisted host-operations MCP server for SDL2 lab machines. It answers "is the service up, what did it log, did the USB adapter come back, is the local device API answering" — the class of incident (boot USB-enumeration races, corrupt-venv locks, silent partial syncs) that a network client can see but not diagnose.
Deliberate non-goals: no shell tool, no arbitrary commands, and no path
to any device /control/* endpoint. Hardware control belongs to the
lab-skills SDK and its MCP server (ac-organic-lab — AGENT_RULES §1.1).
The tool surface below is the complete surface; every target must be
whitelisted in config.toml, so a service or port not listed cannot be
touched at all.
| Tool | Mutates? | Notes |
|---|---|---|
host_info() |
no | hostname, OS, uptime, disk free, loadavg, backend |
service_status(service) |
no | systemd unit / Windows service state |
tail_service_log(service, lines) |
no | journald (Linux) or NSSM .out/.err logs (Windows); 10–400 lines |
restart_service(service) |
yes | only services in the smaller restartable list; Windows path runs nssm restart + the mandatory sc continue quirk; audited (see below) |
list_serial_ports() |
no | pyserial ([serial] extra) or /dev glob fallback |
probe_local_status(port) |
no | GET http://127.0.0.1:<port>/status for whitelisted ports |
Selected automatically by platform:
-
systemd (Linux, Raspberry Pi):
systemctl/journalctl. Restarts can be prefixed withsudo -n(use_sudo = true) — grant only the exact units, e.g./etc/sudoers.d/sdl-lab-hostops:sdl2 ALL=(root) NOPASSWD: /usr/bin/systemctl restart plateloc.service -
nssm (Windows device PCs):
sc query,nssm restart+sc continue(DEVICE_PC_SETUP §7 quirk), log tails fromC:\SDL_Logs\<svc>.{out,err}.log.
stdio(default): the MCP client spawns the process per connection. No daemon, no token. This is also the daemonless "lite" mode — see the Pi section.http(streamable-http): long-lived service. A non-loopback bind refuses to start unlessHOSTOPS_TOKENis set; clients sendAuthorization: Bearer <token>. Network reachability is additionally gated by Tailscale ACLs, per the lab's v1 auth posture (STATUS_SPEC §11). Exception:GET /statusis unauthenticated — it serves a read-only STATUS_SPEC v1.2 envelope (read-only conformance clause, §9) so the dashboard can register the instance inequipment.yamland monitor the watchers. Primary operation: none (ops service) —activityis permanentlyidle; claims/allowed_actionsare N/A because there is no control surface.
| Var | Meaning |
|---|---|
HOSTOPS_CONFIG |
path to config.toml (or pass --config) |
HOSTOPS_TOKEN |
bearer token, required for non-loopback http binds |
HOSTOPS_INGEST_URL |
e.g. http://sdl2-server:8001 — enables audit rows |
Audit: every restart_service posts a hostops_action event to the
dashboard's POST /api/ingest/events (best-effort, never blocks the
restart). Reads are not audited. Register the hostops_action event type in
ac-organic-lab/docs/LAB_MONITORING.md §4 when the first instance ships.
Follow ac-organic-lab/docs/DEVICE_PC_SETUP.md §3 with:
cd C:\Users\sdl2\Projects
git clone <this-repo> sdl-lab-hostops
cd sdl-lab-hostops
Copy-Item config.example.toml config.toml # edit whitelists for this PC
C:\SDL_Tools\uv.exe sync --extra serial
nssm install sdl-lab-hostops C:\SDL_Tools\uv.exe run --project C:\Users\sdl2\Projects\sdl-lab-hostops lab-hostops-serve --transport http
nssm set sdl-lab-hostops AppDirectory C:\Users\sdl2\Projects\sdl-lab-hostops
nssm set sdl-lab-hostops AppEnvironmentExtra HOSTOPS_TOKEN=<token> HOSTOPS_INGEST_URL=http://sdl2-server-gaia.tail6a1dd7.ts.net:8001
# ... stdout/stderr logs, auto-start, run-as user: exactly as DEVICE_PC_SETUP §3
nssm start sdl-lab-hostops
sc continue sdl-lab-hostopsNote the service account needs rights to restart the other services
(sc sdset per service, or run sdl-lab-hostops under an account that has them).
A permission failure is reported truthfully in the tool result.
git clone <this-repo> /opt/sdl-lab-hostops && cd /opt/sdl-lab-hostops
cp config.example.toml config.toml # edit
uv sync # base deps only — no extras needed
sudo cp deploy/sdl-lab-hostops.service /etc/systemd/system/
sudo systemctl enable --now sdl-lab-hostopsThe base install is small (mcp + httpx/starlette/uvicorn, all pure-Python or
armv8 wheels) and idles at a few tens of MB — the same footprint class as the
sense-every-zone gateway already running on sdl2-pi0-environ-01. Two ways
to run it lighter:
-
http, minimal: skip the
[serial]extra, keep the whitelist tiny. -
daemonless stdio-over-SSH — zero resident footprint; the central Hermes spawns it on demand and SSH is the auth (no token, no open port):
# Hermes mcp_servers entry on the central server hostops-pi0-environ: command: ssh args: [sdl2@sdl2-pi0-environ-01, /opt/sdl-lab-hostops/.venv/bin/lab-hostops-serve, --transport, stdio, --config, /opt/sdl-lab-hostops/config.toml]
Requires key-based SSH from the central server. Prefer this on the Pis; prefer the http service on the Windows PCs (no SSH daemon there).
Register each instance in the lab's reviewed MCP registry
(ac-organic-lab/mcp/servers.yaml) with the full 6-tool include list, and
mirror that allowlist in the client config (e.g. the Hermes lab-ops
profile's mcp_servers.<name>.tools.include). A new tool added here should
only reach an agent after both lists are updated — that's the point.
uv run pytest -q