@akms/mcp-ssh 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -4
- package/lib/cli.cjs +2629 -248
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +2626 -245
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +340 -89
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +58 -5
- package/lib/index.d.ts +58 -5
- package/lib/index.js +339 -88
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,37 @@ npx -y @akms/mcp-ssh --help # or run it straight from the registry
|
|
|
15
15
|
|
|
16
16
|
## ⚙️ MCP client setup
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
**Let the CLI write it.** `config` opens a small manager for the hosts registered in a client's config. It asks plain questions — host, user, key or password, jump host, read-only — tests the connection, offers to pin the host key, and writes the entry. No `SSH_*` name has to be remembered.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx -y @akms/mcp-ssh config # opens on the first client found on this machine
|
|
22
|
+
npx -y @akms/mcp-ssh config --target codex # claude-code | codex | claude-desktop | cursor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Claude Code (user scope) /home/me/.claude.json
|
|
27
|
+
|
|
28
|
+
? 1 SSH host — pick it to manage it
|
|
29
|
+
❯ ssh-prod deploy@10.0.0.5:22 key · readonly, pinned
|
|
30
|
+
────────────────────────
|
|
31
|
+
Add a host
|
|
32
|
+
Switch client also found: Codex CLI
|
|
33
|
+
Quit
|
|
34
|
+
↑↓ move · enter select · esc back
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The hosts are the menu: pick one to edit, check, print or remove it. Choices are picked with the arrow keys — the private key from the keys found in `~/.ssh`, the route as "directly / through a jump host / through a proxy command". Where the terminal cannot send arrow keys (a pipe, Git Bash's mintty), the same lists are numbered and answered by number.
|
|
38
|
+
|
|
39
|
+
| Client | Config file | How it is written |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Claude Code | `~/.claude.json` (user scope) | through `claude mcp add-json … -s user`, never by editing the file |
|
|
42
|
+
| Codex | `~/.codex/config.toml` | only the `[mcp_servers.<name>]` tables are rewritten; keys you added to them stay |
|
|
43
|
+
| Claude Desktop | `claude_desktop_config.json` | `mcpServers.<name>` is set, every other key kept |
|
|
44
|
+
| Cursor | `~/.cursor/mcp.json` | same |
|
|
45
|
+
|
|
46
|
+
Only entries that launch this package are listed or touched. An edit keeps every `SSH_*` variable the wizard does not ask about (timeouts, deny patterns, `SSH_CWD`, …). **Print** shows an entry as JSON, as a Codex TOML table and as a `claude mcp add-json` line, for when pasting is easier. On Windows the entry launches through `cmd /c npx`, the form Claude Code expects for npx servers there.
|
|
47
|
+
|
|
48
|
+
**Or by hand — one server entry per host** — plain `SSH_*` variables, no config file, no JSON inside JSON. Claude Code reads `.mcp.json` in the project or `~/.claude.json` globally; Claude Desktop takes the same block inside `claude_desktop_config.json`:
|
|
19
49
|
|
|
20
50
|
```json
|
|
21
51
|
{
|
|
@@ -54,9 +84,50 @@ Every setting is an environment variable in this server's own entry. There is no
|
|
|
54
84
|
| `SSH_NAME` | Alias shown to the agent; defaults to the hostname |
|
|
55
85
|
| `SSH_DESCRIPTION` | Shown to the agent — say what the host is for |
|
|
56
86
|
| `SSH_CWD` | Directory new sessions start in |
|
|
87
|
+
| `SSH_PROXY_COMMAND` | Command whose stdin/stdout carry the SSH stream — OpenSSH's `ProxyCommand`. `%h` / `%p` / `%r` expand to the first hop's host, port and user, `%%` to a literal `%` |
|
|
88
|
+
| `SSH_JUMP_HOST` | Jump host the target is reached through — OpenSSH's `ProxyJump` / `-J` |
|
|
89
|
+
| `SSH_JUMP_PORT` | Default `22` |
|
|
90
|
+
| `SSH_JUMP_USER` | Defaults to `SSH_USER` |
|
|
91
|
+
| `SSH_JUMP_KEY` | **Path** to the jump host's private key; defaults to `SSH_KEY` (and then shares `SSH_PASSPHRASE`) |
|
|
92
|
+
| `SSH_JUMP_PASSPHRASE` | Passphrase for `SSH_JUMP_KEY` |
|
|
93
|
+
| `SSH_JUMP_PASSWORD` | Password auth on the jump host |
|
|
94
|
+
| `SSH_JUMP_HOST_KEY` | Pin the jump host's key, `SHA256:…` |
|
|
57
95
|
|
|
58
96
|
Authentication is tried in this order: **private key by path** (with `SSH_PASSPHRASE` if encrypted), **password**, then the **SSH agent** when neither is set (`SSH_AUTH_SOCK`, or the OpenSSH named pipe on Windows). A key path that doesn't exist is reported at startup, not at first connection.
|
|
59
97
|
|
|
98
|
+
### Through a jump host or a proxy command
|
|
99
|
+
|
|
100
|
+
`~/.ssh/config` is **not** read — the server never shells out to `ssh` — so a `ProxyJump` or `ProxyCommand` that works on the command line has to be stated in the entry. Both OpenSSH shapes are available, and they compose:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"ssh-dev": {
|
|
104
|
+
"command": "npx", "args": ["-y", "@akms/mcp-ssh"],
|
|
105
|
+
"env": {
|
|
106
|
+
"SSH_HOST": "10.0.16.12", "SSH_USER": "ubuntu", "SSH_KEY": "~/.ssh/id_ed25519",
|
|
107
|
+
"SSH_JUMP_HOST": "dev-ak-ssh.example.com",
|
|
108
|
+
"SSH_PROXY_COMMAND": "cloudflared access ssh --hostname %h"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **`SSH_JUMP_HOST`** connects to the jump host first and opens a forwarded TCP channel to `SSH_HOST:SSH_PORT`, which the target handshake then runs over — exactly what `ssh -J` does. The jump host authenticates in the same order from `SSH_JUMP_*` — user and key fall back to the target's, the password does not — and its host key can be pinned with `SSH_JUMP_HOST_KEY`. Any `SSH_JUMP_*` set without `SSH_JUMP_HOST` fails startup rather than quietly connecting straight to the target.
|
|
114
|
+
- **`SSH_PROXY_COMMAND`** starts the command through the shell and talks SSH over its stdin/stdout, as `ProxyCommand` does — `cloudflared access ssh --hostname %h`, `ssh -W %h:%p bastion`, `nc %h %p`. It carries the **first hop**: the jump host when one is set, otherwise the target. The entry above therefore reads "cloudflared to the jump host, then forward to 10.0.16.12".
|
|
115
|
+
- Either alone works too: a jump host reachable directly needs only `SSH_JUMP_HOST`; a host behind a Cloudflare Access tunnel needs only `SSH_PROXY_COMMAND`.
|
|
116
|
+
|
|
117
|
+
The proxy process lives exactly as long as the connection it carries and is killed with it (the whole process tree on Windows). Its stderr goes to the log at `warn`, which is where a `cloudflared` login prompt or a refused tunnel shows up. Log in once with `cloudflared access login <url>` before wiring the entry up: the server cannot open a browser for you, and `SSH_CONNECT_TIMEOUT_MS` expires first.
|
|
118
|
+
|
|
119
|
+
`--check` prints the route and both host key fingerprints, so the jump host's key can be pinned the same way:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Checking 10.0.16.12 → ubuntu@10.0.16.12:22
|
|
123
|
+
via proxy command: cloudflared access ssh --hostname %h
|
|
124
|
+
via jump host: ubuntu@dev-ak-ssh.example.com:22
|
|
125
|
+
|
|
126
|
+
OK ubuntu@10.0.16.12:22 (key /home/me/.ssh/id_ed25519, 1840ms)
|
|
127
|
+
host key SHA256:9x2K… (not pinned — set SSH_HOST_KEY to it to detect a changed key)
|
|
128
|
+
jump host key SHA256:Qm7d… (not pinned — set SSH_JUMP_HOST_KEY to it to detect a changed key)
|
|
129
|
+
```
|
|
130
|
+
|
|
60
131
|
### Policy
|
|
61
132
|
|
|
62
133
|
| `SSH_*` variable | Default | Effect |
|
|
@@ -100,7 +171,7 @@ Add a server entry per host. Each one carries its own connection and its own pol
|
|
|
100
171
|
akms-mcp-ssh --check # or: npx -y @akms/mcp-ssh --check
|
|
101
172
|
```
|
|
102
173
|
|
|
103
|
-
Connects to the configured host and reports what happened — exit code 0 when it is reachable. Otherwise a setup mistake is indistinguishable from a policy rejection when the agent tries something.
|
|
174
|
+
Connects to the configured host and reports what happened — exit code 0 when it is reachable. Otherwise a setup mistake is indistinguishable from a policy rejection when the agent tries something. Warnings still print during the check (a missing key file, the proxy command's stderr); add `SSH_MCP_LOG_LEVEL=debug` to see the handshake.
|
|
104
175
|
|
|
105
176
|
```
|
|
106
177
|
Checking prod-web → deploy@10.0.0.5:22
|
|
@@ -115,7 +186,7 @@ That fingerprint is the same string `ssh-keyscan -t ed25519 host | ssh-keygen -l
|
|
|
115
186
|
|
|
116
187
|
| Tool | Purpose |
|
|
117
188
|
|---|---|
|
|
118
|
-
| `ssh_list_hosts` | The configured host and its guard policy. No credentials |
|
|
189
|
+
| `ssh_list_hosts` | The configured host, its route and its guard policy. No credentials |
|
|
119
190
|
| `ssh_connect` | Open a reusable session, returns a session id |
|
|
120
191
|
| `ssh_disconnect` | Close a session |
|
|
121
192
|
| `ssh_list_sessions` | Open sessions with cwd, command count and idle time |
|
|
@@ -176,7 +247,7 @@ Everything goes to **stderr** (stdout is the MCP protocol channel). `SSH_MCP_LOG
|
|
|
176
247
|
|
|
177
248
|
**The real boundary is the remote account.** The guards stop mistakes; containment has to come from the host — give each server entry a dedicated unprivileged user rather than `root`, restrict its `sudoers` entries to the exact commands it needs, pair `SSH_READONLY=true` with filesystem permissions that agree with it, reach for `SSH_ALLOW_COMMANDS` on the strictest hosts, and back up anything you would miss.
|
|
178
249
|
|
|
179
|
-
- The MCP client entry names the host and may hold `SSH_PASSWORD` or `SSH_PASSPHRASE
|
|
250
|
+
- The MCP client entry names the host and may hold `SSH_PASSWORD` or `SSH_PASSPHRASE` (and their `SSH_JUMP_*` counterparts). Prefer a key file so only its *path* is stored, keep the entry out of version control, and `chmod 600` it — the shipped `.gitignore` already excludes `*.pem`, `id_rsa*` and `id_ed25519*`.
|
|
180
251
|
- **Anything the agent can reach, a prompt injection can reach.** Output from a remote host is untrusted input; a file the agent reads can contain instructions. With permissive defaults that matters more, not less — keep a production entry `SSH_READONLY=true` and let write access be a deliberate exception.
|
|
181
252
|
- The catastrophe rules are the *only* ones that hold regardless of configuration, and even they read the command as text. Treat them as a seatbelt, not a cage.
|
|
182
253
|
- Point this at hosts you are authorized to administer. This is an administration tool, not a scanner.
|