@yawlabs/ssh-mcp 0.11.5 → 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 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
- connectConfig.privateKey = readFileSync3(config.privateKeyPath);
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(localPath, remotePath, (err) => {
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, localPath, (err) => {
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+[kMGTP]?$/;
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/T/P (e.g. "1M", "100k")`
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/T/P (e.g. "10M", "500k")`
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");
@@ -1453,7 +1465,7 @@ ${result.stderr}`);
1453
1465
  "Write content to a file on a remote host via SFTP. Creates or overwrites the file.",
1454
1466
  {
1455
1467
  ...connectionParams,
1456
- path: z.string().describe("Absolute path to the remote file"),
1468
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
1457
1469
  content: z.string().describe("File content to write")
1458
1470
  },
1459
1471
  async ({ path, content, ...conn }) => {
@@ -1469,7 +1481,7 @@ ${result.stderr}`);
1469
1481
  {
1470
1482
  ...connectionParams,
1471
1483
  localPath: z.string().describe("Path to the local file to upload"),
1472
- remotePath: z.string().describe("Absolute path on the remote host")
1484
+ remotePath: AbsoluteRemotePathSchema.describe("Absolute path on the remote host. Must start with /.")
1473
1485
  },
1474
1486
  async ({ localPath, remotePath, ...conn }) => {
1475
1487
  return connectionPool.withConnection(conn, async (client) => {
@@ -1483,7 +1495,7 @@ ${result.stderr}`);
1483
1495
  "Download a file from a remote host to local filesystem via SFTP.",
1484
1496
  {
1485
1497
  ...connectionParams,
1486
- remotePath: z.string().describe("Absolute path to the remote file"),
1498
+ remotePath: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
1487
1499
  localPath: z.string().describe("Local path to save the downloaded file")
1488
1500
  },
1489
1501
  async ({ remotePath, localPath, ...conn }) => {
@@ -1498,7 +1510,7 @@ ${result.stderr}`);
1498
1510
  "List files in a directory on a remote host via SFTP.",
1499
1511
  {
1500
1512
  ...connectionParams,
1501
- path: z.string().describe("Absolute path to the remote directory")
1513
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote directory. Must start with /.")
1502
1514
  },
1503
1515
  async ({ path, ...conn }) => {
1504
1516
  return connectionPool.withConnection(conn, async (client) => {
@@ -1512,7 +1524,7 @@ ${result.stderr}`);
1512
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.",
1513
1525
  {
1514
1526
  ...connectionParams,
1515
- path: z.string().describe("Absolute path to the remote file or directory")
1527
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file or directory. Must start with /.")
1516
1528
  },
1517
1529
  async ({ path, ...conn }) => {
1518
1530
  return connectionPool.withConnection(conn, async (client) => {
@@ -1549,7 +1561,9 @@ ${result.stderr}`);
1549
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.",
1550
1562
  {
1551
1563
  ...connectionParams,
1552
- path: z.string().describe("Absolute path of the file or empty directory to delete")
1564
+ path: AbsoluteRemotePathSchema.describe(
1565
+ "Absolute path of the file or empty directory to delete. Must start with /."
1566
+ )
1553
1567
  },
1554
1568
  async ({ path, ...conn }) => {
1555
1569
  return connectionPool.withConnection(conn, async (client) => {
@@ -1824,8 +1838,7 @@ ${files.join("\n")}` }] };
1824
1838
  }
1825
1839
 
1826
1840
  // src/server.ts
1827
- var pkgPath = join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1828
- 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;
1829
1842
  function createServer(pool) {
1830
1843
  const server = new McpServer({
1831
1844
  name: "ssh-mcp",
@@ -1836,6 +1849,10 @@ function createServer(pool) {
1836
1849
  }
1837
1850
 
1838
1851
  // src/index.ts
1852
+ if (process.argv[2] === "--version" || process.argv[2] === "version") {
1853
+ console.log(version);
1854
+ process.exit(0);
1855
+ }
1839
1856
  async function main() {
1840
1857
  const pool = new ConnectionPool();
1841
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
- connectConfig.privateKey = readFileSync3(config.privateKeyPath);
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(localPath, remotePath, (err) => {
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, localPath, (err) => {
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+[kMGTP]?$/;
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/T/P (e.g. "1M", "100k")`
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/T/P (e.g. "10M", "500k")`
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");
@@ -1459,7 +1471,7 @@ ${result.stderr}`);
1459
1471
  "Write content to a file on a remote host via SFTP. Creates or overwrites the file.",
1460
1472
  {
1461
1473
  ...connectionParams,
1462
- path: z.string().describe("Absolute path to the remote file"),
1474
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
1463
1475
  content: z.string().describe("File content to write")
1464
1476
  },
1465
1477
  async ({ path, content, ...conn }) => {
@@ -1475,7 +1487,7 @@ ${result.stderr}`);
1475
1487
  {
1476
1488
  ...connectionParams,
1477
1489
  localPath: z.string().describe("Path to the local file to upload"),
1478
- remotePath: z.string().describe("Absolute path on the remote host")
1490
+ remotePath: AbsoluteRemotePathSchema.describe("Absolute path on the remote host. Must start with /.")
1479
1491
  },
1480
1492
  async ({ localPath, remotePath, ...conn }) => {
1481
1493
  return connectionPool.withConnection(conn, async (client) => {
@@ -1489,7 +1501,7 @@ ${result.stderr}`);
1489
1501
  "Download a file from a remote host to local filesystem via SFTP.",
1490
1502
  {
1491
1503
  ...connectionParams,
1492
- remotePath: z.string().describe("Absolute path to the remote file"),
1504
+ remotePath: AbsoluteRemotePathSchema.describe("Absolute path to the remote file. Must start with /."),
1493
1505
  localPath: z.string().describe("Local path to save the downloaded file")
1494
1506
  },
1495
1507
  async ({ remotePath, localPath, ...conn }) => {
@@ -1504,7 +1516,7 @@ ${result.stderr}`);
1504
1516
  "List files in a directory on a remote host via SFTP.",
1505
1517
  {
1506
1518
  ...connectionParams,
1507
- path: z.string().describe("Absolute path to the remote directory")
1519
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote directory. Must start with /.")
1508
1520
  },
1509
1521
  async ({ path, ...conn }) => {
1510
1522
  return connectionPool.withConnection(conn, async (client) => {
@@ -1518,7 +1530,7 @@ ${result.stderr}`);
1518
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.",
1519
1531
  {
1520
1532
  ...connectionParams,
1521
- path: z.string().describe("Absolute path to the remote file or directory")
1533
+ path: AbsoluteRemotePathSchema.describe("Absolute path to the remote file or directory. Must start with /.")
1522
1534
  },
1523
1535
  async ({ path, ...conn }) => {
1524
1536
  return connectionPool.withConnection(conn, async (client) => {
@@ -1555,7 +1567,9 @@ ${result.stderr}`);
1555
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.",
1556
1568
  {
1557
1569
  ...connectionParams,
1558
- path: z.string().describe("Absolute path of the file or empty directory to delete")
1570
+ path: AbsoluteRemotePathSchema.describe(
1571
+ "Absolute path of the file or empty directory to delete. Must start with /."
1572
+ )
1559
1573
  },
1560
1574
  async ({ path, ...conn }) => {
1561
1575
  return connectionPool.withConnection(conn, async (client) => {
@@ -1830,8 +1844,7 @@ ${files.join("\n")}` }] };
1830
1844
  }
1831
1845
 
1832
1846
  // src/server.ts
1833
- var pkgPath = join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1834
- 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;
1835
1848
  function createServer(pool) {
1836
1849
  const server = new McpServer({
1837
1850
  name: "ssh-mcp",
@@ -1877,5 +1890,6 @@ export {
1877
1890
  tail,
1878
1891
  testConnection,
1879
1892
  uploadFile,
1893
+ version,
1880
1894
  writeFile
1881
1895
  };
package/package.json CHANGED
@@ -1,62 +1,67 @@
1
- {
2
- "name": "@yawlabs/ssh-mcp",
3
- "version": "0.11.5",
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": "^25.7.0",
57
- "@types/ssh2": "^1.15.5",
58
- "tsup": "^8.5.1",
59
- "typescript": "^6.0.3",
60
- "vitest": "^4.1.6"
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
+ }