@yawlabs/ssh-mcp 0.15.0 → 0.15.2
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 +28 -0
- package/bin/ssh-mcp.mjs +54 -4
- package/package.json +19 -8
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@yawlabs/ssh-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://x.com/TokenLimitNews)
|
|
5
6
|
|
|
6
7
|
**Make SSH work for AI tools.** MCP server that manages your SSH environment, diagnoses what's broken, fixes it, and gives your agent remote access to anything.
|
|
7
8
|
|
|
@@ -302,10 +303,37 @@ for (const check of report.checks) {
|
|
|
302
303
|
}
|
|
303
304
|
```
|
|
304
305
|
|
|
306
|
+
## Runtime selection
|
|
307
|
+
|
|
308
|
+
The `ssh-mcp` command — what `npx @yawlabs/ssh-mcp` runs — is a small launcher. It prefers the [oam](https://oamjs.org) runtime and otherwise runs the server on Node, so oam is optional. Two environment variables control the choice; set them in your MCP client's `env` block:
|
|
309
|
+
|
|
310
|
+
- `SSH_MCP_RUNTIME` — which runtime to use. Case-insensitive; any value other than `oam` or `node` behaves as `auto`.
|
|
311
|
+
- `auto` (default) — use oam if it is usable, otherwise fall back to Node.
|
|
312
|
+
- `oam` — require oam. Fails loudly: if oam is missing or unusable, the launcher prints the reason to stderr and exits with status 1 instead of starting the server.
|
|
313
|
+
- `node` — never use oam. The launcher does not look for it, and `OAM_BIN` is ignored.
|
|
314
|
+
- `OAM_BIN` — path to a specific oam binary. When set to a non-empty value it wins over discovery and nothing else is searched, so a path that does not exist counts as no oam found even if a working oam is installed elsewhere. Without it, the launcher takes the first oam it finds in `%LOCALAPPDATA%\oam\bin` (Windows only), then `~/.oam/bin`, then `PATH`, and does not keep looking if that one is unusable.
|
|
315
|
+
|
|
316
|
+
oam must be **0.9.0 or newer**. By default an unusable oam is not an error: in `auto` mode the launcher falls back to Node — silently when no oam is found (including an `OAM_BIN` path that does not exist), or with a note on stderr when it finds one it cannot use: older than 0.9.0, not runnable, or on Windows only an `oam.cmd`/`oam.bat` shim on `PATH`. With `SSH_MCP_RUNTIME=oam`, each of these cases exits with status 1 instead.
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"mcpServers": {
|
|
321
|
+
"ssh": {
|
|
322
|
+
"command": "npx",
|
|
323
|
+
"args": ["-y", "@yawlabs/ssh-mcp@latest"],
|
|
324
|
+
"env": { "SSH_MCP_RUNTIME": "node" }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
On Windows, add the same `env` block to the `cmd /c` form from [Quick start](#quick-start).
|
|
331
|
+
|
|
305
332
|
## Requirements
|
|
306
333
|
|
|
307
334
|
- Node.js 18+
|
|
308
335
|
- SSH client installed (for diagnostics and environment management)
|
|
336
|
+
- Optional: [oam](https://oamjs.org) 0.9.0+ — see [Runtime selection](#runtime-selection)
|
|
309
337
|
|
|
310
338
|
## License
|
|
311
339
|
|
package/bin/ssh-mcp.mjs
CHANGED
|
@@ -22,6 +22,23 @@
|
|
|
22
22
|
* For an MCP host config, point straight at oam and skip this file:
|
|
23
23
|
* { "command": "oam", "args": ["run", "<abs>/dist/index.js"] }
|
|
24
24
|
*
|
|
25
|
+
* ALREADY RUNNING ON OAM
|
|
26
|
+
* A host can resolve this package's `bin` and launch `oam run <this file>`
|
|
27
|
+
* instead of `node <this file>` -- Yaw MCP does, and so does oam's sidecar
|
|
28
|
+
* regression matrix. This launcher used to discover oam and spawn it anyway,
|
|
29
|
+
* so one server cost two runtime boots: measured on Windows, oam.exe with a
|
|
30
|
+
* NESTED oam.exe + conhost.exe underneath it. Now, when `process.versions.oam`
|
|
31
|
+
* clears the same MINIMUM OAM VERSION a discovered binary has to, the server is
|
|
32
|
+
* imported into THIS process exactly as the Node fallback is -- no discovery,
|
|
33
|
+
* no `oam --version` probe, no second oam. OAM_BIN is a discovery input, so it
|
|
34
|
+
* is not consulted on that path: the host has already chosen which oam runs.
|
|
35
|
+
*
|
|
36
|
+
* A host oam below the floor still takes the discovery path exactly as it
|
|
37
|
+
* always did. Nothing else has to keep spawning on oam: the spawn below passes
|
|
38
|
+
* oam no runtime flags -- there is no `--permission` sandbox to apply, see NO
|
|
39
|
+
* SANDBOX HERE -- so serving in-process drops nothing a fresh oam would have
|
|
40
|
+
* applied.
|
|
41
|
+
*
|
|
25
42
|
* NO SANDBOX HERE -- DELIBERATELY
|
|
26
43
|
* The purpose of this server is to open outbound SSH to hosts the caller names
|
|
27
44
|
* at run time and run commands there, so the net and child-process grants would
|
|
@@ -41,6 +58,7 @@
|
|
|
41
58
|
*
|
|
42
59
|
* SELECTION
|
|
43
60
|
* SSH_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
61
|
+
* (already running on oam satisfies it)
|
|
44
62
|
* SSH_MCP_RUNTIME=node never use oam
|
|
45
63
|
* SSH_MCP_RUNTIME=auto prefer oam, silently fall back (default)
|
|
46
64
|
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
|
|
@@ -158,17 +176,27 @@ async function errSync(message) {
|
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
/**
|
|
161
|
-
*
|
|
179
|
+
* Version text -> [major, minor, patch], or null when it holds no version.
|
|
162
180
|
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
181
|
+
*
|
|
182
|
+
* Shared by the two places a version is read -- a discovered binary's
|
|
183
|
+
* `oam --version` output ("oam 0.15.1") and the host's own
|
|
184
|
+
* `process.versions.oam` ("0.15.1") -- so they cannot disagree about what a
|
|
185
|
+
* version string means, or which floor it has to clear.
|
|
163
186
|
*/
|
|
187
|
+
function parseVersion(text) {
|
|
188
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
189
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `oam --version` -> [major, minor, patch], or null when it cannot be read. */
|
|
164
193
|
function oamVersion(cmd) {
|
|
165
194
|
try {
|
|
166
195
|
const out = execFileSync(cmd, ["--version"], {
|
|
167
196
|
encoding: "utf-8",
|
|
168
197
|
stdio: ["ignore", "pipe", "ignore"],
|
|
169
198
|
});
|
|
170
|
-
|
|
171
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
199
|
+
return parseVersion(out);
|
|
172
200
|
} catch {
|
|
173
201
|
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
174
202
|
return null;
|
|
@@ -185,6 +213,27 @@ function atLeast(v, min) {
|
|
|
185
213
|
return true;
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Where the server runs, decided BEFORE any discovery:
|
|
218
|
+
* "in-process" import it into THIS process
|
|
219
|
+
* "discover" find an oam binary, gate its version, spawn it -- or fall
|
|
220
|
+
* back to Node in-process when that fails
|
|
221
|
+
*
|
|
222
|
+
* `hostOam` is `process.versions.oam`: oam's own key, absent on Node, so on
|
|
223
|
+
* Node every mode but `node` is the discovery path it always was. The floor is
|
|
224
|
+
* OAM_MIN itself, not a parameter, so a host oam and a discovered one can never
|
|
225
|
+
* be held to different minimums. There is no sandbox input because this
|
|
226
|
+
* launcher has no sandbox; see ALREADY RUNNING ON OAM above for why nothing
|
|
227
|
+
* else forces a spawn.
|
|
228
|
+
*
|
|
229
|
+
* Pure on purpose: every input is passed in, so the whole decision is testable
|
|
230
|
+
* without booting a runtime.
|
|
231
|
+
*/
|
|
232
|
+
function runtimePlan({ mode, hostOam }) {
|
|
233
|
+
if (mode === "node") return "in-process";
|
|
234
|
+
return atLeast(parseVersion(hostOam ?? ""), OAM_MIN) ? "in-process" : "discover";
|
|
235
|
+
}
|
|
236
|
+
|
|
188
237
|
/** Run the server in THIS process. The zero-overhead fallback. */
|
|
189
238
|
async function runInProcess() {
|
|
190
239
|
// A server may gate its bootstrap on being the process ENTRY POINT --
|
|
@@ -202,8 +251,9 @@ async function runInProcess() {
|
|
|
202
251
|
}
|
|
203
252
|
|
|
204
253
|
const mode = (process.env.SSH_MCP_RUNTIME ?? "auto").toLowerCase();
|
|
254
|
+
const plan = runtimePlan({ mode, hostOam: process.versions.oam });
|
|
205
255
|
|
|
206
|
-
if (
|
|
256
|
+
if (plan === "in-process") {
|
|
207
257
|
await runInProcess();
|
|
208
258
|
} else {
|
|
209
259
|
const { path: oam, shim: oamShim } = findOam();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/ssh-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"mcpName": "io.github.YawLabs/ssh-mcp",
|
|
5
|
-
"description": "MCP server
|
|
5
|
+
"description": "SSH MCP server: run remote commands, transfer files over SFTP, manage ssh-agent keys and known_hosts, and auto-diagnose SSH failures.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ssh-mcp": "bin/ssh-mcp.mjs"
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
24
24
|
"dev": "tsup --watch",
|
|
25
|
-
"lint": "
|
|
26
|
-
"lint:fix": "
|
|
25
|
+
"lint": "node scripts/lint.mjs check src/",
|
|
26
|
+
"lint:fix": "node scripts/lint.mjs check --write src/",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
28
|
"test": "vitest run",
|
|
29
29
|
"test:integration": "docker compose -f test/docker/docker-compose.yml up -d --build --wait && SSH_MCP_INTEGRATION=1 vitest run src/tests/integration.test.ts; docker compose -f test/docker/docker-compose.yml down",
|
|
@@ -31,12 +31,22 @@
|
|
|
31
31
|
"prepublishOnly": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
|
-
"mcp",
|
|
35
34
|
"ssh",
|
|
36
|
-
"
|
|
35
|
+
"mcp",
|
|
37
36
|
"model-context-protocol",
|
|
37
|
+
"mcp-server",
|
|
38
|
+
"sftp",
|
|
39
|
+
"ssh-agent",
|
|
40
|
+
"ssh-keys",
|
|
41
|
+
"known-hosts",
|
|
42
|
+
"ssh-config",
|
|
43
|
+
"proxyjump",
|
|
44
|
+
"remote",
|
|
45
|
+
"remote-execution",
|
|
46
|
+
"diagnostics",
|
|
47
|
+
"devops",
|
|
38
48
|
"ai",
|
|
39
|
-
"
|
|
49
|
+
"ai-agents"
|
|
40
50
|
],
|
|
41
51
|
"author": "Yaw Labs <contact@yaw.sh>",
|
|
42
52
|
"license": "MIT",
|
|
@@ -65,5 +75,6 @@
|
|
|
65
75
|
},
|
|
66
76
|
"overrides": {
|
|
67
77
|
"esbuild": "^0.28.1"
|
|
68
|
-
}
|
|
78
|
+
},
|
|
79
|
+
"homepage": "https://yaw.sh/mcp-servers/ssh-mcp/"
|
|
69
80
|
}
|