@yawlabs/ssh-mcp 0.11.4 → 0.12.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/dist/index.js +37 -18
- package/dist/server.d.ts +2 -1
- package/dist/server.js +34 -18
- package/package.json +67 -62
package/dist/index.js
CHANGED
|
@@ -214,8 +214,6 @@ function checkSshConfig(host) {
|
|
|
214
214
|
if (inHostBlock) hostConfig.push(trimmed);
|
|
215
215
|
} else if (inHostBlock && trimmed) {
|
|
216
216
|
hostConfig.push(trimmed);
|
|
217
|
-
} else if (inHostBlock && !trimmed) {
|
|
218
|
-
inHostBlock = false;
|
|
219
217
|
}
|
|
220
218
|
}
|
|
221
219
|
if (hostConfig.length === 0) {
|
|
@@ -615,7 +613,15 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
615
613
|
import { homedir as homedir3 } from "os";
|
|
616
614
|
import { join as join3 } from "path";
|
|
617
615
|
import { Client } from "ssh2";
|
|
616
|
+
var sshConfigCache = /* @__PURE__ */ new Map();
|
|
618
617
|
function resolveFromSshConfig(host) {
|
|
618
|
+
const cached = sshConfigCache.get(host);
|
|
619
|
+
if (cached !== void 0) return cached;
|
|
620
|
+
const result = resolveFromSshConfigUncached(host);
|
|
621
|
+
sshConfigCache.set(host, result);
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function resolveFromSshConfigUncached(host) {
|
|
619
625
|
try {
|
|
620
626
|
const { stdout, ok } = runArgs("ssh", ["-G", host]);
|
|
621
627
|
if (!ok) return null;
|
|
@@ -695,8 +701,10 @@ function resolveConfig(config) {
|
|
|
695
701
|
keepaliveCountMax: 3,
|
|
696
702
|
hostVerifier: buildHostVerifier(verifierHosts, port)
|
|
697
703
|
};
|
|
704
|
+
const home = homedir3();
|
|
698
705
|
if (config.privateKeyPath) {
|
|
699
|
-
|
|
706
|
+
const keyPath = config.privateKeyPath.startsWith("~") ? join3(home, config.privateKeyPath.slice(1)) : config.privateKeyPath;
|
|
707
|
+
connectConfig.privateKey = readFileSync3(keyPath);
|
|
700
708
|
} else if (config.password) {
|
|
701
709
|
connectConfig.password = config.password;
|
|
702
710
|
} else {
|
|
@@ -704,7 +712,6 @@ function resolveConfig(config) {
|
|
|
704
712
|
if (agentSock) {
|
|
705
713
|
connectConfig.agent = agentSock;
|
|
706
714
|
}
|
|
707
|
-
const home = homedir3();
|
|
708
715
|
const keyPaths = sshConfig && sshConfig.identityFiles.length > 0 ? sshConfig.identityFiles.map((p) => p.startsWith("~") ? join3(home, p.slice(1)) : p) : [join3(home, ".ssh", "id_ed25519"), join3(home, ".ssh", "id_rsa"), join3(home, ".ssh", "id_ecdsa")];
|
|
709
716
|
for (const keyPath of keyPaths) {
|
|
710
717
|
try {
|
|
@@ -941,10 +948,11 @@ async function writeFile(client, remotePath, content) {
|
|
|
941
948
|
}
|
|
942
949
|
}
|
|
943
950
|
async function uploadFile(client, localPath, remotePath) {
|
|
951
|
+
const resolvedLocal = localPath.startsWith("~") ? join3(homedir3(), localPath.slice(1)) : localPath;
|
|
944
952
|
const sftp = await getSftp(client);
|
|
945
953
|
try {
|
|
946
954
|
await new Promise((resolve, reject) => {
|
|
947
|
-
sftp.fastPut(
|
|
955
|
+
sftp.fastPut(resolvedLocal, remotePath, (err) => {
|
|
948
956
|
if (err) return reject(err);
|
|
949
957
|
resolve();
|
|
950
958
|
});
|
|
@@ -954,10 +962,11 @@ async function uploadFile(client, localPath, remotePath) {
|
|
|
954
962
|
}
|
|
955
963
|
}
|
|
956
964
|
async function downloadFile(client, remotePath, localPath) {
|
|
965
|
+
const resolvedLocal = localPath.startsWith("~") ? join3(homedir3(), localPath.slice(1)) : localPath;
|
|
957
966
|
const sftp = await getSftp(client);
|
|
958
967
|
try {
|
|
959
968
|
await new Promise((resolve, reject) => {
|
|
960
|
-
sftp.fastGet(remotePath,
|
|
969
|
+
sftp.fastGet(remotePath, resolvedLocal, (err) => {
|
|
961
970
|
if (err) return reject(err);
|
|
962
971
|
resolve();
|
|
963
972
|
});
|
|
@@ -1296,16 +1305,16 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
1296
1305
|
};
|
|
1297
1306
|
});
|
|
1298
1307
|
}
|
|
1299
|
-
var VALID_FIND_SIZE = /^\d+[
|
|
1308
|
+
var VALID_FIND_SIZE = /^\d+[cwbkMG]?$/;
|
|
1300
1309
|
async function find(client, options, timeoutMs = 3e4) {
|
|
1301
1310
|
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
1302
1311
|
throw new Error(
|
|
1303
|
-
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional k/M/G
|
|
1312
|
+
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "1M", "100k")`
|
|
1304
1313
|
);
|
|
1305
1314
|
}
|
|
1306
1315
|
if (options.maxsize && !VALID_FIND_SIZE.test(options.maxsize)) {
|
|
1307
1316
|
throw new Error(
|
|
1308
|
-
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G
|
|
1317
|
+
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "10M", "500k")`
|
|
1309
1318
|
);
|
|
1310
1319
|
}
|
|
1311
1320
|
const args = ["--", shellQuote(options.path)];
|
|
@@ -1386,6 +1395,9 @@ function enforcePolicy(command) {
|
|
|
1386
1395
|
|
|
1387
1396
|
// src/tools.ts
|
|
1388
1397
|
var HostSchema = z.string().describe("SSH hostname or IP address");
|
|
1398
|
+
var AbsoluteRemotePathSchema = z.string().refine((p) => p.startsWith("/"), {
|
|
1399
|
+
message: "Path must be absolute (start with /). SFTP does not expand ~ or resolve relative paths through a shell."
|
|
1400
|
+
});
|
|
1389
1401
|
var PortSchema = z.number().int().min(1).max(65535).optional().describe("SSH port (default: 22)");
|
|
1390
1402
|
var UsernameSchema = z.string().optional().describe("SSH username (default: current user)");
|
|
1391
1403
|
var KeyPathSchema = z.string().optional().describe("Path to SSH private key");
|
|
@@ -1437,7 +1449,9 @@ ${result.stderr}`);
|
|
|
1437
1449
|
"Read a file from a remote host via SFTP.",
|
|
1438
1450
|
{
|
|
1439
1451
|
...connectionParams,
|
|
1440
|
-
path: z.string().
|
|
1452
|
+
path: z.string().refine((p) => p.startsWith("/"), {
|
|
1453
|
+
message: "Path must be absolute (start with /). Relative paths resolve against the remote user's CWD, which is rarely what you want."
|
|
1454
|
+
}).describe("Absolute path to the remote file. Must start with /.")
|
|
1441
1455
|
},
|
|
1442
1456
|
async ({ path, ...conn }) => {
|
|
1443
1457
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1451,7 +1465,7 @@ ${result.stderr}`);
|
|
|
1451
1465
|
"Write content to a file on a remote host via SFTP. Creates or overwrites the file.",
|
|
1452
1466
|
{
|
|
1453
1467
|
...connectionParams,
|
|
1454
|
-
path:
|
|
1468
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
|
|
1455
1469
|
content: z.string().describe("File content to write")
|
|
1456
1470
|
},
|
|
1457
1471
|
async ({ path, content, ...conn }) => {
|
|
@@ -1467,7 +1481,7 @@ ${result.stderr}`);
|
|
|
1467
1481
|
{
|
|
1468
1482
|
...connectionParams,
|
|
1469
1483
|
localPath: z.string().describe("Path to the local file to upload"),
|
|
1470
|
-
remotePath:
|
|
1484
|
+
remotePath: AbsoluteRemotePathSchema.describe("Absolute path on the remote host. Must start with /.")
|
|
1471
1485
|
},
|
|
1472
1486
|
async ({ localPath, remotePath, ...conn }) => {
|
|
1473
1487
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1481,7 +1495,7 @@ ${result.stderr}`);
|
|
|
1481
1495
|
"Download a file from a remote host to local filesystem via SFTP.",
|
|
1482
1496
|
{
|
|
1483
1497
|
...connectionParams,
|
|
1484
|
-
remotePath:
|
|
1498
|
+
remotePath: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
|
|
1485
1499
|
localPath: z.string().describe("Local path to save the downloaded file")
|
|
1486
1500
|
},
|
|
1487
1501
|
async ({ remotePath, localPath, ...conn }) => {
|
|
@@ -1496,7 +1510,7 @@ ${result.stderr}`);
|
|
|
1496
1510
|
"List files in a directory on a remote host via SFTP.",
|
|
1497
1511
|
{
|
|
1498
1512
|
...connectionParams,
|
|
1499
|
-
path:
|
|
1513
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote directory. Must start with /.")
|
|
1500
1514
|
},
|
|
1501
1515
|
async ({ path, ...conn }) => {
|
|
1502
1516
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1510,7 +1524,7 @@ ${result.stderr}`);
|
|
|
1510
1524
|
"Get metadata for a file or directory on a remote host via SFTP. Returns size, permissions (octal), uid/gid, mtime/atime, and type flags (isFile, isDirectory, isSymbolicLink). Use this instead of parsing `ls -la` output.",
|
|
1511
1525
|
{
|
|
1512
1526
|
...connectionParams,
|
|
1513
|
-
path:
|
|
1527
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file or directory. Must start with /.")
|
|
1514
1528
|
},
|
|
1515
1529
|
async ({ path, ...conn }) => {
|
|
1516
1530
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1547,7 +1561,9 @@ ${result.stderr}`);
|
|
|
1547
1561
|
"Delete a file or empty directory on a remote host via SFTP. Auto-detects the path type and calls the right SFTP op (unlink for files/symlinks, rmdir for empty dirs). Recursive directory delete is intentionally NOT supported -- for that, use ssh_exec with `rm -rf` explicitly so the destructive intent is visible in the tool trace.",
|
|
1548
1562
|
{
|
|
1549
1563
|
...connectionParams,
|
|
1550
|
-
path:
|
|
1564
|
+
path: AbsoluteRemotePathSchema.describe(
|
|
1565
|
+
"Absolute path of the file or empty directory to delete. Must start with /."
|
|
1566
|
+
)
|
|
1551
1567
|
},
|
|
1552
1568
|
async ({ path, ...conn }) => {
|
|
1553
1569
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1822,8 +1838,7 @@ ${files.join("\n")}` }] };
|
|
|
1822
1838
|
}
|
|
1823
1839
|
|
|
1824
1840
|
// src/server.ts
|
|
1825
|
-
var
|
|
1826
|
-
var { version } = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1841
|
+
var version = typeof __VERSION__ !== "undefined" ? __VERSION__ : JSON.parse(readFileSync4(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
1827
1842
|
function createServer(pool) {
|
|
1828
1843
|
const server = new McpServer({
|
|
1829
1844
|
name: "ssh-mcp",
|
|
@@ -1834,6 +1849,10 @@ function createServer(pool) {
|
|
|
1834
1849
|
}
|
|
1835
1850
|
|
|
1836
1851
|
// src/index.ts
|
|
1852
|
+
if (process.argv[2] === "--version" || process.argv[2] === "version") {
|
|
1853
|
+
console.log(version);
|
|
1854
|
+
process.exit(0);
|
|
1855
|
+
}
|
|
1837
1856
|
async function main() {
|
|
1838
1857
|
const pool = new ConnectionPool();
|
|
1839
1858
|
const server = createServer(pool);
|
package/dist/server.d.ts
CHANGED
|
@@ -217,6 +217,7 @@ declare function isPolicyConfigured(): boolean;
|
|
|
217
217
|
|
|
218
218
|
declare function registerTools(server: McpServer, pool?: ConnectionPool): void;
|
|
219
219
|
|
|
220
|
+
declare const version: string;
|
|
220
221
|
declare function createServer(pool?: ConnectionPool): McpServer;
|
|
221
222
|
|
|
222
|
-
export { type AgentResult, type ConfigLookupResult, ConnectionPool, type DiagnosticReport, type DiagnosticResult, type ExecResult, type FileStats, type FindOptions, type KeyInfo, type MultiExecHost, type MultiExecResult, type PoolOptions, type ResolvedConfig, type SSHConfig, type ServiceStatus, checkConnectivity, checkGitSsh, checkKnownHosts, checkSshAgent, checkSshConfig, checkSshKeys, configLookup, connect, connectRaw, connectWithProxy, createServer, deleteFile, diagnose, downloadFile, enforcePolicy, ensureAgent, exec, find, fixKnownHosts, formatDiagnostics, isPolicyConfigured, listDir, listSshKeys, loadKey, makeDir, multiExec, readFile, readKnownHostsKeys, registerTools, resolveConfig, serviceStatus, statFile, tail, testConnection, uploadFile, writeFile };
|
|
223
|
+
export { type AgentResult, type ConfigLookupResult, ConnectionPool, type DiagnosticReport, type DiagnosticResult, type ExecResult, type FileStats, type FindOptions, type KeyInfo, type MultiExecHost, type MultiExecResult, type PoolOptions, type ResolvedConfig, type SSHConfig, type ServiceStatus, checkConnectivity, checkGitSsh, checkKnownHosts, checkSshAgent, checkSshConfig, checkSshKeys, configLookup, connect, connectRaw, connectWithProxy, createServer, deleteFile, diagnose, downloadFile, enforcePolicy, ensureAgent, exec, find, fixKnownHosts, formatDiagnostics, isPolicyConfigured, listDir, listSshKeys, loadKey, makeDir, multiExec, readFile, readKnownHostsKeys, registerTools, resolveConfig, serviceStatus, statFile, tail, testConnection, uploadFile, version, writeFile };
|
package/dist/server.js
CHANGED
|
@@ -212,8 +212,6 @@ function checkSshConfig(host) {
|
|
|
212
212
|
if (inHostBlock) hostConfig.push(trimmed);
|
|
213
213
|
} else if (inHostBlock && trimmed) {
|
|
214
214
|
hostConfig.push(trimmed);
|
|
215
|
-
} else if (inHostBlock && !trimmed) {
|
|
216
|
-
inHostBlock = false;
|
|
217
215
|
}
|
|
218
216
|
}
|
|
219
217
|
if (hostConfig.length === 0) {
|
|
@@ -608,7 +606,15 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
608
606
|
import { homedir as homedir3 } from "os";
|
|
609
607
|
import { join as join3 } from "path";
|
|
610
608
|
import { Client } from "ssh2";
|
|
609
|
+
var sshConfigCache = /* @__PURE__ */ new Map();
|
|
611
610
|
function resolveFromSshConfig(host) {
|
|
611
|
+
const cached = sshConfigCache.get(host);
|
|
612
|
+
if (cached !== void 0) return cached;
|
|
613
|
+
const result = resolveFromSshConfigUncached(host);
|
|
614
|
+
sshConfigCache.set(host, result);
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
function resolveFromSshConfigUncached(host) {
|
|
612
618
|
try {
|
|
613
619
|
const { stdout, ok } = runArgs("ssh", ["-G", host]);
|
|
614
620
|
if (!ok) return null;
|
|
@@ -688,8 +694,10 @@ function resolveConfig(config) {
|
|
|
688
694
|
keepaliveCountMax: 3,
|
|
689
695
|
hostVerifier: buildHostVerifier(verifierHosts, port)
|
|
690
696
|
};
|
|
697
|
+
const home = homedir3();
|
|
691
698
|
if (config.privateKeyPath) {
|
|
692
|
-
|
|
699
|
+
const keyPath = config.privateKeyPath.startsWith("~") ? join3(home, config.privateKeyPath.slice(1)) : config.privateKeyPath;
|
|
700
|
+
connectConfig.privateKey = readFileSync3(keyPath);
|
|
693
701
|
} else if (config.password) {
|
|
694
702
|
connectConfig.password = config.password;
|
|
695
703
|
} else {
|
|
@@ -697,7 +705,6 @@ function resolveConfig(config) {
|
|
|
697
705
|
if (agentSock) {
|
|
698
706
|
connectConfig.agent = agentSock;
|
|
699
707
|
}
|
|
700
|
-
const home = homedir3();
|
|
701
708
|
const keyPaths = sshConfig && sshConfig.identityFiles.length > 0 ? sshConfig.identityFiles.map((p) => p.startsWith("~") ? join3(home, p.slice(1)) : p) : [join3(home, ".ssh", "id_ed25519"), join3(home, ".ssh", "id_rsa"), join3(home, ".ssh", "id_ecdsa")];
|
|
702
709
|
for (const keyPath of keyPaths) {
|
|
703
710
|
try {
|
|
@@ -952,10 +959,11 @@ async function writeFile(client, remotePath, content) {
|
|
|
952
959
|
}
|
|
953
960
|
}
|
|
954
961
|
async function uploadFile(client, localPath, remotePath) {
|
|
962
|
+
const resolvedLocal = localPath.startsWith("~") ? join3(homedir3(), localPath.slice(1)) : localPath;
|
|
955
963
|
const sftp = await getSftp(client);
|
|
956
964
|
try {
|
|
957
965
|
await new Promise((resolve, reject) => {
|
|
958
|
-
sftp.fastPut(
|
|
966
|
+
sftp.fastPut(resolvedLocal, remotePath, (err) => {
|
|
959
967
|
if (err) return reject(err);
|
|
960
968
|
resolve();
|
|
961
969
|
});
|
|
@@ -965,10 +973,11 @@ async function uploadFile(client, localPath, remotePath) {
|
|
|
965
973
|
}
|
|
966
974
|
}
|
|
967
975
|
async function downloadFile(client, remotePath, localPath) {
|
|
976
|
+
const resolvedLocal = localPath.startsWith("~") ? join3(homedir3(), localPath.slice(1)) : localPath;
|
|
968
977
|
const sftp = await getSftp(client);
|
|
969
978
|
try {
|
|
970
979
|
await new Promise((resolve, reject) => {
|
|
971
|
-
sftp.fastGet(remotePath,
|
|
980
|
+
sftp.fastGet(remotePath, resolvedLocal, (err) => {
|
|
972
981
|
if (err) return reject(err);
|
|
973
982
|
resolve();
|
|
974
983
|
});
|
|
@@ -1088,16 +1097,16 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
1088
1097
|
};
|
|
1089
1098
|
});
|
|
1090
1099
|
}
|
|
1091
|
-
var VALID_FIND_SIZE = /^\d+[
|
|
1100
|
+
var VALID_FIND_SIZE = /^\d+[cwbkMG]?$/;
|
|
1092
1101
|
async function find(client, options, timeoutMs = 3e4) {
|
|
1093
1102
|
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
1094
1103
|
throw new Error(
|
|
1095
|
-
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional k/M/G
|
|
1104
|
+
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "1M", "100k")`
|
|
1096
1105
|
);
|
|
1097
1106
|
}
|
|
1098
1107
|
if (options.maxsize && !VALID_FIND_SIZE.test(options.maxsize)) {
|
|
1099
1108
|
throw new Error(
|
|
1100
|
-
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G
|
|
1109
|
+
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional c/w/b/k/M/G (e.g. "10M", "500k")`
|
|
1101
1110
|
);
|
|
1102
1111
|
}
|
|
1103
1112
|
const args = ["--", shellQuote(options.path)];
|
|
@@ -1392,6 +1401,9 @@ ${diag}`);
|
|
|
1392
1401
|
|
|
1393
1402
|
// src/tools.ts
|
|
1394
1403
|
var HostSchema = z.string().describe("SSH hostname or IP address");
|
|
1404
|
+
var AbsoluteRemotePathSchema = z.string().refine((p) => p.startsWith("/"), {
|
|
1405
|
+
message: "Path must be absolute (start with /). SFTP does not expand ~ or resolve relative paths through a shell."
|
|
1406
|
+
});
|
|
1395
1407
|
var PortSchema = z.number().int().min(1).max(65535).optional().describe("SSH port (default: 22)");
|
|
1396
1408
|
var UsernameSchema = z.string().optional().describe("SSH username (default: current user)");
|
|
1397
1409
|
var KeyPathSchema = z.string().optional().describe("Path to SSH private key");
|
|
@@ -1443,7 +1455,9 @@ ${result.stderr}`);
|
|
|
1443
1455
|
"Read a file from a remote host via SFTP.",
|
|
1444
1456
|
{
|
|
1445
1457
|
...connectionParams,
|
|
1446
|
-
path: z.string().
|
|
1458
|
+
path: z.string().refine((p) => p.startsWith("/"), {
|
|
1459
|
+
message: "Path must be absolute (start with /). Relative paths resolve against the remote user's CWD, which is rarely what you want."
|
|
1460
|
+
}).describe("Absolute path to the remote file. Must start with /.")
|
|
1447
1461
|
},
|
|
1448
1462
|
async ({ path, ...conn }) => {
|
|
1449
1463
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1457,7 +1471,7 @@ ${result.stderr}`);
|
|
|
1457
1471
|
"Write content to a file on a remote host via SFTP. Creates or overwrites the file.",
|
|
1458
1472
|
{
|
|
1459
1473
|
...connectionParams,
|
|
1460
|
-
path:
|
|
1474
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
|
|
1461
1475
|
content: z.string().describe("File content to write")
|
|
1462
1476
|
},
|
|
1463
1477
|
async ({ path, content, ...conn }) => {
|
|
@@ -1473,7 +1487,7 @@ ${result.stderr}`);
|
|
|
1473
1487
|
{
|
|
1474
1488
|
...connectionParams,
|
|
1475
1489
|
localPath: z.string().describe("Path to the local file to upload"),
|
|
1476
|
-
remotePath:
|
|
1490
|
+
remotePath: AbsoluteRemotePathSchema.describe("Absolute path on the remote host. Must start with /.")
|
|
1477
1491
|
},
|
|
1478
1492
|
async ({ localPath, remotePath, ...conn }) => {
|
|
1479
1493
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1487,7 +1501,7 @@ ${result.stderr}`);
|
|
|
1487
1501
|
"Download a file from a remote host to local filesystem via SFTP.",
|
|
1488
1502
|
{
|
|
1489
1503
|
...connectionParams,
|
|
1490
|
-
remotePath:
|
|
1504
|
+
remotePath: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
|
|
1491
1505
|
localPath: z.string().describe("Local path to save the downloaded file")
|
|
1492
1506
|
},
|
|
1493
1507
|
async ({ remotePath, localPath, ...conn }) => {
|
|
@@ -1502,7 +1516,7 @@ ${result.stderr}`);
|
|
|
1502
1516
|
"List files in a directory on a remote host via SFTP.",
|
|
1503
1517
|
{
|
|
1504
1518
|
...connectionParams,
|
|
1505
|
-
path:
|
|
1519
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote directory. Must start with /.")
|
|
1506
1520
|
},
|
|
1507
1521
|
async ({ path, ...conn }) => {
|
|
1508
1522
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1516,7 +1530,7 @@ ${result.stderr}`);
|
|
|
1516
1530
|
"Get metadata for a file or directory on a remote host via SFTP. Returns size, permissions (octal), uid/gid, mtime/atime, and type flags (isFile, isDirectory, isSymbolicLink). Use this instead of parsing `ls -la` output.",
|
|
1517
1531
|
{
|
|
1518
1532
|
...connectionParams,
|
|
1519
|
-
path:
|
|
1533
|
+
path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file or directory. Must start with /.")
|
|
1520
1534
|
},
|
|
1521
1535
|
async ({ path, ...conn }) => {
|
|
1522
1536
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1553,7 +1567,9 @@ ${result.stderr}`);
|
|
|
1553
1567
|
"Delete a file or empty directory on a remote host via SFTP. Auto-detects the path type and calls the right SFTP op (unlink for files/symlinks, rmdir for empty dirs). Recursive directory delete is intentionally NOT supported -- for that, use ssh_exec with `rm -rf` explicitly so the destructive intent is visible in the tool trace.",
|
|
1554
1568
|
{
|
|
1555
1569
|
...connectionParams,
|
|
1556
|
-
path:
|
|
1570
|
+
path: AbsoluteRemotePathSchema.describe(
|
|
1571
|
+
"Absolute path of the file or empty directory to delete. Must start with /."
|
|
1572
|
+
)
|
|
1557
1573
|
},
|
|
1558
1574
|
async ({ path, ...conn }) => {
|
|
1559
1575
|
return connectionPool.withConnection(conn, async (client) => {
|
|
@@ -1828,8 +1844,7 @@ ${files.join("\n")}` }] };
|
|
|
1828
1844
|
}
|
|
1829
1845
|
|
|
1830
1846
|
// src/server.ts
|
|
1831
|
-
var
|
|
1832
|
-
var { version } = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1847
|
+
var version = typeof __VERSION__ !== "undefined" ? __VERSION__ : JSON.parse(readFileSync4(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
1833
1848
|
function createServer(pool) {
|
|
1834
1849
|
const server = new McpServer({
|
|
1835
1850
|
name: "ssh-mcp",
|
|
@@ -1875,5 +1890,6 @@ export {
|
|
|
1875
1890
|
tail,
|
|
1876
1891
|
testConnection,
|
|
1877
1892
|
uploadFile,
|
|
1893
|
+
version,
|
|
1878
1894
|
writeFile
|
|
1879
1895
|
};
|
package/package.json
CHANGED
|
@@ -1,62 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yawlabs/ssh-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"mcpName": "io.github.YawLabs/ssh-mcp",
|
|
5
|
-
"description": "MCP server for SSH operations with built-in diagnostics",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"ssh-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"import": "./dist/server.js",
|
|
13
|
-
"types": "./dist/server.d.ts"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"LICENSE",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsup",
|
|
23
|
-
"dev": "tsup --watch",
|
|
24
|
-
"lint": "biome check src/",
|
|
25
|
-
"lint:fix": "biome check --write src/",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"test": "vitest run",
|
|
28
|
-
"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",
|
|
29
|
-
"test:ci": "npm run build && npm test",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
31
|
-
},
|
|
32
|
-
"keywords": [
|
|
33
|
-
"mcp",
|
|
34
|
-
"ssh",
|
|
35
|
-
"remote",
|
|
36
|
-
"model-context-protocol",
|
|
37
|
-
"ai",
|
|
38
|
-
"devops"
|
|
39
|
-
],
|
|
40
|
-
"author": "Yaw Labs <contact@yaw.sh>",
|
|
41
|
-
"license": "MIT",
|
|
42
|
-
"repository": {
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/YawLabs/ssh-mcp.git"
|
|
45
|
-
},
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=18"
|
|
48
|
-
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
|
-
"ssh2": "^1.17.0",
|
|
52
|
-
"zod": "^4.4.3"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@biomejs/biome": "^2.4.15",
|
|
56
|
-
"@types/node": "^
|
|
57
|
-
"@types/ssh2": "^1.15.5",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@yawlabs/ssh-mcp",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"mcpName": "io.github.YawLabs/ssh-mcp",
|
|
5
|
+
"description": "MCP server for SSH operations with built-in diagnostics",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ssh-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/server.js",
|
|
13
|
+
"types": "./dist/server.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"lint": "biome check src/",
|
|
25
|
+
"lint:fix": "biome check --write src/",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"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",
|
|
29
|
+
"test:ci": "npm run build && npm test",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"ssh",
|
|
35
|
+
"remote",
|
|
36
|
+
"model-context-protocol",
|
|
37
|
+
"ai",
|
|
38
|
+
"devops"
|
|
39
|
+
],
|
|
40
|
+
"author": "Yaw Labs <contact@yaw.sh>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/YawLabs/ssh-mcp.git"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
|
+
"ssh2": "^1.17.0",
|
|
52
|
+
"zod": "^4.4.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^2.4.15",
|
|
56
|
+
"@types/node": "^26.1.1",
|
|
57
|
+
"@types/ssh2": "^1.15.5",
|
|
58
|
+
"esbuild": "^0.28.1",
|
|
59
|
+
"postject": "^1.0.0-alpha.6",
|
|
60
|
+
"tsup": "^8.5.1",
|
|
61
|
+
"typescript": "^7.0.2",
|
|
62
|
+
"vitest": "^4.1.6"
|
|
63
|
+
},
|
|
64
|
+
"overrides": {
|
|
65
|
+
"esbuild": "^0.28.1"
|
|
66
|
+
}
|
|
67
|
+
}
|