feat(tooling): register the remote-ssh MCP server and split the SSH guidance
elway is a convergent deploy runner and it is good at that: 64 playbooks, sudo handled once, upload, and three tiers of idempotency. It is the wrong shape for "what is running over there", which is why this session answered every reconnaissance question with a raw `ssh host '...'` despite CLAUDE.md saying to use elway — a playbook for a read-only `docker ps` is ceremony, and each ad-hoc command is a fresh handshake and a fresh permission decision. The MCP server fills that half. It holds a persistent shell, so cwd and exported variables survive between calls and a command costs ~6 ms rather than a full connect. Measured against nh3-extdev and irv-ml1: state persisted across calls on both, including the host reached over WireGuard. CLAUDE.md now routes by intent — changing versus looking — rather than naming one tool, because the previous single-tool instruction was being ignored in practice for exactly the cases it did not fit. Registered project-scoped rather than user-scoped: this workspace is where SSH infra work happens, and a server that can open shells on the fleet should be opted into per project rather than inherited everywhere. SSH_MCP_STRICT_ALLOWLIST=1 is set in .mcp.json deliberately and documented as load-bearing. Without it the allowlist is every Host in ~/.ssh/config — 18 aliases including both backup servers, database VMs and a tenant-managed host. Verified: corviduo-dev, present in ssh_config but not in our allowlist, is refused with host_not_allowed.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-ssh": {
|
||||
"command": "node",
|
||||
"args": ["/home/lkraven/development/remote-ssh-mcp/dist/index.js"],
|
||||
"env": {
|
||||
"SSH_MCP_STRICT_ALLOWLIST": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,38 @@ lags the code defeats its own purpose.
|
||||
|
||||
**New session starting here?** Read [`docs/orientation.md`](docs/orientation.md) first — fleet topology, backup architecture, governing principles, and all the NFS/DSM/naming gotchas that have cost past sessions time.
|
||||
|
||||
**For SSH-driven work: use `scripts/elway`.** Write a playbook under
|
||||
**SSH work splits two ways. Pick by whether you are CHANGING something or
|
||||
LOOKING at something — neither tool replaces the other.**
|
||||
|
||||
**Changing things → `scripts/elway`.** Write a playbook under
|
||||
`playbooks/<name>.yaml` and run
|
||||
`scripts/elway <host> --playbook ...` instead of chaining
|
||||
`ssh -t host 'sudo …'` commands — handles sudo once lazily,
|
||||
structured pass/change/fail reporting, idempotency via
|
||||
`creates:` / `when:` / `changed_when:`. Template:
|
||||
`creates:` / `when:` / `changed_when:`, and file upload. Template:
|
||||
[`playbooks/elway-smoke.yaml`](playbooks/elway-smoke.yaml).
|
||||
|
||||
**Looking at things → the `remote-ssh` MCP server.** Registered project-scoped in
|
||||
[`.mcp.json`](.mcp.json); tools `ssh_hosts` / `ssh_open` / `ssh_run` / `ssh_peek` /
|
||||
`ssh_interrupt` / `ssh_list` / `ssh_close`. It holds a **persistent shell**, so
|
||||
`cd` and exported variables survive between calls and each command costs ~6 ms
|
||||
instead of a fresh handshake. Reach for it for reconnaissance — the
|
||||
`docker ps` / `grep` / `curl` questions where authoring a playbook is ceremony.
|
||||
It deliberately has **no file transfer and no idempotency**; that is elway's half.
|
||||
|
||||
- Source: `~/development/remote-ssh-mcp` — our fork of
|
||||
[the-nine-nation/remote-ssh-mcp](https://gitea.phasefinal.com/vh/remote-ssh-mcp)
|
||||
(MIT). `.mcp.json` points at the **built** `dist/`, so after editing the fork
|
||||
run `npm run build` or the server keeps serving the old code.
|
||||
- ⚠ **The host allowlist is the only real containment boundary.** The denylist
|
||||
is three regexes and a `bash -c` walks past it. We run with
|
||||
`SSH_MCP_STRICT_ALLOWLIST=1`, which makes `~/.config/remote-ssh-mcp/config.json`'s
|
||||
`allowedHosts` **authoritative** rather than additive — without it, every one of
|
||||
the 18 `Host` entries in `~/.ssh/config` is reachable. Widening that list is a
|
||||
deliberate act; do it in the config file, not by relying on discovery.
|
||||
- Hosts not in the allowlist return `host_not_allowed`. Hosts reachable only by
|
||||
raw IP (no `Host` alias) are not reachable at all — add an alias first.
|
||||
|
||||
**Task visibility via task-board.** If the Claude Code session has
|
||||
the `task-board` plugin enabled (installed from
|
||||
`git@gitea.phasefinal.com:vh/task-board.git`), a card at
|
||||
|
||||
Reference in New Issue
Block a user