Files

6.5 KiB

name, description, version, author, license, platforms, prerequisites, metadata
name description version author license platforms prerequisites metadata
saiop-mcp-servers MCP server setup for SAIOP (git, postgres read-only, docker, proxmox snapshot access) — least-privilege patterns, and what was operator-approved to widen beyond the initial conservative defaults. 2.0.0 Claude Code (SAIOP ops session) MIT
linux
env_vars commands
MCP_POSTGRES_READONLY_PASSWORD
uvx
uv
ssh
hermes
tags
SAIOP
MCP
security
least-privilege
postgres
docker
git
proxmox
ssh

MCP servers on SAIOP (Phase 6.3)

When to use

Adding or auditing MCP tool access for Hermes (Deployment Guide §6.3).

What's wired up and why

  • git-mcp (uvx mcp-server-git -r /opt/ai-stack) — all 12 tools enabled (including commit/branch/checkout). Low risk: git is naturally revertible, no host-level escalation path.
  • postgres-mcp (uvx postgres-mcp --access-mode restricted) — a dedicated mcp_readonly Postgres role exists (SELECT-only, granted on saiop + netbox DBs specifically, not the other app DBs). Two independent enforcement layers: the role itself can't write (verified: DELETE returns permission denied), and --access-mode restricted blocks non-SELECT statements at the app layer too. Connection string goes through the DATABASE_URI env var (checked the package source — os.environ.get("DATABASE_URI", args.database_url)), not a CLI arg, since CLI args are visible to any local user via ps aux. postgresql's port is published to 127.0.0.1:5432 only (loopback) since Hermes runs on the host, not in Docker, and can't resolve container names.
  • docker-mcp (uvx docker-mcp) — operator-approved widening: originally restricted to get-logs/list-containers only (the package has no native read-only mode; create-container/deploy-compose are full write/deploy). After an explicit flag-and-confirm round, the operator chose to enable all 4 tools. This is a real, deliberate widening of what Hermes can do to the host via MCP — if revisiting this decision, remember the restriction mechanism is still available and field-proven (tools: {include: [...]} in config.yaml, same mechanism verified for the Telegram platform-toolset restriction) if you want to dial it back.
  • proxmox-mcp — a custom-written, single-tool MCP server (/opt/ai-stack/hermes/scripts/proxmox_mcp.py, run via uv run --with mcp python3 ...), not an off-the-shelf package. Exposes exactly one tool, proxmox_qm(command), which SSHes to the Proxmox host as the existing scoped claude account using a dedicated keypair generated specifically for Hermes (not reusing the operator's desktop key — separate blast radius per principal, same pattern as every other dedicated credential in this project).
    • All three off-the-shelf SSH MCP packages checked (mcp-server-ssh, ssh-mcp-server, ssh-mcp) were rejected before writing a custom one: mcp-server-ssh exposes a sprawling toolkit (execute_command, file_operations, process_manager, sftp_upload/download, system_monitor) — far more surface than needed. ssh-mcp (the mcpssh package) was the narrowest (2 tools) but depends on an external "Passman" credential-management service to resolve connection details — an unwanted new dependency just to satisfy one fixed host/user/key. ssh-mcp-server didn't expose a usable interface for this case either. Writing ~60 lines directly against the mcp Python SDK (FastMCP) was less work than adapting any of them, and means the only security-relevant logic in the whole path is the server-side sshd restriction (see below) — the MCP server itself is a deliberately thin, unopinionated pipe.
    • The real enforcement is server-side, not in the MCP tool. An sshd ForceCommand drop-in on the Proxmox host (Match User claude in /etc/ssh/sshd_config.d/claude-restricted.conf) routes every SSH session for that account through a wrapper script (/usr/local/bin/claude-ssh-wrapper.sh) that only permits qm snapshot/listsnapshot/status patterns — regardless of what the SSH client (the MCP tool, or anything else) requests. Verified directly: allowed commands work, arbitrary commands (whoami, cat /etc/passwd) are rejected, and a command-injection attempt via the snapshot-name argument (qm snapshot 141 test; whoami) is rejected too (the wrapper splits on whitespace and validates each token, not just regex-matching the whole string). AllowAgentForwarding/AllowTcpForwarding/ PermitTTY are all disabled in the same Match block.
    • Wrapper-script gotcha hit while building this: a long elif [[ ... =~ ... ]] line got an actual newline inserted into it during copy/paste into nano, breaking the regex syntax (manifested as a confusing unexpected argument 'newline' to conditional binary operator error — not obviously a paste problem from the error text alone). Rewritten to avoid any long lines at all (read -ra parts <<< "$cmd" + per-token validation instead of one big regex) specifically so there's nothing left for a wrap/paste to corrupt.
    • I don't have write access to /etc/ssh or reload sshd myself — only the scoped claude account, which can't escalate to root. The operator applied the sshd-side changes directly; I verified them afterward over SSH from the VM side.

What was deliberately NOT wired up

  • pfSense/MikroTik MCP — no real device credentials exist in this environment to configure them safely. Needs the operator to provide scoped, read-only-by-default credentials first.

Useful commands

hermes mcp list                 # see all servers + tool-count summary
hermes mcp test <name>          # verify a server connects and discover its tools

hermes mcp add has an interactive curses-based tool picker with no clean non-interactive path — for a scripted/repeatable setup, hand-edit mcp_servers: in config.yaml directly instead (schema: {command, args, env, enabled, tools: {include: [...]}}).

If revisiting the Docker or Proxmox scope later

  • To re-restrict docker-mcp: add back tools: {include: ["get-logs", "list-containers"]} under its entry.
  • To extend proxmox-mcp's reach: the only place that matters is the sshd wrapper on the Proxmox host, not this MCP server or Hermes's config — the MCP tool will happily send any command, the wrapper decides what actually runs.