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.
|
**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
|
`playbooks/<name>.yaml` and run
|
||||||
`scripts/elway <host> --playbook ...` instead of chaining
|
`scripts/elway <host> --playbook ...` instead of chaining
|
||||||
`ssh -t host 'sudo …'` commands — handles sudo once lazily,
|
`ssh -t host 'sudo …'` commands — handles sudo once lazily,
|
||||||
structured pass/change/fail reporting, idempotency via
|
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).
|
[`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
|
**Task visibility via task-board.** If the Claude Code session has
|
||||||
the `task-board` plugin enabled (installed from
|
the `task-board` plugin enabled (installed from
|
||||||
`git@gitea.phasefinal.com:vh/task-board.git`), a card at
|
`git@gitea.phasefinal.com:vh/task-board.git`), a card at
|
||||||
|
|||||||
Reference in New Issue
Block a user