@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/README.md +219 -219
- package/dist/index.js +523 -492
- package/dist/server.d.ts +39 -39
- package/dist/server.js +32 -10
- package/package.json +61 -61
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,
|
|
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._
|
|
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,
|
|
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)
|
|
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]})` :
|
|
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
|
|
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.
|
|
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.
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^
|
|
55
|
-
"@types/node": "^
|
|
56
|
-
"@types/ssh2": "^1.15.4",
|
|
57
|
-
"tsup": "^8.5.1",
|
|
58
|
-
"typescript": "^
|
|
59
|
-
"vitest": "^
|
|
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
|
+
}
|