@yawlabs/ssh-mcp 0.13.0 → 0.14.0
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/bin/ssh-mcp.mjs +68 -1
- package/package.json +1 -1
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
|
+
* NO SANDBOX HERE -- DELIBERATELY
|
|
26
|
+
* The purpose of this server is to open outbound SSH to hosts the caller names
|
|
27
|
+
* at run time and run commands there, so the net and child-process grants would
|
|
28
|
+
* both have to be unrestricted, and key material plus known_hosts need the
|
|
29
|
+
* filesystem. Nothing meaningful is left to deny, so `--permission` is not
|
|
30
|
+
* wired up here.
|
|
31
|
+
*
|
|
32
|
+
* MINIMUM OAM VERSION
|
|
33
|
+
* 0.9.0. Below it `child_process.execFile` ran its arguments through a SHELL,
|
|
34
|
+
* `exec` accepted `timeout` and ignored it, `spawnSync` truncated at
|
|
35
|
+
* `maxBuffer` while reporting success, and `stdio: 'inherit'`/`'ignore'` both
|
|
36
|
+
* behaved as `'pipe'`. This server shells out to a CLI on its
|
|
37
|
+
* main paths, so those were reachable bugs rather than theoretical ones: an
|
|
38
|
+
* argument containing shell metacharacters was re-split and executed.
|
|
39
|
+
* An older oam is not an error: the launcher falls back to Node and says so on
|
|
40
|
+
* stderr. Pinning the floor here is what makes that fallback automatic.
|
|
41
|
+
*
|
|
25
42
|
* SELECTION
|
|
26
43
|
* SSH_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
27
44
|
* SSH_MCP_RUNTIME=node never use oam
|
|
@@ -29,12 +46,15 @@
|
|
|
29
46
|
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
|
|
30
47
|
*/
|
|
31
48
|
|
|
32
|
-
import { spawn } from "node:child_process";
|
|
49
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
33
50
|
import { existsSync } from "node:fs";
|
|
34
51
|
import { constants, homedir } from "node:os";
|
|
35
52
|
import { delimiter, join } from "node:path";
|
|
36
53
|
import { fileURLToPath } from "node:url";
|
|
37
54
|
|
|
55
|
+
/** Oldest oam whose `child_process` matches Node. See MINIMUM OAM VERSION above. */
|
|
56
|
+
const OAM_MIN = [0, 9, 0];
|
|
57
|
+
|
|
38
58
|
// Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
|
|
39
59
|
// with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
|
|
40
60
|
// in-process fallback must use the file:// URL. spawn() needs a real path.
|
|
@@ -83,6 +103,34 @@ function findOam() {
|
|
|
83
103
|
return null;
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
/**
|
|
107
|
+
* `oam --version` -> [major, minor, patch], or null when it cannot be read.
|
|
108
|
+
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
109
|
+
*/
|
|
110
|
+
function oamVersion(cmd) {
|
|
111
|
+
try {
|
|
112
|
+
const out = execFileSync(cmd, ["--version"], {
|
|
113
|
+
encoding: "utf-8",
|
|
114
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
115
|
+
});
|
|
116
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
|
|
117
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
118
|
+
} catch {
|
|
119
|
+
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** True when `v` is at least `min`, comparing major/minor/patch in order. */
|
|
125
|
+
function atLeast(v, min) {
|
|
126
|
+
if (!v) return false;
|
|
127
|
+
for (let i = 0; i < min.length; i++) {
|
|
128
|
+
if (v[i] > min[i]) return true;
|
|
129
|
+
if (v[i] < min[i]) return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
86
134
|
/** Run the server in THIS process. The zero-overhead fallback. */
|
|
87
135
|
async function runInProcess() {
|
|
88
136
|
// A server may gate its bootstrap on being the process ENTRY POINT --
|
|
@@ -120,6 +168,25 @@ if (mode === "node") {
|
|
|
120
168
|
process.exit(1);
|
|
121
169
|
}
|
|
122
170
|
await runInProcess();
|
|
171
|
+
} else if (!atLeast(oamVersion(oam), OAM_MIN)) {
|
|
172
|
+
// Discovery itself stays stat-only; this is the first subprocess, and it
|
|
173
|
+
// runs only once we have already decided to spawn oam anyway. Measured 26ms
|
|
174
|
+
// median (n=12, windows-arm64), paid once per MCP session.
|
|
175
|
+
const min = OAM_MIN.join(".");
|
|
176
|
+
if (mode === "oam") {
|
|
177
|
+
const { writeSync } = await import("node:fs");
|
|
178
|
+
writeSync(
|
|
179
|
+
2,
|
|
180
|
+
`ssh-mcp: SSH_MCP_RUNTIME=oam but ${oam} is older than oam ${min}.\n` +
|
|
181
|
+
`Run \`oam self-update\`, or use SSH_MCP_RUNTIME=node.\n`,
|
|
182
|
+
);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
// auto: an old oam is a reason to prefer Node, not to fail. Say so, because
|
|
186
|
+
// a silent downgrade is how someone keeps running an oam they meant to
|
|
187
|
+
// update. stderr is safe -- MCP frames travel on stdout.
|
|
188
|
+
process.stderr.write(`ssh-mcp: oam at ${oam} is older than ${min}; using Node instead.\n`);
|
|
189
|
+
await runInProcess();
|
|
123
190
|
} else {
|
|
124
191
|
// `--` separates oam's own flags from the script's argv, so `ssh-mcp
|
|
125
192
|
// --version` and any host-supplied flags survive the hop unchanged.
|