@yawlabs/ssh-mcp 0.8.0 → 0.9.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/server.d.ts CHANGED
@@ -57,8 +57,6 @@ declare class ConnectionPool {
57
57
  get connectCount(): number;
58
58
  }
59
59
 
60
- declare function registerTools(server: McpServer, pool?: ConnectionPool): void;
61
-
62
60
  interface DiagnosticResult {
63
61
  status: "ok" | "warning" | "error";
64
62
  message: string;
@@ -77,43 +75,6 @@ declare function checkConnectivity(host: string, port?: number): DiagnosticResul
77
75
  declare function checkSshConfig(host: string): DiagnosticResult;
78
76
  declare function diagnose(host: string, port?: number): DiagnosticReport;
79
77
 
80
- interface MultiExecResult {
81
- host: string;
82
- stdout: string;
83
- stderr: string;
84
- code: number;
85
- error?: string;
86
- }
87
- interface MultiExecHost {
88
- host: string;
89
- port?: number;
90
- username?: string;
91
- privateKeyPath?: string;
92
- password?: string;
93
- }
94
- declare function multiExec(pool: ConnectionPool, hosts: MultiExecHost[], command: string, timeoutMs?: number): Promise<MultiExecResult[]>;
95
- interface FindOptions {
96
- path: string;
97
- name?: string;
98
- type?: "f" | "d" | "l";
99
- maxdepth?: number;
100
- minsize?: string;
101
- maxsize?: string;
102
- newer?: string;
103
- }
104
- declare function find(client: Client, options: FindOptions, timeoutMs?: number): Promise<string[]>;
105
- declare function tail(client: Client, path: string, lines?: number, grep?: string, timeoutMs?: number): Promise<string>;
106
- interface ServiceStatus {
107
- name: string;
108
- active: boolean;
109
- status: string;
110
- description?: string;
111
- since?: string;
112
- pid?: number;
113
- raw: string;
114
- }
115
- declare function serviceStatus(client: Client, serviceName: string, timeoutMs?: number): Promise<ServiceStatus>;
116
-
117
78
  interface KeyInfo {
118
79
  name: string;
119
80
  path: string;
@@ -167,6 +128,45 @@ declare function testConnection(host: string, port?: number): {
167
128
  message: string;
168
129
  };
169
130
 
131
+ interface MultiExecResult {
132
+ host: string;
133
+ stdout: string;
134
+ stderr: string;
135
+ code: number;
136
+ error?: string;
137
+ }
138
+ interface MultiExecHost {
139
+ host: string;
140
+ port?: number;
141
+ username?: string;
142
+ privateKeyPath?: string;
143
+ password?: string;
144
+ }
145
+ declare function multiExec(pool: ConnectionPool, hosts: MultiExecHost[], command: string, timeoutMs?: number): Promise<MultiExecResult[]>;
146
+ interface FindOptions {
147
+ path: string;
148
+ name?: string;
149
+ type?: "f" | "d" | "l";
150
+ maxdepth?: number;
151
+ minsize?: string;
152
+ maxsize?: string;
153
+ newer?: string;
154
+ }
155
+ declare function find(client: Client, options: FindOptions, timeoutMs?: number): Promise<string[]>;
156
+ declare function tail(client: Client, path: string, lines?: number, grep?: string, timeoutMs?: number): Promise<string>;
157
+ interface ServiceStatus {
158
+ name: string;
159
+ active: boolean;
160
+ status: string;
161
+ description?: string;
162
+ since?: string;
163
+ pid?: number;
164
+ raw: string;
165
+ }
166
+ declare function serviceStatus(client: Client, serviceName: string, timeoutMs?: number): Promise<ServiceStatus>;
167
+
168
+ declare function registerTools(server: McpServer, pool?: ConnectionPool): void;
169
+
170
170
  declare function createServer(pool?: ConnectionPool): McpServer;
171
171
 
172
172
  export { type AgentResult, type ConfigLookupResult, ConnectionPool, type DiagnosticReport, type DiagnosticResult, type ExecResult, 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, diagnose, downloadFile, ensureAgent, exec, find, fixKnownHosts, formatDiagnostics, listDir, listSshKeys, loadKey, multiExec, readFile, readKnownHostsKeys, registerTools, resolveConfig, serviceStatus, tail, testConnection, uploadFile, writeFile };
package/dist/server.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // src/server.ts
2
+ import { readFileSync as readFileSync4 } from "fs";
3
+ import { dirname, join as join4 } from "path";
4
+ import { fileURLToPath } from "url";
2
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
6
 
4
7
  // src/tools.ts
@@ -6,7 +9,7 @@ import { z } from "zod";
6
9
 
7
10
  // src/diagnose.ts
8
11
  import { execFileSync } from "child_process";
9
- import { existsSync, readFileSync, readdirSync } from "fs";
12
+ import { existsSync, readdirSync, readFileSync } from "fs";
10
13
  import { homedir } from "os";
11
14
  import { join } from "path";
12
15
  function isValidHostname(host) {
@@ -14,7 +17,7 @@ function isValidHostname(host) {
14
17
  if (host.startsWith("[")) {
15
18
  return /^\[[0-9a-fA-F:]+\]$/.test(host);
16
19
  }
17
- return /^[a-zA-Z0-9._\-]+$/.test(host);
20
+ return /^[a-zA-Z0-9._-]+$/.test(host);
18
21
  }
19
22
  function runArgs(cmd, args) {
20
23
  try {
@@ -256,7 +259,7 @@ function diagnose(host, port = 22) {
256
259
  }
257
260
 
258
261
  // src/env.ts
259
- import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync } from "fs";
262
+ import { appendFileSync, existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
260
263
  import { homedir as homedir2 } from "os";
261
264
  import { join as join2 } from "path";
262
265
  function probeAgent(socket, agentLabel) {
@@ -273,6 +276,7 @@ function probeAgent(socket, agentLabel) {
273
276
  message: keys.length > 0 ? `${agentLabel} running with ${keys.length} key(s) loaded` : `${agentLabel} running but no keys loaded. Use ssh_key_load to add one.`
274
277
  };
275
278
  }
279
+ var startedAgentPid = null;
276
280
  function ensureAgent() {
277
281
  const sock = process.env.SSH_AUTH_SOCK;
278
282
  if (sock) {
@@ -289,7 +293,10 @@ function ensureAgent() {
289
293
  const pidMatch = stdout.match(/SSH_AGENT_PID=([^;]+)/);
290
294
  if (sockMatch) {
291
295
  process.env.SSH_AUTH_SOCK = sockMatch[1];
292
- if (pidMatch) process.env.SSH_AGENT_PID = pidMatch[1];
296
+ if (pidMatch) {
297
+ process.env.SSH_AGENT_PID = pidMatch[1];
298
+ startedAgentPid = Number.parseInt(pidMatch[1], 10);
299
+ }
293
300
  return {
294
301
  running: true,
295
302
  reachable: true,
@@ -746,6 +753,7 @@ var DEFAULT_MAX_EXEC_BYTES = 10 * 1024 * 1024;
746
753
  function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTES) {
747
754
  return new Promise((resolve, reject) => {
748
755
  let settled = false;
756
+ let activeStream = null;
749
757
  const settle = (fn) => {
750
758
  if (settled) return;
751
759
  settled = true;
@@ -753,6 +761,16 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
753
761
  fn();
754
762
  };
755
763
  const timer = setTimeout(() => {
764
+ if (activeStream) {
765
+ try {
766
+ activeStream.signal("TERM");
767
+ } catch {
768
+ }
769
+ try {
770
+ activeStream.close();
771
+ } catch {
772
+ }
773
+ }
756
774
  settle(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)));
757
775
  }, timeoutMs);
758
776
  client.exec(command, (err, stream) => {
@@ -760,6 +778,7 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
760
778
  settle(() => reject(err));
761
779
  return;
762
780
  }
781
+ activeStream = stream;
763
782
  const stdoutChunks = [];
764
783
  const stderrChunks = [];
765
784
  let stdoutBytes = 0;
@@ -935,7 +954,7 @@ async function find(client, options, timeoutMs = 3e4) {
935
954
  `Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "10M", "500k")`
936
955
  );
937
956
  }
938
- const args = [shellQuote(options.path)];
957
+ const args = ["--", shellQuote(options.path)];
939
958
  if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
940
959
  if (options.type) args.push("-type", options.type);
941
960
  if (options.name) args.push("-name", shellQuote(options.name));
@@ -950,9 +969,9 @@ async function find(client, options, timeoutMs = 3e4) {
950
969
  return result.stdout.split("\n").filter(Boolean);
951
970
  }
952
971
  async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
953
- let command = `tail -n ${lines} ${shellQuote(path)}`;
972
+ let command = `tail -n ${lines} -- ${shellQuote(path)}`;
954
973
  if (grep) {
955
- command += ` | grep -i ${shellQuote(grep)}`;
974
+ command += ` | grep -i -e ${shellQuote(grep)}`;
956
975
  }
957
976
  const result = await exec(client, command, timeoutMs);
958
977
  if (result.stderr.trim()) {
@@ -961,16 +980,17 @@ async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
961
980
  return result.stdout;
962
981
  }
963
982
  async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
964
- const result = await exec(client, `systemctl status ${shellQuote(serviceName)} 2>&1`, timeoutMs);
983
+ const result = await exec(client, `systemctl status -- ${shellQuote(serviceName)} 2>&1`, timeoutMs);
965
984
  const raw = result.stdout;
966
985
  const activeMatch = raw.match(/Active:\s+(\S+)\s+\(([^)]+)\)/);
967
986
  const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
968
987
  const pidMatch = raw.match(/Main PID:\s+(\d+)/);
969
988
  const sinceMatch = raw.match(/since\s+(.+?);/);
989
+ const fallbackStatus = result.code === 0 ? "active" : "inactive";
970
990
  return {
971
991
  name: serviceName,
972
992
  active: activeMatch?.[1] === "active",
973
- status: activeMatch ? `${activeMatch[1]} (${activeMatch[2]})` : result.code === 0 ? "active" : "unknown",
993
+ status: activeMatch ? `${activeMatch[1]} (${activeMatch[2]})` : fallbackStatus,
974
994
  description: descMatch?.[1]?.trim(),
975
995
  since: sinceMatch?.[1]?.trim(),
976
996
  pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
@@ -1525,10 +1545,12 @@ ${files.join("\n")}` }] };
1525
1545
  }
1526
1546
 
1527
1547
  // src/server.ts
1548
+ var pkgPath = join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1549
+ var { version } = JSON.parse(readFileSync4(pkgPath, "utf8"));
1528
1550
  function createServer(pool) {
1529
1551
  const server = new McpServer({
1530
1552
  name: "ssh-mcp",
1531
- version: "0.7.0"
1553
+ version
1532
1554
  });
1533
1555
  registerTools(server, pool);
1534
1556
  return server;
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@yawlabs/ssh-mcp",
3
- "version": "0.8.0",
4
- "description": "MCP server for SSH operations with built-in diagnostics",
5
- "type": "module",
6
- "bin": {
7
- "ssh-mcp": "dist/index.js"
8
- },
9
- "exports": {
10
- ".": {
11
- "import": "./dist/server.js",
12
- "types": "./dist/server.d.ts"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "LICENSE",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "build": "tsup",
22
- "dev": "tsup --watch",
23
- "lint": "biome check src/",
24
- "lint:fix": "biome check --write src/",
25
- "typecheck": "tsc --noEmit",
26
- "test": "vitest run",
27
- "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",
28
- "test:ci": "npm run build && npm test",
29
- "prepublishOnly": "npm run build"
30
- },
31
- "keywords": [
32
- "mcp",
33
- "ssh",
34
- "remote",
35
- "model-context-protocol",
36
- "ai",
37
- "devops"
38
- ],
39
- "author": "Yaw Labs <contact@yaw.sh>",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/YawLabs/ssh-mcp.git"
44
- },
45
- "engines": {
46
- "node": ">=18"
47
- },
48
- "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.29.0",
50
- "ssh2": "^1.16.0",
51
- "zod": "^3.24.4"
52
- },
53
- "devDependencies": {
54
- "@biomejs/biome": "^1.9.4",
55
- "@types/node": "^22.15.2",
56
- "@types/ssh2": "^1.15.4",
57
- "tsup": "^8.5.1",
58
- "typescript": "^5.8.3",
59
- "vitest": "^3.2.4"
60
- }
61
- }
1
+ {
2
+ "name": "@yawlabs/ssh-mcp",
3
+ "version": "0.9.0",
4
+ "description": "MCP server for SSH operations with built-in diagnostics",
5
+ "type": "module",
6
+ "bin": {
7
+ "ssh-mcp": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/server.js",
12
+ "types": "./dist/server.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "dev": "tsup --watch",
23
+ "lint": "biome check src/",
24
+ "lint:fix": "biome check --write src/",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "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",
28
+ "test:ci": "npm run build && npm test",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "mcp",
33
+ "ssh",
34
+ "remote",
35
+ "model-context-protocol",
36
+ "ai",
37
+ "devops"
38
+ ],
39
+ "author": "Yaw Labs <contact@yaw.sh>",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/YawLabs/ssh-mcp.git"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
+ "ssh2": "^1.16.0",
51
+ "zod": "^4.3.6"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^2.4.13",
55
+ "@types/node": "^25.6.0",
56
+ "@types/ssh2": "^1.15.4",
57
+ "tsup": "^8.5.1",
58
+ "typescript": "^6.0.3",
59
+ "vitest": "^4.1.5"
60
+ }
61
+ }