Lean SSH MCP: hint engine + tailnet-only + v2 OAuth-gateway design
  • TypeScript 58%
  • JavaScript 42%
Find a file
2026-06-17 09:24:54 +00:00
src kua-control: governed MCP entrypoint (policy+audit over tool core); isDestructive risk tiering; dev identity resolver 2026-06-17 09:24:54 +00:00
.gitignore kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00
DESIGN-v2-oauth-gateway.md kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00
LICENSE kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00
package-lock.json kua-control: governed MCP entrypoint (policy+audit over tool core); isDestructive risk tiering; dev identity resolver 2026-06-17 09:24:54 +00:00
package.json kua-control: governed MCP entrypoint (policy+audit over tool core); isDestructive risk tiering; dev identity resolver 2026-06-17 09:24:54 +00:00
README.md kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00
servers.example.json kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00
tsconfig.json kua-ssh v0.1.0: lean SSH MCP (hint engine, tailnet-only, 7 tools) + v2 OAuth-gateway design/policy core 2026-06-16 12:57:00 +00:00

kua-ssh

An SSH MCP that teaches the agent to use it well.

Most SSH MCPs ship a big drawer of narrow tools — ssh_tunnel_create, ssh_backup_schedule, ssh_session_send, ssh_db_query, forty of them. In practice an agent ignores almost all of it and runs everything through one generic exec, the hard way: hand-rolled nohup for long jobs, docker logs | tail for logs, docker exec ... psql for queries, heredocs for file writes.

We know because we measured it: across a full production migration, an agent used 1 of 37 tools. Every build, log tail, database query, and file copy went through raw exec.

kua-ssh is the reaction to that.

Two ideas

1. Fewer, broader tools. Seven, each one something you'll actually reach for:

tool what it does
servers list configured hosts + their tier and access policy
exec run a command (bash by default); fan out across hosts; detach:true for long jobs
job poll / wait / stop a detached job — no sleep-loops
logs container or file logs, paged and greppable
transfer scp up/down without remembering flags
health probe an endpoint or container, clean pass/fail
db read-query or dump a registered database, credentials resolved for you

2. A hint engine. This is the part worth stealing. Every exec runs its command through a set of rules. When the command looks like a job a dedicated tool does better, the result comes back with a nudge:

exit=0  812ms on sol-prod
stdout:
...

--- kua-ssh hints ---
This ran through `exec`, but a sharper tool fits:
  - you ran `docker logs` -> next time use the **logs** tool. Hand-tailing logs is
    fragile and easy to truncate; the logs tool reads container or file logs with
    consistent paging.
      e.g. logs({server, target:"muralla", lines:80})

The agent doesn't have to read a manual or remember the toolset. The toolset reminds it, at the exact moment it took the lazy path. Rules are pure data (src/hints.ts) — a regex, the better tool, and one sentence of why — so teaching a new better-path is a one-line PR.

Why it's better than what it replaces

  • No timeout wall. Long builds and syncs run with detach:true and are tracked by job. The old failure mode — a hard ~45s cutoff that killed the call while the remote process kept running — is gone.
  • bash, not dash. Commands run under bash -lc by default, so ${var:0:6}, [[ ]], and process substitution just work. No heredoc gymnastics.
  • A real registry. Hosts are named, aliased, tiered (prod/dev/bastion), and policed (full/restricted/readonly). A readonly host refuses destructive commands; a restricted host runs only allow-listed ones.
  • Self-documenting. The hint engine turns 37-tools-you-ignore into 7-tools-you-use.

Quickstart

git clone <repo> kua-ssh && cd kua-ssh
npm install
cp servers.example.json servers.json   # edit to taste
npm run typecheck                       # optional
npm start                               # speaks MCP over stdio

Register it with your MCP client (Claude Desktop / Cowork shown):

{
  "mcpServers": {
    "kua-ssh": {
      "command": "node",
      "args": ["--import", "tsx", "/abs/path/to/kua-ssh/src/index.ts"],
      "env": { "KUA_SSH_SERVERS": "/abs/path/to/kua-ssh/servers.json" }
    }
  }
}

It uses your existing system ssh/scp and keys — nothing new to provision.

servers.json

{
  "requireTailnet": true,
  "servers": [
    { "name": "sol-prod", "aliases": ["solprod","prod"], "host": "100.64.x.x",
      "user": "root", "tier": "prod", "policy": "full" },
    { "name": "sol-dev", "aliases": ["soldev","dev"], "host": "100.71.x.x",
      "user": "kavi", "tier": "dev", "policy": "full" }
  ],
  "databases": [
    { "name": "app-db", "server": "sol-prod", "container": "app-db",
      "engine": "postgres" }
  ]
}

Tailnet-only

kua-ssh refuses to connect to anything that isn't a Tailscale address — an IPv4 in 100.64.0.0/10, an IPv6 in fd7a:115c:a1e0::/48, or a *.ts.net MagicDNS name. A public IP in servers.json is rejected before a socket is opened. Set "requireTailnet": false at the top of the registry to disable, or "allowNonTailnet": true on a single host for break-glass.

This only governs what kua-ssh will dial. For it to actually reach those addresses, the process must run on a tailnet node:

  • Run it on a machine that's already on the tailnet (your workstation, a bastion) — nothing else to do.
  • Run it in a container/sandbox that isn't on the tailnet — attach that runtime to Tailscale and point kua-ssh at it: a tailscaled sidecar, or userspace Tailscale with a SOCKS5 proxy (tailscale up --socks5-server=localhost:1055), then set KUA_SSH_PROXY_COMMAND="nc -X 5 -x 127.0.0.1:1055 %h %p" so every ssh/scp is dialed through the tailnet.

A note on where this should run

kua-ssh is the tool an agent uses to reach your servers — so two deployment rules matter:

  1. Don't run it on a host it isn't allowed to manage. If your design says the dev box can't reach prod, then don't run the prod-managing instance on the dev box. Run it from a control point allowed to reach every tier (your workstation, or a dedicated bastion). Develop anywhere; run it where the trust boundary says it belongs.
  2. Don't edit the instance you depend on. Ship changes as a second connector, prove them, then retire the old one. Breaking the SSH tool from inside the SSH tool is a bad afternoon.

v2: kua-control

A remote, OAuth-gated sibling for "manage the fleet from any browser" — with short sessions, continuous/step-up authentication, per-action authorization, ephemeral SSH certificates, and approvals for prod. It wraps this v1 tool core rather than replacing it. See DESIGN-v2-oauth-gateway.md.

Status

Early. The exec / job / logs / transfer / health / db core is here and typechecks against @modelcontextprotocol/sdk ^1.26. The hint engine is intentionally the most finished part. PRs that add hint rules are the most welcome kind.

MIT.