@yawlabs/ssh-mcp 0.17.2 → 0.17.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 +59 -26
- package/dist/server.js +59 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
|
|
12
12
|
// src/env.ts
|
|
13
13
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
appendFileSync,
|
|
16
|
+
closeSync,
|
|
17
|
+
existsSync as existsSync2,
|
|
18
|
+
constants as fsConstants,
|
|
19
|
+
fstatSync,
|
|
20
|
+
openSync,
|
|
21
|
+
readdirSync as readdirSync2,
|
|
22
|
+
readFileSync as readFileSync3
|
|
23
|
+
} from "fs";
|
|
15
24
|
import { homedir as homedir3 } from "os";
|
|
16
25
|
import { join as join3 } from "path";
|
|
17
26
|
|
|
@@ -1083,7 +1092,7 @@ function ensureAgent() {
|
|
|
1083
1092
|
message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
|
|
1084
1093
|
};
|
|
1085
1094
|
}
|
|
1086
|
-
function detectKeyType(filePath, fileName) {
|
|
1095
|
+
function detectKeyType(filePath, fileName, content) {
|
|
1087
1096
|
const pubPath = `${filePath}.pub`;
|
|
1088
1097
|
if (existsSync2(pubPath)) {
|
|
1089
1098
|
try {
|
|
@@ -1100,7 +1109,6 @@ function detectKeyType(filePath, fileName) {
|
|
|
1100
1109
|
if (fileName.includes("ecdsa")) return "ecdsa";
|
|
1101
1110
|
if (fileName.includes("dsa")) return "dsa";
|
|
1102
1111
|
try {
|
|
1103
|
-
const content = readFileSync3(filePath, "utf8");
|
|
1104
1112
|
if (content.includes("RSA PRIVATE KEY")) return "rsa";
|
|
1105
1113
|
if (content.includes("EC PRIVATE KEY")) return "ecdsa";
|
|
1106
1114
|
if (content.includes("DSA PRIVATE KEY")) return "dsa";
|
|
@@ -1136,12 +1144,13 @@ function listSshKeysDetailed() {
|
|
|
1136
1144
|
for (const file of files) {
|
|
1137
1145
|
if (file.endsWith(".pub") || file.startsWith(".") || SSH_NON_KEY_FILES.has(file)) continue;
|
|
1138
1146
|
const filePath = join3(sshDir, file);
|
|
1147
|
+
let fd;
|
|
1139
1148
|
try {
|
|
1140
|
-
|
|
1141
|
-
if (!
|
|
1142
|
-
const content = readFileSync3(
|
|
1149
|
+
fd = openSync(filePath, fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0) | (fsConstants.O_NOCTTY ?? 0));
|
|
1150
|
+
if (!fstatSync(fd).isFile()) continue;
|
|
1151
|
+
const content = readFileSync3(fd, "utf8");
|
|
1143
1152
|
if (!content.includes("PRIVATE KEY")) continue;
|
|
1144
|
-
const type = detectKeyType(filePath, file);
|
|
1153
|
+
const type = detectKeyType(filePath, file, content);
|
|
1145
1154
|
let fingerprint;
|
|
1146
1155
|
const { stdout: fpOut, ok: fpOk } = runArgs("ssh-keygen", ["-lf", filePath]);
|
|
1147
1156
|
if (fpOk) {
|
|
@@ -1151,6 +1160,8 @@ function listSshKeysDetailed() {
|
|
|
1151
1160
|
const loadedInAgent = fingerprint ? loadedFingerprints.has(fingerprint) : false;
|
|
1152
1161
|
keys.push({ name: file, path: filePath, type, fingerprint, loadedInAgent });
|
|
1153
1162
|
} catch {
|
|
1163
|
+
} finally {
|
|
1164
|
+
if (fd !== void 0) closeSync(fd);
|
|
1154
1165
|
}
|
|
1155
1166
|
}
|
|
1156
1167
|
return { status: "ok", dir: sshDir, keys };
|
|
@@ -1306,7 +1317,7 @@ function testConnection(host, port = 22) {
|
|
|
1306
1317
|
}
|
|
1307
1318
|
|
|
1308
1319
|
// src/pool.ts
|
|
1309
|
-
import {
|
|
1320
|
+
import { createHmac, randomBytes, scryptSync } from "crypto";
|
|
1310
1321
|
var POOL_FULL_ERROR_CODE = "ERR_SSH_MCP_POOL_FULL";
|
|
1311
1322
|
var PoolFullError = class extends Error {
|
|
1312
1323
|
code = POOL_FULL_ERROR_CODE;
|
|
@@ -1336,21 +1347,24 @@ function defaultMaxPoolSize() {
|
|
|
1336
1347
|
const parsed = Number.parseInt(raw, 10);
|
|
1337
1348
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
|
|
1338
1349
|
}
|
|
1350
|
+
var AUTH_FINGERPRINT_SECRET = randomBytes(32);
|
|
1351
|
+
var PASSWORD_SCRYPT_COST = 1024;
|
|
1352
|
+
function updateField(h, tag, value) {
|
|
1353
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
|
|
1354
|
+
const len = Buffer.alloc(4);
|
|
1355
|
+
len.writeUInt32BE(bytes.length);
|
|
1356
|
+
h.update(tag);
|
|
1357
|
+
h.update(len);
|
|
1358
|
+
h.update(bytes);
|
|
1359
|
+
}
|
|
1339
1360
|
function authFingerprint(cc) {
|
|
1340
|
-
const h =
|
|
1341
|
-
if (cc.privateKey)
|
|
1342
|
-
h.update("k");
|
|
1343
|
-
h.update(cc.privateKey);
|
|
1344
|
-
}
|
|
1361
|
+
const h = createHmac("sha256", AUTH_FINGERPRINT_SECRET);
|
|
1362
|
+
if (cc.privateKey) updateField(h, "k", cc.privateKey);
|
|
1345
1363
|
if (cc.password !== void 0) {
|
|
1346
|
-
h
|
|
1347
|
-
h.update(cc.password);
|
|
1348
|
-
}
|
|
1349
|
-
if (typeof cc.agent === "string") {
|
|
1350
|
-
h.update("a");
|
|
1351
|
-
h.update(cc.agent);
|
|
1364
|
+
updateField(h, "p", scryptSync(cc.password, AUTH_FINGERPRINT_SECRET, 32, { N: PASSWORD_SCRYPT_COST }));
|
|
1352
1365
|
}
|
|
1353
|
-
|
|
1366
|
+
if (typeof cc.agent === "string") updateField(h, "a", cc.agent);
|
|
1367
|
+
return h.digest("hex").slice(0, 32);
|
|
1354
1368
|
}
|
|
1355
1369
|
function resolveOrDiagnose(config) {
|
|
1356
1370
|
try {
|
|
@@ -1510,7 +1524,7 @@ var ConnectionPool = class {
|
|
|
1510
1524
|
return client;
|
|
1511
1525
|
}
|
|
1512
1526
|
throw new Error(
|
|
1513
|
-
`Failed to acquire SSH connection for ${
|
|
1527
|
+
`Failed to acquire SSH connection for ${cc.username}@${cc.host}:${cc.port} after ${MAX_ACQUIRE_ATTEMPTS} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
1514
1528
|
);
|
|
1515
1529
|
}
|
|
1516
1530
|
release(client) {
|
|
@@ -1743,7 +1757,23 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
1743
1757
|
}
|
|
1744
1758
|
var VALID_FIND_SIZE = /^\d+[cwbkMG]?$/;
|
|
1745
1759
|
var FIND_EXPRESSION_TOKENS = /* @__PURE__ */ new Set(["(", ")", "!", ","]);
|
|
1760
|
+
var VALID_FIND_TYPES = /* @__PURE__ */ new Set(["f", "d", "l"]);
|
|
1761
|
+
function isNonNegativeInteger(n) {
|
|
1762
|
+
return typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
1763
|
+
}
|
|
1746
1764
|
async function find(client, options, timeoutMs = 3e4) {
|
|
1765
|
+
if (options.type !== void 0 && !VALID_FIND_TYPES.has(options.type)) {
|
|
1766
|
+
throw new Error(`Invalid type: "${String(options.type)}". Expected one of: f (file), d (directory), l (symlink)`);
|
|
1767
|
+
}
|
|
1768
|
+
if (options.maxdepth !== void 0 && !isNonNegativeInteger(options.maxdepth)) {
|
|
1769
|
+
throw new Error(`Invalid maxdepth: ${String(options.maxdepth)}. Expected a non-negative integer`);
|
|
1770
|
+
}
|
|
1771
|
+
if (options.minsize !== void 0 && typeof options.minsize !== "string") {
|
|
1772
|
+
throw new Error('Invalid minsize: expected a string such as "1M" or "100k"');
|
|
1773
|
+
}
|
|
1774
|
+
if (options.maxsize !== void 0 && typeof options.maxsize !== "string") {
|
|
1775
|
+
throw new Error('Invalid maxsize: expected a string such as "10M" or "500k"');
|
|
1776
|
+
}
|
|
1747
1777
|
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
1748
1778
|
throw new Error(
|
|
1749
1779
|
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "1M", "100k")`
|
|
@@ -1756,11 +1786,11 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
1756
1786
|
}
|
|
1757
1787
|
const pathOperand = options.path.startsWith("-") || FIND_EXPRESSION_TOKENS.has(options.path) ? `./${options.path}` : options.path;
|
|
1758
1788
|
const args = [shellQuote(pathOperand)];
|
|
1759
|
-
if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
|
|
1760
|
-
if (options.type) args.push("-type", options.type);
|
|
1789
|
+
if (options.maxdepth !== void 0) args.push("-maxdepth", shellQuote(String(options.maxdepth)));
|
|
1790
|
+
if (options.type) args.push("-type", shellQuote(options.type));
|
|
1761
1791
|
if (options.name) args.push("-name", shellQuote(options.name));
|
|
1762
|
-
if (options.minsize) args.push("-size", `+${options.minsize}`);
|
|
1763
|
-
if (options.maxsize) args.push("-size", `-${options.maxsize}`);
|
|
1792
|
+
if (options.minsize) args.push("-size", shellQuote(`+${options.minsize}`));
|
|
1793
|
+
if (options.maxsize) args.push("-size", shellQuote(`-${options.maxsize}`));
|
|
1764
1794
|
if (options.newer) args.push("-newer", shellQuote(options.newer));
|
|
1765
1795
|
const command = `find ${args.join(" ")}`;
|
|
1766
1796
|
const result = await exec(client, command, timeoutMs);
|
|
@@ -1770,6 +1800,9 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
1770
1800
|
return result.stdout.split("\n").filter(Boolean);
|
|
1771
1801
|
}
|
|
1772
1802
|
async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
|
|
1803
|
+
if (!isNonNegativeInteger(lines) || lines === 0) {
|
|
1804
|
+
throw new Error(`Invalid lines: ${String(lines)}. Expected a positive integer`);
|
|
1805
|
+
}
|
|
1773
1806
|
let command = `tail -n ${lines} -- ${shellQuote(path)}`;
|
|
1774
1807
|
if (grep) {
|
|
1775
1808
|
command += ` | grep -i -e ${shellQuote(grep)}`;
|
|
@@ -2314,7 +2347,7 @@ ${result.stderr}`);
|
|
|
2314
2347
|
path: z.string().describe("Directory to search in (e.g. /var/log, /home/user)"),
|
|
2315
2348
|
name: z.string().optional().describe("Filename pattern with wildcards (e.g. '*.log', 'config.*')"),
|
|
2316
2349
|
type: z.enum(["f", "d", "l"]).optional().describe("File type: f=file, d=directory, l=symlink"),
|
|
2317
|
-
maxdepth: z.number().optional().describe("Maximum directory depth to search"),
|
|
2350
|
+
maxdepth: z.number().int().nonnegative().optional().describe("Maximum directory depth to search (0 = the path itself only)"),
|
|
2318
2351
|
minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
|
|
2319
2352
|
maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
|
|
2320
2353
|
newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
|
package/dist/server.js
CHANGED
|
@@ -299,7 +299,16 @@ function diagnose(host, port = 22) {
|
|
|
299
299
|
|
|
300
300
|
// src/env.ts
|
|
301
301
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
302
|
-
import {
|
|
302
|
+
import {
|
|
303
|
+
appendFileSync,
|
|
304
|
+
closeSync,
|
|
305
|
+
existsSync as existsSync2,
|
|
306
|
+
constants as fsConstants,
|
|
307
|
+
fstatSync,
|
|
308
|
+
openSync,
|
|
309
|
+
readdirSync as readdirSync2,
|
|
310
|
+
readFileSync as readFileSync3
|
|
311
|
+
} from "fs";
|
|
303
312
|
import { homedir as homedir3 } from "os";
|
|
304
313
|
import { join as join3 } from "path";
|
|
305
314
|
|
|
@@ -1092,7 +1101,7 @@ function ensureAgent() {
|
|
|
1092
1101
|
message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
|
|
1093
1102
|
};
|
|
1094
1103
|
}
|
|
1095
|
-
function detectKeyType(filePath, fileName) {
|
|
1104
|
+
function detectKeyType(filePath, fileName, content) {
|
|
1096
1105
|
const pubPath = `${filePath}.pub`;
|
|
1097
1106
|
if (existsSync2(pubPath)) {
|
|
1098
1107
|
try {
|
|
@@ -1109,7 +1118,6 @@ function detectKeyType(filePath, fileName) {
|
|
|
1109
1118
|
if (fileName.includes("ecdsa")) return "ecdsa";
|
|
1110
1119
|
if (fileName.includes("dsa")) return "dsa";
|
|
1111
1120
|
try {
|
|
1112
|
-
const content = readFileSync3(filePath, "utf8");
|
|
1113
1121
|
if (content.includes("RSA PRIVATE KEY")) return "rsa";
|
|
1114
1122
|
if (content.includes("EC PRIVATE KEY")) return "ecdsa";
|
|
1115
1123
|
if (content.includes("DSA PRIVATE KEY")) return "dsa";
|
|
@@ -1148,12 +1156,13 @@ function listSshKeysDetailed() {
|
|
|
1148
1156
|
for (const file of files) {
|
|
1149
1157
|
if (file.endsWith(".pub") || file.startsWith(".") || SSH_NON_KEY_FILES.has(file)) continue;
|
|
1150
1158
|
const filePath = join3(sshDir, file);
|
|
1159
|
+
let fd;
|
|
1151
1160
|
try {
|
|
1152
|
-
|
|
1153
|
-
if (!
|
|
1154
|
-
const content = readFileSync3(
|
|
1161
|
+
fd = openSync(filePath, fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0) | (fsConstants.O_NOCTTY ?? 0));
|
|
1162
|
+
if (!fstatSync(fd).isFile()) continue;
|
|
1163
|
+
const content = readFileSync3(fd, "utf8");
|
|
1155
1164
|
if (!content.includes("PRIVATE KEY")) continue;
|
|
1156
|
-
const type = detectKeyType(filePath, file);
|
|
1165
|
+
const type = detectKeyType(filePath, file, content);
|
|
1157
1166
|
let fingerprint;
|
|
1158
1167
|
const { stdout: fpOut, ok: fpOk } = runArgs("ssh-keygen", ["-lf", filePath]);
|
|
1159
1168
|
if (fpOk) {
|
|
@@ -1163,6 +1172,8 @@ function listSshKeysDetailed() {
|
|
|
1163
1172
|
const loadedInAgent = fingerprint ? loadedFingerprints.has(fingerprint) : false;
|
|
1164
1173
|
keys.push({ name: file, path: filePath, type, fingerprint, loadedInAgent });
|
|
1165
1174
|
} catch {
|
|
1175
|
+
} finally {
|
|
1176
|
+
if (fd !== void 0) closeSync(fd);
|
|
1166
1177
|
}
|
|
1167
1178
|
}
|
|
1168
1179
|
return { status: "ok", dir: sshDir, keys };
|
|
@@ -1318,7 +1329,7 @@ function testConnection(host, port = 22) {
|
|
|
1318
1329
|
}
|
|
1319
1330
|
|
|
1320
1331
|
// src/pool.ts
|
|
1321
|
-
import {
|
|
1332
|
+
import { createHmac, randomBytes, scryptSync } from "crypto";
|
|
1322
1333
|
var POOL_FULL_ERROR_CODE = "ERR_SSH_MCP_POOL_FULL";
|
|
1323
1334
|
var PoolFullError = class extends Error {
|
|
1324
1335
|
code = POOL_FULL_ERROR_CODE;
|
|
@@ -1348,21 +1359,24 @@ function defaultMaxPoolSize() {
|
|
|
1348
1359
|
const parsed = Number.parseInt(raw, 10);
|
|
1349
1360
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
|
|
1350
1361
|
}
|
|
1362
|
+
var AUTH_FINGERPRINT_SECRET = randomBytes(32);
|
|
1363
|
+
var PASSWORD_SCRYPT_COST = 1024;
|
|
1364
|
+
function updateField(h, tag, value) {
|
|
1365
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
|
|
1366
|
+
const len = Buffer.alloc(4);
|
|
1367
|
+
len.writeUInt32BE(bytes.length);
|
|
1368
|
+
h.update(tag);
|
|
1369
|
+
h.update(len);
|
|
1370
|
+
h.update(bytes);
|
|
1371
|
+
}
|
|
1351
1372
|
function authFingerprint(cc) {
|
|
1352
|
-
const h =
|
|
1353
|
-
if (cc.privateKey)
|
|
1354
|
-
h.update("k");
|
|
1355
|
-
h.update(cc.privateKey);
|
|
1356
|
-
}
|
|
1373
|
+
const h = createHmac("sha256", AUTH_FINGERPRINT_SECRET);
|
|
1374
|
+
if (cc.privateKey) updateField(h, "k", cc.privateKey);
|
|
1357
1375
|
if (cc.password !== void 0) {
|
|
1358
|
-
h
|
|
1359
|
-
h.update(cc.password);
|
|
1360
|
-
}
|
|
1361
|
-
if (typeof cc.agent === "string") {
|
|
1362
|
-
h.update("a");
|
|
1363
|
-
h.update(cc.agent);
|
|
1376
|
+
updateField(h, "p", scryptSync(cc.password, AUTH_FINGERPRINT_SECRET, 32, { N: PASSWORD_SCRYPT_COST }));
|
|
1364
1377
|
}
|
|
1365
|
-
|
|
1378
|
+
if (typeof cc.agent === "string") updateField(h, "a", cc.agent);
|
|
1379
|
+
return h.digest("hex").slice(0, 32);
|
|
1366
1380
|
}
|
|
1367
1381
|
function resolveOrDiagnose(config) {
|
|
1368
1382
|
try {
|
|
@@ -1522,7 +1536,7 @@ var ConnectionPool = class {
|
|
|
1522
1536
|
return client;
|
|
1523
1537
|
}
|
|
1524
1538
|
throw new Error(
|
|
1525
|
-
`Failed to acquire SSH connection for ${
|
|
1539
|
+
`Failed to acquire SSH connection for ${cc.username}@${cc.host}:${cc.port} after ${MAX_ACQUIRE_ATTEMPTS} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
1526
1540
|
);
|
|
1527
1541
|
}
|
|
1528
1542
|
release(client) {
|
|
@@ -1746,7 +1760,23 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
1746
1760
|
}
|
|
1747
1761
|
var VALID_FIND_SIZE = /^\d+[cwbkMG]?$/;
|
|
1748
1762
|
var FIND_EXPRESSION_TOKENS = /* @__PURE__ */ new Set(["(", ")", "!", ","]);
|
|
1763
|
+
var VALID_FIND_TYPES = /* @__PURE__ */ new Set(["f", "d", "l"]);
|
|
1764
|
+
function isNonNegativeInteger(n) {
|
|
1765
|
+
return typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
1766
|
+
}
|
|
1749
1767
|
async function find(client, options, timeoutMs = 3e4) {
|
|
1768
|
+
if (options.type !== void 0 && !VALID_FIND_TYPES.has(options.type)) {
|
|
1769
|
+
throw new Error(`Invalid type: "${String(options.type)}". Expected one of: f (file), d (directory), l (symlink)`);
|
|
1770
|
+
}
|
|
1771
|
+
if (options.maxdepth !== void 0 && !isNonNegativeInteger(options.maxdepth)) {
|
|
1772
|
+
throw new Error(`Invalid maxdepth: ${String(options.maxdepth)}. Expected a non-negative integer`);
|
|
1773
|
+
}
|
|
1774
|
+
if (options.minsize !== void 0 && typeof options.minsize !== "string") {
|
|
1775
|
+
throw new Error('Invalid minsize: expected a string such as "1M" or "100k"');
|
|
1776
|
+
}
|
|
1777
|
+
if (options.maxsize !== void 0 && typeof options.maxsize !== "string") {
|
|
1778
|
+
throw new Error('Invalid maxsize: expected a string such as "10M" or "500k"');
|
|
1779
|
+
}
|
|
1750
1780
|
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
1751
1781
|
throw new Error(
|
|
1752
1782
|
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "1M", "100k")`
|
|
@@ -1759,11 +1789,11 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
1759
1789
|
}
|
|
1760
1790
|
const pathOperand = options.path.startsWith("-") || FIND_EXPRESSION_TOKENS.has(options.path) ? `./${options.path}` : options.path;
|
|
1761
1791
|
const args = [shellQuote(pathOperand)];
|
|
1762
|
-
if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
|
|
1763
|
-
if (options.type) args.push("-type", options.type);
|
|
1792
|
+
if (options.maxdepth !== void 0) args.push("-maxdepth", shellQuote(String(options.maxdepth)));
|
|
1793
|
+
if (options.type) args.push("-type", shellQuote(options.type));
|
|
1764
1794
|
if (options.name) args.push("-name", shellQuote(options.name));
|
|
1765
|
-
if (options.minsize) args.push("-size", `+${options.minsize}`);
|
|
1766
|
-
if (options.maxsize) args.push("-size", `-${options.maxsize}`);
|
|
1795
|
+
if (options.minsize) args.push("-size", shellQuote(`+${options.minsize}`));
|
|
1796
|
+
if (options.maxsize) args.push("-size", shellQuote(`-${options.maxsize}`));
|
|
1767
1797
|
if (options.newer) args.push("-newer", shellQuote(options.newer));
|
|
1768
1798
|
const command = `find ${args.join(" ")}`;
|
|
1769
1799
|
const result = await exec(client, command, timeoutMs);
|
|
@@ -1773,6 +1803,9 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
1773
1803
|
return result.stdout.split("\n").filter(Boolean);
|
|
1774
1804
|
}
|
|
1775
1805
|
async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
|
|
1806
|
+
if (!isNonNegativeInteger(lines) || lines === 0) {
|
|
1807
|
+
throw new Error(`Invalid lines: ${String(lines)}. Expected a positive integer`);
|
|
1808
|
+
}
|
|
1776
1809
|
let command = `tail -n ${lines} -- ${shellQuote(path)}`;
|
|
1777
1810
|
if (grep) {
|
|
1778
1811
|
command += ` | grep -i -e ${shellQuote(grep)}`;
|
|
@@ -2320,7 +2353,7 @@ ${result.stderr}`);
|
|
|
2320
2353
|
path: z.string().describe("Directory to search in (e.g. /var/log, /home/user)"),
|
|
2321
2354
|
name: z.string().optional().describe("Filename pattern with wildcards (e.g. '*.log', 'config.*')"),
|
|
2322
2355
|
type: z.enum(["f", "d", "l"]).optional().describe("File type: f=file, d=directory, l=symlink"),
|
|
2323
|
-
maxdepth: z.number().optional().describe("Maximum directory depth to search"),
|
|
2356
|
+
maxdepth: z.number().int().nonnegative().optional().describe("Maximum directory depth to search (0 = the path itself only)"),
|
|
2324
2357
|
minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
|
|
2325
2358
|
maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
|
|
2326
2359
|
newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/ssh-mcp",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"mcpName": "io.github.YawLabs/ssh-mcp",
|
|
5
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",
|