@yawlabs/ssh-mcp 0.9.1 → 0.9.3
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/dist/index.js +48 -19
- package/dist/server.d.ts +20 -1
- package/dist/server.js +48 -19
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -202,8 +202,9 @@ function checkSshConfig(host) {
|
|
|
202
202
|
inHostBlock = patterns.some((p) => {
|
|
203
203
|
if (p === "*") return true;
|
|
204
204
|
if (p === host) return true;
|
|
205
|
-
if (p.includes("*")) {
|
|
206
|
-
const
|
|
205
|
+
if (p.includes("*") || p.includes("?")) {
|
|
206
|
+
const escaped = p.replace(/[\\^$.|+()[\]{}]/g, "\\$&");
|
|
207
|
+
const regex = new RegExp("^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
|
|
207
208
|
return regex.test(host);
|
|
208
209
|
}
|
|
209
210
|
return false;
|
|
@@ -339,9 +340,16 @@ function ensureAgent() {
|
|
|
339
340
|
const result = probeAgent(sock, "ssh-agent");
|
|
340
341
|
if (result) return result;
|
|
341
342
|
}
|
|
342
|
-
if (
|
|
343
|
+
if (process.platform === "win32") {
|
|
343
344
|
const result = probeAgent("\\\\.\\pipe\\openssh-ssh-agent", "Windows OpenSSH agent");
|
|
344
345
|
if (result) return result;
|
|
346
|
+
return {
|
|
347
|
+
running: false,
|
|
348
|
+
reachable: false,
|
|
349
|
+
keys: [],
|
|
350
|
+
started: false,
|
|
351
|
+
message: "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
|
|
352
|
+
};
|
|
345
353
|
}
|
|
346
354
|
const { stdout, ok } = runArgs("ssh-agent", ["-s"]);
|
|
347
355
|
if (ok) {
|
|
@@ -369,7 +377,7 @@ function ensureAgent() {
|
|
|
369
377
|
reachable: false,
|
|
370
378
|
keys: [],
|
|
371
379
|
started: false,
|
|
372
|
-
message:
|
|
380
|
+
message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
|
|
373
381
|
};
|
|
374
382
|
}
|
|
375
383
|
function detectKeyType(filePath, fileName) {
|
|
@@ -458,10 +466,10 @@ function loadKey(keyPath) {
|
|
|
458
466
|
if (ok) {
|
|
459
467
|
return { status: "ok", message: `Key loaded: ${resolved}` };
|
|
460
468
|
}
|
|
461
|
-
if (stdout.includes("
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
469
|
+
if (stdout.includes("UNPROTECTED PRIVATE KEY") || stdout.includes("too open") || stdout.includes("bad permissions")) {
|
|
470
|
+
return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
|
|
471
|
+
}
|
|
472
|
+
if (stdout.includes("passphrase") || stdout.includes("incorrect")) {
|
|
465
473
|
return { status: "error", message: `Key ${resolved} requires a passphrase. Add it manually: ssh-add ${resolved}` };
|
|
466
474
|
}
|
|
467
475
|
return { status: "error", message: `Failed to load key: ${stdout}` };
|
|
@@ -749,10 +757,16 @@ async function connectWithProxy(resolved) {
|
|
|
749
757
|
const jumpClient = await connectWithProxy(jumpResolved);
|
|
750
758
|
const targetHost = resolved.connectConfig.host;
|
|
751
759
|
const targetPort = resolved.connectConfig.port;
|
|
760
|
+
const endJump = () => {
|
|
761
|
+
try {
|
|
762
|
+
jumpClient.end();
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
};
|
|
752
766
|
const stream = await new Promise((resolve, reject) => {
|
|
753
767
|
jumpClient.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream2) => {
|
|
754
768
|
if (err) {
|
|
755
|
-
|
|
769
|
+
endJump();
|
|
756
770
|
return reject(err);
|
|
757
771
|
}
|
|
758
772
|
resolve(stream2);
|
|
@@ -761,10 +775,10 @@ async function connectWithProxy(resolved) {
|
|
|
761
775
|
return new Promise((resolve, reject) => {
|
|
762
776
|
const client = new Client();
|
|
763
777
|
client.on("ready", () => resolve(client)).on("error", (err) => {
|
|
764
|
-
|
|
778
|
+
endJump();
|
|
765
779
|
reject(err);
|
|
766
780
|
}).on("close", () => {
|
|
767
|
-
|
|
781
|
+
endJump();
|
|
768
782
|
}).connect({ ...resolved.connectConfig, sock: stream });
|
|
769
783
|
});
|
|
770
784
|
}
|
|
@@ -832,14 +846,19 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
|
|
|
832
846
|
stderrTruncated = true;
|
|
833
847
|
}
|
|
834
848
|
};
|
|
835
|
-
stream.on("close", (code) => {
|
|
849
|
+
stream.on("close", (code, signal) => {
|
|
836
850
|
let stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
837
851
|
let stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
838
852
|
if (stdoutTruncated) stdout += `
|
|
839
853
|
[output truncated at ${maxBytes} bytes]`;
|
|
840
854
|
if (stderrTruncated) stderr += `
|
|
841
855
|
[stderr truncated at ${maxBytes} bytes]`;
|
|
842
|
-
|
|
856
|
+
const exitCode = typeof code === "number" ? code : -1;
|
|
857
|
+
const result = { stdout, stderr, code: exitCode };
|
|
858
|
+
if (stdoutTruncated) result.stdoutTruncated = true;
|
|
859
|
+
if (stderrTruncated) result.stderrTruncated = true;
|
|
860
|
+
if (signal) result.signal = signal;
|
|
861
|
+
settle(() => resolve(result));
|
|
843
862
|
}).on("data", appendStdout).on("error", (err2) => {
|
|
844
863
|
settle(() => reject(err2));
|
|
845
864
|
});
|
|
@@ -936,6 +955,12 @@ async function listDir(client, remotePath) {
|
|
|
936
955
|
}
|
|
937
956
|
|
|
938
957
|
// src/pool.ts
|
|
958
|
+
function defaultMaxPoolSize() {
|
|
959
|
+
const raw = process.env.SSH_MCP_MAX_POOL_SIZE;
|
|
960
|
+
if (!raw) return 100;
|
|
961
|
+
const parsed = Number.parseInt(raw, 10);
|
|
962
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
|
|
963
|
+
}
|
|
939
964
|
var ConnectionPool = class {
|
|
940
965
|
entries = /* @__PURE__ */ new Map();
|
|
941
966
|
// Coalesces concurrent connect attempts for the same key so we don't open N
|
|
@@ -952,7 +977,7 @@ var ConnectionPool = class {
|
|
|
952
977
|
drained = false;
|
|
953
978
|
constructor(options) {
|
|
954
979
|
this.idleTtlMs = options?.idleTtlMs ?? 6e4;
|
|
955
|
-
this.maxPoolSize = options?.maxPoolSize ??
|
|
980
|
+
this.maxPoolSize = options?.maxPoolSize ?? defaultMaxPoolSize();
|
|
956
981
|
}
|
|
957
982
|
async acquire(config) {
|
|
958
983
|
const resolved = resolveConfig(config);
|
|
@@ -1202,6 +1227,7 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
1202
1227
|
const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
|
|
1203
1228
|
const pidMatch = raw.match(/Main PID:\s+(\d+)/);
|
|
1204
1229
|
const sinceMatch = raw.match(/since\s+(.+?);/);
|
|
1230
|
+
const unknown = !activeMatch && result.code !== 0;
|
|
1205
1231
|
const fallbackStatus = result.code === 0 ? "active" : "inactive";
|
|
1206
1232
|
return {
|
|
1207
1233
|
name: serviceName,
|
|
@@ -1210,7 +1236,8 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
1210
1236
|
description: descMatch?.[1]?.trim(),
|
|
1211
1237
|
since: sinceMatch?.[1]?.trim(),
|
|
1212
1238
|
pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
|
|
1213
|
-
raw
|
|
1239
|
+
raw,
|
|
1240
|
+
unknown
|
|
1214
1241
|
};
|
|
1215
1242
|
}
|
|
1216
1243
|
|
|
@@ -1247,6 +1274,7 @@ function registerTools(server, pool) {
|
|
|
1247
1274
|
if (result.stdout) parts.push(result.stdout);
|
|
1248
1275
|
if (result.stderr) parts.push(`[stderr]
|
|
1249
1276
|
${result.stderr}`);
|
|
1277
|
+
if (result.signal) parts.push(`[signal: ${result.signal}]`);
|
|
1250
1278
|
parts.push(`[exit code: ${result.code}]`);
|
|
1251
1279
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1252
1280
|
});
|
|
@@ -1523,11 +1551,12 @@ ${result.stderr}`);
|
|
|
1523
1551
|
maxdepth: z.number().optional().describe("Maximum directory depth to search"),
|
|
1524
1552
|
minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
|
|
1525
1553
|
maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
|
|
1554
|
+
newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
|
|
1526
1555
|
timeout: TimeoutSchema
|
|
1527
1556
|
},
|
|
1528
|
-
async ({ path, name, type, maxdepth, minsize, maxsize, timeout, ...conn }) => {
|
|
1557
|
+
async ({ path, name, type, maxdepth, minsize, maxsize, newer, timeout, ...conn }) => {
|
|
1529
1558
|
return connectionPool.withConnection(conn, async (client) => {
|
|
1530
|
-
const files = await find(client, { path, name, type, maxdepth, minsize, maxsize }, timeout || 3e4);
|
|
1559
|
+
const files = await find(client, { path, name, type, maxdepth, minsize, maxsize, newer }, timeout || 3e4);
|
|
1531
1560
|
if (files.length === 0) {
|
|
1532
1561
|
return { content: [{ type: "text", text: "No files found." }] };
|
|
1533
1562
|
}
|
|
@@ -1542,7 +1571,7 @@ ${files.join("\n")}` }] };
|
|
|
1542
1571
|
{
|
|
1543
1572
|
...connectionParams,
|
|
1544
1573
|
path: z.string().describe("Absolute path to the file to tail"),
|
|
1545
|
-
lines: z.number().optional().describe("Number of lines to read from the end (default: 100)"),
|
|
1574
|
+
lines: z.number().int().positive().optional().describe("Number of lines to read from the end (default: 100). Must be a positive integer."),
|
|
1546
1575
|
grep: z.string().optional().describe("Case-insensitive pattern to filter lines"),
|
|
1547
1576
|
timeout: TimeoutSchema
|
|
1548
1577
|
},
|
|
@@ -1582,7 +1611,7 @@ ${files.join("\n")}` }] };
|
|
|
1582
1611
|
if (status.since) lines.push(`Since: ${status.since}`);
|
|
1583
1612
|
lines.push("");
|
|
1584
1613
|
lines.push(status.raw);
|
|
1585
|
-
return { content: [{ type: "text", text: lines.join("\n") }], isError:
|
|
1614
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: status.unknown };
|
|
1586
1615
|
});
|
|
1587
1616
|
}
|
|
1588
1617
|
);
|
package/dist/server.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ interface ExecResult {
|
|
|
13
13
|
stdout: string;
|
|
14
14
|
stderr: string;
|
|
15
15
|
code: number;
|
|
16
|
+
/** True when stdout was truncated at the byte cap. */
|
|
17
|
+
stdoutTruncated?: boolean;
|
|
18
|
+
/** True when stderr was truncated at the byte cap. */
|
|
19
|
+
stderrTruncated?: boolean;
|
|
20
|
+
/** Signal name (e.g. "TERM") if the remote channel closed via signal instead of exit. */
|
|
21
|
+
signal?: string;
|
|
16
22
|
}
|
|
17
23
|
interface ResolvedConfig {
|
|
18
24
|
connectConfig: ConnectConfig;
|
|
@@ -34,7 +40,13 @@ declare function listDir(client: Client, remotePath: string): Promise<string[]>;
|
|
|
34
40
|
interface PoolOptions {
|
|
35
41
|
/** Milliseconds before an idle connection is closed. Default: 60000 (60s) */
|
|
36
42
|
idleTtlMs?: number;
|
|
37
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of connections in the pool. Default: 100, overridable via the
|
|
45
|
+
* `SSH_MCP_MAX_POOL_SIZE` env var. When at capacity, the pool first tries to evict
|
|
46
|
+
* an idle entry; if every entry is in use, `acquire()` rejects with
|
|
47
|
+
* "Connection pool is full". Bump this for fan-out workloads against many distinct
|
|
48
|
+
* hosts (e.g. `ssh_multi_exec` across a large fleet).
|
|
49
|
+
*/
|
|
38
50
|
maxPoolSize?: number;
|
|
39
51
|
}
|
|
40
52
|
declare class ConnectionPool {
|
|
@@ -163,6 +175,13 @@ interface ServiceStatus {
|
|
|
163
175
|
since?: string;
|
|
164
176
|
pid?: number;
|
|
165
177
|
raw: string;
|
|
178
|
+
/**
|
|
179
|
+
* True when systemctl could not report on the unit at all: no `Active:` line
|
|
180
|
+
* parseable AND non-zero exit. Typical causes: typo'd unit name, unit file
|
|
181
|
+
* doesn't exist, systemd unreachable. Distinct from "service exists but is
|
|
182
|
+
* stopped" (active=false but unknown=false).
|
|
183
|
+
*/
|
|
184
|
+
unknown: boolean;
|
|
166
185
|
}
|
|
167
186
|
declare function serviceStatus(client: Client, serviceName: string, timeoutMs?: number): Promise<ServiceStatus>;
|
|
168
187
|
|
package/dist/server.js
CHANGED
|
@@ -200,8 +200,9 @@ function checkSshConfig(host) {
|
|
|
200
200
|
inHostBlock = patterns.some((p) => {
|
|
201
201
|
if (p === "*") return true;
|
|
202
202
|
if (p === host) return true;
|
|
203
|
-
if (p.includes("*")) {
|
|
204
|
-
const
|
|
203
|
+
if (p.includes("*") || p.includes("?")) {
|
|
204
|
+
const escaped = p.replace(/[\\^$.|+()[\]{}]/g, "\\$&");
|
|
205
|
+
const regex = new RegExp("^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
|
|
205
206
|
return regex.test(host);
|
|
206
207
|
}
|
|
207
208
|
return false;
|
|
@@ -335,9 +336,16 @@ function ensureAgent() {
|
|
|
335
336
|
const result = probeAgent(sock, "ssh-agent");
|
|
336
337
|
if (result) return result;
|
|
337
338
|
}
|
|
338
|
-
if (
|
|
339
|
+
if (process.platform === "win32") {
|
|
339
340
|
const result = probeAgent("\\\\.\\pipe\\openssh-ssh-agent", "Windows OpenSSH agent");
|
|
340
341
|
if (result) return result;
|
|
342
|
+
return {
|
|
343
|
+
running: false,
|
|
344
|
+
reachable: false,
|
|
345
|
+
keys: [],
|
|
346
|
+
started: false,
|
|
347
|
+
message: "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
|
|
348
|
+
};
|
|
341
349
|
}
|
|
342
350
|
const { stdout, ok } = runArgs("ssh-agent", ["-s"]);
|
|
343
351
|
if (ok) {
|
|
@@ -365,7 +373,7 @@ function ensureAgent() {
|
|
|
365
373
|
reachable: false,
|
|
366
374
|
keys: [],
|
|
367
375
|
started: false,
|
|
368
|
-
message:
|
|
376
|
+
message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
|
|
369
377
|
};
|
|
370
378
|
}
|
|
371
379
|
function detectKeyType(filePath, fileName) {
|
|
@@ -454,10 +462,10 @@ function loadKey(keyPath) {
|
|
|
454
462
|
if (ok) {
|
|
455
463
|
return { status: "ok", message: `Key loaded: ${resolved}` };
|
|
456
464
|
}
|
|
457
|
-
if (stdout.includes("
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
465
|
+
if (stdout.includes("UNPROTECTED PRIVATE KEY") || stdout.includes("too open") || stdout.includes("bad permissions")) {
|
|
466
|
+
return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
|
|
467
|
+
}
|
|
468
|
+
if (stdout.includes("passphrase") || stdout.includes("incorrect")) {
|
|
461
469
|
return { status: "error", message: `Key ${resolved} requires a passphrase. Add it manually: ssh-add ${resolved}` };
|
|
462
470
|
}
|
|
463
471
|
return { status: "error", message: `Failed to load key: ${stdout}` };
|
|
@@ -745,10 +753,16 @@ async function connectWithProxy(resolved) {
|
|
|
745
753
|
const jumpClient = await connectWithProxy(jumpResolved);
|
|
746
754
|
const targetHost = resolved.connectConfig.host;
|
|
747
755
|
const targetPort = resolved.connectConfig.port;
|
|
756
|
+
const endJump = () => {
|
|
757
|
+
try {
|
|
758
|
+
jumpClient.end();
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
};
|
|
748
762
|
const stream = await new Promise((resolve, reject) => {
|
|
749
763
|
jumpClient.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream2) => {
|
|
750
764
|
if (err) {
|
|
751
|
-
|
|
765
|
+
endJump();
|
|
752
766
|
return reject(err);
|
|
753
767
|
}
|
|
754
768
|
resolve(stream2);
|
|
@@ -757,10 +771,10 @@ async function connectWithProxy(resolved) {
|
|
|
757
771
|
return new Promise((resolve, reject) => {
|
|
758
772
|
const client = new Client();
|
|
759
773
|
client.on("ready", () => resolve(client)).on("error", (err) => {
|
|
760
|
-
|
|
774
|
+
endJump();
|
|
761
775
|
reject(err);
|
|
762
776
|
}).on("close", () => {
|
|
763
|
-
|
|
777
|
+
endJump();
|
|
764
778
|
}).connect({ ...resolved.connectConfig, sock: stream });
|
|
765
779
|
});
|
|
766
780
|
}
|
|
@@ -846,14 +860,19 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
|
|
|
846
860
|
stderrTruncated = true;
|
|
847
861
|
}
|
|
848
862
|
};
|
|
849
|
-
stream.on("close", (code) => {
|
|
863
|
+
stream.on("close", (code, signal) => {
|
|
850
864
|
let stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
851
865
|
let stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
852
866
|
if (stdoutTruncated) stdout += `
|
|
853
867
|
[output truncated at ${maxBytes} bytes]`;
|
|
854
868
|
if (stderrTruncated) stderr += `
|
|
855
869
|
[stderr truncated at ${maxBytes} bytes]`;
|
|
856
|
-
|
|
870
|
+
const exitCode = typeof code === "number" ? code : -1;
|
|
871
|
+
const result = { stdout, stderr, code: exitCode };
|
|
872
|
+
if (stdoutTruncated) result.stdoutTruncated = true;
|
|
873
|
+
if (stderrTruncated) result.stderrTruncated = true;
|
|
874
|
+
if (signal) result.signal = signal;
|
|
875
|
+
settle(() => resolve(result));
|
|
857
876
|
}).on("data", appendStdout).on("error", (err2) => {
|
|
858
877
|
settle(() => reject(err2));
|
|
859
878
|
});
|
|
@@ -1019,6 +1038,7 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
1019
1038
|
const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
|
|
1020
1039
|
const pidMatch = raw.match(/Main PID:\s+(\d+)/);
|
|
1021
1040
|
const sinceMatch = raw.match(/since\s+(.+?);/);
|
|
1041
|
+
const unknown = !activeMatch && result.code !== 0;
|
|
1022
1042
|
const fallbackStatus = result.code === 0 ? "active" : "inactive";
|
|
1023
1043
|
return {
|
|
1024
1044
|
name: serviceName,
|
|
@@ -1027,11 +1047,18 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
1027
1047
|
description: descMatch?.[1]?.trim(),
|
|
1028
1048
|
since: sinceMatch?.[1]?.trim(),
|
|
1029
1049
|
pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
|
|
1030
|
-
raw
|
|
1050
|
+
raw,
|
|
1051
|
+
unknown
|
|
1031
1052
|
};
|
|
1032
1053
|
}
|
|
1033
1054
|
|
|
1034
1055
|
// src/pool.ts
|
|
1056
|
+
function defaultMaxPoolSize() {
|
|
1057
|
+
const raw = process.env.SSH_MCP_MAX_POOL_SIZE;
|
|
1058
|
+
if (!raw) return 100;
|
|
1059
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1060
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
|
|
1061
|
+
}
|
|
1035
1062
|
var ConnectionPool = class {
|
|
1036
1063
|
entries = /* @__PURE__ */ new Map();
|
|
1037
1064
|
// Coalesces concurrent connect attempts for the same key so we don't open N
|
|
@@ -1048,7 +1075,7 @@ var ConnectionPool = class {
|
|
|
1048
1075
|
drained = false;
|
|
1049
1076
|
constructor(options) {
|
|
1050
1077
|
this.idleTtlMs = options?.idleTtlMs ?? 6e4;
|
|
1051
|
-
this.maxPoolSize = options?.maxPoolSize ??
|
|
1078
|
+
this.maxPoolSize = options?.maxPoolSize ?? defaultMaxPoolSize();
|
|
1052
1079
|
}
|
|
1053
1080
|
async acquire(config) {
|
|
1054
1081
|
const resolved = resolveConfig(config);
|
|
@@ -1252,6 +1279,7 @@ function registerTools(server, pool) {
|
|
|
1252
1279
|
if (result.stdout) parts.push(result.stdout);
|
|
1253
1280
|
if (result.stderr) parts.push(`[stderr]
|
|
1254
1281
|
${result.stderr}`);
|
|
1282
|
+
if (result.signal) parts.push(`[signal: ${result.signal}]`);
|
|
1255
1283
|
parts.push(`[exit code: ${result.code}]`);
|
|
1256
1284
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1257
1285
|
});
|
|
@@ -1528,11 +1556,12 @@ ${result.stderr}`);
|
|
|
1528
1556
|
maxdepth: z.number().optional().describe("Maximum directory depth to search"),
|
|
1529
1557
|
minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
|
|
1530
1558
|
maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
|
|
1559
|
+
newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
|
|
1531
1560
|
timeout: TimeoutSchema
|
|
1532
1561
|
},
|
|
1533
|
-
async ({ path, name, type, maxdepth, minsize, maxsize, timeout, ...conn }) => {
|
|
1562
|
+
async ({ path, name, type, maxdepth, minsize, maxsize, newer, timeout, ...conn }) => {
|
|
1534
1563
|
return connectionPool.withConnection(conn, async (client) => {
|
|
1535
|
-
const files = await find(client, { path, name, type, maxdepth, minsize, maxsize }, timeout || 3e4);
|
|
1564
|
+
const files = await find(client, { path, name, type, maxdepth, minsize, maxsize, newer }, timeout || 3e4);
|
|
1536
1565
|
if (files.length === 0) {
|
|
1537
1566
|
return { content: [{ type: "text", text: "No files found." }] };
|
|
1538
1567
|
}
|
|
@@ -1547,7 +1576,7 @@ ${files.join("\n")}` }] };
|
|
|
1547
1576
|
{
|
|
1548
1577
|
...connectionParams,
|
|
1549
1578
|
path: z.string().describe("Absolute path to the file to tail"),
|
|
1550
|
-
lines: z.number().optional().describe("Number of lines to read from the end (default: 100)"),
|
|
1579
|
+
lines: z.number().int().positive().optional().describe("Number of lines to read from the end (default: 100). Must be a positive integer."),
|
|
1551
1580
|
grep: z.string().optional().describe("Case-insensitive pattern to filter lines"),
|
|
1552
1581
|
timeout: TimeoutSchema
|
|
1553
1582
|
},
|
|
@@ -1587,7 +1616,7 @@ ${files.join("\n")}` }] };
|
|
|
1587
1616
|
if (status.since) lines.push(`Since: ${status.since}`);
|
|
1588
1617
|
lines.push("");
|
|
1589
1618
|
lines.push(status.raw);
|
|
1590
|
-
return { content: [{ type: "text", text: lines.join("\n") }], isError:
|
|
1619
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: status.unknown };
|
|
1591
1620
|
});
|
|
1592
1621
|
}
|
|
1593
1622
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/ssh-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "MCP server for SSH operations with built-in diagnostics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
-
"ssh2": "^1.
|
|
51
|
-
"zod": "^4.3
|
|
50
|
+
"ssh2": "^1.17.0",
|
|
51
|
+
"zod": "^4.4.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^2.4.
|
|
55
|
-
"@types/node": "^25.
|
|
56
|
-
"@types/ssh2": "^1.15.
|
|
54
|
+
"@biomejs/biome": "^2.4.15",
|
|
55
|
+
"@types/node": "^25.7.0",
|
|
56
|
+
"@types/ssh2": "^1.15.5",
|
|
57
57
|
"tsup": "^8.5.1",
|
|
58
58
|
"typescript": "^6.0.3",
|
|
59
|
-
"vitest": "^4.1.
|
|
59
|
+
"vitest": "^4.1.6"
|
|
60
60
|
}
|
|
61
61
|
}
|