@yawlabs/ssh-mcp 0.5.0 → 0.6.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 +67 -20
- package/dist/server.d.ts +4 -1
- package/dist/server.js +67 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,16 +15,21 @@ import { existsSync, readFileSync, readdirSync } from "fs";
|
|
|
15
15
|
import { homedir } from "os";
|
|
16
16
|
import { join } from "path";
|
|
17
17
|
function isValidHostname(host) {
|
|
18
|
-
|
|
18
|
+
if (host.length === 0 || host.length > 253) return false;
|
|
19
|
+
if (host.startsWith("[")) {
|
|
20
|
+
return /^\[[0-9a-fA-F:]+\]$/.test(host);
|
|
21
|
+
}
|
|
22
|
+
return /^[a-zA-Z0-9._\-]+$/.test(host);
|
|
19
23
|
}
|
|
20
24
|
function runArgs(cmd, args) {
|
|
21
25
|
try {
|
|
22
26
|
const stdout = execFileSync(cmd, args, { encoding: "utf8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
|
|
23
27
|
return { stdout: stdout.trim(), ok: true };
|
|
24
28
|
} catch (e) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
29
|
+
const err = e;
|
|
30
|
+
const stdout = err.stdout?.toString().trim() || "";
|
|
31
|
+
const stderr = err.stderr?.toString().trim() || "";
|
|
32
|
+
const output = [stdout, stderr].filter(Boolean).join("\n") || err.message || "";
|
|
28
33
|
return { stdout: output, ok: false };
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -42,12 +47,10 @@ ${stdout2}` };
|
|
|
42
47
|
message: "Windows OpenSSH agent is running but has no keys loaded. Run: ssh-add <key-path>"
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
}
|
|
50
|
+
return {
|
|
51
|
+
status: "error",
|
|
52
|
+
message: "Windows OpenSSH Authentication Agent is not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
|
|
53
|
+
};
|
|
51
54
|
}
|
|
52
55
|
if (!sock) {
|
|
53
56
|
return {
|
|
@@ -111,6 +114,9 @@ function checkSshKeys() {
|
|
|
111
114
|
return { status: "ok", message: `Found SSH keys: ${found.join(", ")}` };
|
|
112
115
|
}
|
|
113
116
|
function checkKnownHosts(host) {
|
|
117
|
+
if (!isValidHostname(host)) {
|
|
118
|
+
return { status: "error", message: `Invalid hostname: "${host}"` };
|
|
119
|
+
}
|
|
114
120
|
const knownHostsPath = join(homedir(), ".ssh", "known_hosts");
|
|
115
121
|
if (!existsSync(knownHostsPath)) {
|
|
116
122
|
return {
|
|
@@ -118,9 +124,6 @@ function checkKnownHosts(host) {
|
|
|
118
124
|
message: "~/.ssh/known_hosts does not exist. First connection to any host will prompt for verification."
|
|
119
125
|
};
|
|
120
126
|
}
|
|
121
|
-
if (!isValidHostname(host)) {
|
|
122
|
-
return { status: "error", message: `Invalid hostname: "${host}"` };
|
|
123
|
-
}
|
|
124
127
|
const { stdout, ok } = runArgs("ssh-keygen", ["-F", host]);
|
|
125
128
|
if (!ok || !stdout.trim()) {
|
|
126
129
|
return {
|
|
@@ -423,9 +426,21 @@ function getSftp(client) {
|
|
|
423
426
|
});
|
|
424
427
|
});
|
|
425
428
|
}
|
|
426
|
-
|
|
429
|
+
var DEFAULT_MAX_READ_BYTES = 10 * 1024 * 1024;
|
|
430
|
+
async function readFile(client, remotePath, maxBytes = DEFAULT_MAX_READ_BYTES) {
|
|
427
431
|
const sftp = await getSftp(client);
|
|
428
432
|
try {
|
|
433
|
+
const stats = await new Promise((resolve, reject) => {
|
|
434
|
+
sftp.stat(remotePath, (err, stats2) => {
|
|
435
|
+
if (err) return reject(err);
|
|
436
|
+
resolve(stats2);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
if (stats.size > maxBytes) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`File is ${(stats.size / 1024 / 1024).toFixed(1)} MB, exceeds ${(maxBytes / 1024 / 1024).toFixed(0)} MB limit. Use ssh_exec with head/tail to read a portion.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
429
444
|
return await new Promise((resolve, reject) => {
|
|
430
445
|
sftp.readFile(remotePath, (err, data) => {
|
|
431
446
|
if (err) return reject(err);
|
|
@@ -493,8 +508,10 @@ async function listDir(client, remotePath) {
|
|
|
493
508
|
var ConnectionPool = class {
|
|
494
509
|
entries = /* @__PURE__ */ new Map();
|
|
495
510
|
idleTtlMs;
|
|
511
|
+
maxPoolSize;
|
|
496
512
|
constructor(options) {
|
|
497
513
|
this.idleTtlMs = options?.idleTtlMs ?? 6e4;
|
|
514
|
+
this.maxPoolSize = options?.maxPoolSize ?? 100;
|
|
498
515
|
}
|
|
499
516
|
async acquire(config) {
|
|
500
517
|
const resolved = resolveConfig(config);
|
|
@@ -512,6 +529,24 @@ var ConnectionPool = class {
|
|
|
512
529
|
if (existing?.dead) {
|
|
513
530
|
this.entries.delete(key);
|
|
514
531
|
}
|
|
532
|
+
if (this.entries.size >= this.maxPoolSize) {
|
|
533
|
+
let evicted = false;
|
|
534
|
+
for (const [k, e] of this.entries) {
|
|
535
|
+
if (e.refCount === 0) {
|
|
536
|
+
if (e.idleTimer) clearTimeout(e.idleTimer);
|
|
537
|
+
try {
|
|
538
|
+
e.client.end();
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
this.entries.delete(k);
|
|
542
|
+
evicted = true;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!evicted) {
|
|
547
|
+
throw new Error(`Connection pool is full (${this.maxPoolSize} active connections)`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
515
550
|
try {
|
|
516
551
|
const client = await connectWithProxy(resolved);
|
|
517
552
|
const entry = { client, key, refCount: 1, idleTimer: null, dead: false };
|
|
@@ -924,7 +959,18 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
924
959
|
};
|
|
925
960
|
});
|
|
926
961
|
}
|
|
962
|
+
var VALID_FIND_SIZE = /^\d+[kMGTP]?$/;
|
|
927
963
|
async function find(client, options, timeoutMs = 3e4) {
|
|
964
|
+
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "1M", "100k")`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (options.maxsize && !VALID_FIND_SIZE.test(options.maxsize)) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "10M", "500k")`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
928
974
|
const args = [shellQuote(options.path)];
|
|
929
975
|
if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
|
|
930
976
|
if (options.type) args.push("-type", options.type);
|
|
@@ -967,11 +1013,11 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
967
1013
|
|
|
968
1014
|
// src/tools.ts
|
|
969
1015
|
var HostSchema = z.string().describe("SSH hostname or IP address");
|
|
970
|
-
var PortSchema = z.number().optional().describe("SSH port (default: 22)");
|
|
1016
|
+
var PortSchema = z.number().int().min(1).max(65535).optional().describe("SSH port (default: 22)");
|
|
971
1017
|
var UsernameSchema = z.string().optional().describe("SSH username (default: current user)");
|
|
972
1018
|
var KeyPathSchema = z.string().optional().describe("Path to SSH private key");
|
|
973
1019
|
var PasswordSchema = z.string().optional().describe("SSH password (prefer keys)");
|
|
974
|
-
var TimeoutSchema = z.number().optional().describe("Command timeout in milliseconds (default: 30000)");
|
|
1020
|
+
var TimeoutSchema = z.number().int().positive().optional().describe("Command timeout in milliseconds (default: 30000)");
|
|
975
1021
|
var connectionParams = {
|
|
976
1022
|
host: HostSchema,
|
|
977
1023
|
port: PortSchema,
|
|
@@ -1099,7 +1145,7 @@ ${result.stderr}`);
|
|
|
1099
1145
|
lines.push(` - ${s}`);
|
|
1100
1146
|
}
|
|
1101
1147
|
}
|
|
1102
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1148
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: report.overall === "error" };
|
|
1103
1149
|
}
|
|
1104
1150
|
);
|
|
1105
1151
|
server.tool(
|
|
@@ -1236,13 +1282,14 @@ ${result.stderr}`);
|
|
|
1236
1282
|
{
|
|
1237
1283
|
hosts: z.array(z.string()).describe("List of SSH hostnames or IPs"),
|
|
1238
1284
|
command: z.string().describe("Shell command to execute on all hosts"),
|
|
1285
|
+
port: PortSchema,
|
|
1239
1286
|
username: UsernameSchema,
|
|
1240
1287
|
privateKeyPath: KeyPathSchema,
|
|
1241
1288
|
password: PasswordSchema,
|
|
1242
1289
|
timeout: TimeoutSchema
|
|
1243
1290
|
},
|
|
1244
|
-
async ({ hosts, command, username, privateKeyPath, password, timeout }) => {
|
|
1245
|
-
const hostConfigs = hosts.map((host) => ({ host, username, privateKeyPath, password }));
|
|
1291
|
+
async ({ hosts, command, port, username, privateKeyPath, password, timeout }) => {
|
|
1292
|
+
const hostConfigs = hosts.map((host) => ({ host, port, username, privateKeyPath, password }));
|
|
1246
1293
|
const results = await multiExec(connectionPool, hostConfigs, command, timeout || 3e4);
|
|
1247
1294
|
const lines = [];
|
|
1248
1295
|
for (const r of results) {
|
|
@@ -1353,7 +1400,7 @@ ${files.join("\n")}` }] };
|
|
|
1353
1400
|
function createServer(pool) {
|
|
1354
1401
|
const server = new McpServer({
|
|
1355
1402
|
name: "ssh-mcp",
|
|
1356
|
-
version: "0.
|
|
1403
|
+
version: "0.6.0"
|
|
1357
1404
|
});
|
|
1358
1405
|
registerTools(server, pool);
|
|
1359
1406
|
return server;
|
package/dist/server.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ declare function connectRaw(connectConfig: ConnectConfig): Promise<Client>;
|
|
|
24
24
|
declare function connectWithProxy(resolved: ResolvedConfig): Promise<Client>;
|
|
25
25
|
declare function connect(config: SSHConfig): Promise<Client>;
|
|
26
26
|
declare function exec(client: Client, command: string, timeoutMs?: number): Promise<ExecResult>;
|
|
27
|
-
declare function readFile(client: Client, remotePath: string): Promise<string>;
|
|
27
|
+
declare function readFile(client: Client, remotePath: string, maxBytes?: number): Promise<string>;
|
|
28
28
|
declare function writeFile(client: Client, remotePath: string, content: string): Promise<void>;
|
|
29
29
|
declare function uploadFile(client: Client, localPath: string, remotePath: string): Promise<void>;
|
|
30
30
|
declare function downloadFile(client: Client, remotePath: string, localPath: string): Promise<void>;
|
|
@@ -33,10 +33,13 @@ declare function listDir(client: Client, remotePath: string): Promise<string[]>;
|
|
|
33
33
|
interface PoolOptions {
|
|
34
34
|
/** Milliseconds before an idle connection is closed. Default: 60000 (60s) */
|
|
35
35
|
idleTtlMs?: number;
|
|
36
|
+
/** Maximum number of connections in the pool. Default: 100 */
|
|
37
|
+
maxPoolSize?: number;
|
|
36
38
|
}
|
|
37
39
|
declare class ConnectionPool {
|
|
38
40
|
private entries;
|
|
39
41
|
private idleTtlMs;
|
|
42
|
+
private maxPoolSize;
|
|
40
43
|
constructor(options?: PoolOptions);
|
|
41
44
|
acquire(config: SSHConfig): Promise<Client>;
|
|
42
45
|
release(client: Client): void;
|
package/dist/server.js
CHANGED
|
@@ -10,16 +10,21 @@ import { existsSync, readFileSync, readdirSync } from "fs";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
function isValidHostname(host) {
|
|
13
|
-
|
|
13
|
+
if (host.length === 0 || host.length > 253) return false;
|
|
14
|
+
if (host.startsWith("[")) {
|
|
15
|
+
return /^\[[0-9a-fA-F:]+\]$/.test(host);
|
|
16
|
+
}
|
|
17
|
+
return /^[a-zA-Z0-9._\-]+$/.test(host);
|
|
14
18
|
}
|
|
15
19
|
function runArgs(cmd, args) {
|
|
16
20
|
try {
|
|
17
21
|
const stdout = execFileSync(cmd, args, { encoding: "utf8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
|
|
18
22
|
return { stdout: stdout.trim(), ok: true };
|
|
19
23
|
} catch (e) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
24
|
+
const err = e;
|
|
25
|
+
const stdout = err.stdout?.toString().trim() || "";
|
|
26
|
+
const stderr = err.stderr?.toString().trim() || "";
|
|
27
|
+
const output = [stdout, stderr].filter(Boolean).join("\n") || err.message || "";
|
|
23
28
|
return { stdout: output, ok: false };
|
|
24
29
|
}
|
|
25
30
|
}
|
|
@@ -37,12 +42,10 @@ ${stdout2}` };
|
|
|
37
42
|
message: "Windows OpenSSH agent is running but has no keys loaded. Run: ssh-add <key-path>"
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
}
|
|
45
|
+
return {
|
|
46
|
+
status: "error",
|
|
47
|
+
message: "Windows OpenSSH Authentication Agent is not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
|
|
48
|
+
};
|
|
46
49
|
}
|
|
47
50
|
if (!sock) {
|
|
48
51
|
return {
|
|
@@ -106,6 +109,9 @@ function checkSshKeys() {
|
|
|
106
109
|
return { status: "ok", message: `Found SSH keys: ${found.join(", ")}` };
|
|
107
110
|
}
|
|
108
111
|
function checkKnownHosts(host) {
|
|
112
|
+
if (!isValidHostname(host)) {
|
|
113
|
+
return { status: "error", message: `Invalid hostname: "${host}"` };
|
|
114
|
+
}
|
|
109
115
|
const knownHostsPath = join(homedir(), ".ssh", "known_hosts");
|
|
110
116
|
if (!existsSync(knownHostsPath)) {
|
|
111
117
|
return {
|
|
@@ -113,9 +119,6 @@ function checkKnownHosts(host) {
|
|
|
113
119
|
message: "~/.ssh/known_hosts does not exist. First connection to any host will prompt for verification."
|
|
114
120
|
};
|
|
115
121
|
}
|
|
116
|
-
if (!isValidHostname(host)) {
|
|
117
|
-
return { status: "error", message: `Invalid hostname: "${host}"` };
|
|
118
|
-
}
|
|
119
122
|
const { stdout, ok } = runArgs("ssh-keygen", ["-F", host]);
|
|
120
123
|
if (!ok || !stdout.trim()) {
|
|
121
124
|
return {
|
|
@@ -732,9 +735,21 @@ function getSftp(client) {
|
|
|
732
735
|
});
|
|
733
736
|
});
|
|
734
737
|
}
|
|
735
|
-
|
|
738
|
+
var DEFAULT_MAX_READ_BYTES = 10 * 1024 * 1024;
|
|
739
|
+
async function readFile(client, remotePath, maxBytes = DEFAULT_MAX_READ_BYTES) {
|
|
736
740
|
const sftp = await getSftp(client);
|
|
737
741
|
try {
|
|
742
|
+
const stats = await new Promise((resolve, reject) => {
|
|
743
|
+
sftp.stat(remotePath, (err, stats2) => {
|
|
744
|
+
if (err) return reject(err);
|
|
745
|
+
resolve(stats2);
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
if (stats.size > maxBytes) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`File is ${(stats.size / 1024 / 1024).toFixed(1)} MB, exceeds ${(maxBytes / 1024 / 1024).toFixed(0)} MB limit. Use ssh_exec with head/tail to read a portion.`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
738
753
|
return await new Promise((resolve, reject) => {
|
|
739
754
|
sftp.readFile(remotePath, (err, data) => {
|
|
740
755
|
if (err) return reject(err);
|
|
@@ -824,7 +839,18 @@ async function multiExec(pool, hosts, command, timeoutMs = 3e4) {
|
|
|
824
839
|
};
|
|
825
840
|
});
|
|
826
841
|
}
|
|
842
|
+
var VALID_FIND_SIZE = /^\d+[kMGTP]?$/;
|
|
827
843
|
async function find(client, options, timeoutMs = 3e4) {
|
|
844
|
+
if (options.minsize && !VALID_FIND_SIZE.test(options.minsize)) {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`Invalid minsize format: "${options.minsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "1M", "100k")`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (options.maxsize && !VALID_FIND_SIZE.test(options.maxsize)) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "10M", "500k")`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
828
854
|
const args = [shellQuote(options.path)];
|
|
829
855
|
if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
|
|
830
856
|
if (options.type) args.push("-type", options.type);
|
|
@@ -869,8 +895,10 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
|
869
895
|
var ConnectionPool = class {
|
|
870
896
|
entries = /* @__PURE__ */ new Map();
|
|
871
897
|
idleTtlMs;
|
|
898
|
+
maxPoolSize;
|
|
872
899
|
constructor(options) {
|
|
873
900
|
this.idleTtlMs = options?.idleTtlMs ?? 6e4;
|
|
901
|
+
this.maxPoolSize = options?.maxPoolSize ?? 100;
|
|
874
902
|
}
|
|
875
903
|
async acquire(config) {
|
|
876
904
|
const resolved = resolveConfig(config);
|
|
@@ -888,6 +916,24 @@ var ConnectionPool = class {
|
|
|
888
916
|
if (existing?.dead) {
|
|
889
917
|
this.entries.delete(key);
|
|
890
918
|
}
|
|
919
|
+
if (this.entries.size >= this.maxPoolSize) {
|
|
920
|
+
let evicted = false;
|
|
921
|
+
for (const [k, e] of this.entries) {
|
|
922
|
+
if (e.refCount === 0) {
|
|
923
|
+
if (e.idleTimer) clearTimeout(e.idleTimer);
|
|
924
|
+
try {
|
|
925
|
+
e.client.end();
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
this.entries.delete(k);
|
|
929
|
+
evicted = true;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (!evicted) {
|
|
934
|
+
throw new Error(`Connection pool is full (${this.maxPoolSize} active connections)`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
891
937
|
try {
|
|
892
938
|
const client = await connectWithProxy(resolved);
|
|
893
939
|
const entry = { client, key, refCount: 1, idleTimer: null, dead: false };
|
|
@@ -978,11 +1024,11 @@ ${diag}`);
|
|
|
978
1024
|
|
|
979
1025
|
// src/tools.ts
|
|
980
1026
|
var HostSchema = z.string().describe("SSH hostname or IP address");
|
|
981
|
-
var PortSchema = z.number().optional().describe("SSH port (default: 22)");
|
|
1027
|
+
var PortSchema = z.number().int().min(1).max(65535).optional().describe("SSH port (default: 22)");
|
|
982
1028
|
var UsernameSchema = z.string().optional().describe("SSH username (default: current user)");
|
|
983
1029
|
var KeyPathSchema = z.string().optional().describe("Path to SSH private key");
|
|
984
1030
|
var PasswordSchema = z.string().optional().describe("SSH password (prefer keys)");
|
|
985
|
-
var TimeoutSchema = z.number().optional().describe("Command timeout in milliseconds (default: 30000)");
|
|
1031
|
+
var TimeoutSchema = z.number().int().positive().optional().describe("Command timeout in milliseconds (default: 30000)");
|
|
986
1032
|
var connectionParams = {
|
|
987
1033
|
host: HostSchema,
|
|
988
1034
|
port: PortSchema,
|
|
@@ -1110,7 +1156,7 @@ ${result.stderr}`);
|
|
|
1110
1156
|
lines.push(` - ${s}`);
|
|
1111
1157
|
}
|
|
1112
1158
|
}
|
|
1113
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1159
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: report.overall === "error" };
|
|
1114
1160
|
}
|
|
1115
1161
|
);
|
|
1116
1162
|
server.tool(
|
|
@@ -1247,13 +1293,14 @@ ${result.stderr}`);
|
|
|
1247
1293
|
{
|
|
1248
1294
|
hosts: z.array(z.string()).describe("List of SSH hostnames or IPs"),
|
|
1249
1295
|
command: z.string().describe("Shell command to execute on all hosts"),
|
|
1296
|
+
port: PortSchema,
|
|
1250
1297
|
username: UsernameSchema,
|
|
1251
1298
|
privateKeyPath: KeyPathSchema,
|
|
1252
1299
|
password: PasswordSchema,
|
|
1253
1300
|
timeout: TimeoutSchema
|
|
1254
1301
|
},
|
|
1255
|
-
async ({ hosts, command, username, privateKeyPath, password, timeout }) => {
|
|
1256
|
-
const hostConfigs = hosts.map((host) => ({ host, username, privateKeyPath, password }));
|
|
1302
|
+
async ({ hosts, command, port, username, privateKeyPath, password, timeout }) => {
|
|
1303
|
+
const hostConfigs = hosts.map((host) => ({ host, port, username, privateKeyPath, password }));
|
|
1257
1304
|
const results = await multiExec(connectionPool, hostConfigs, command, timeout || 3e4);
|
|
1258
1305
|
const lines = [];
|
|
1259
1306
|
for (const r of results) {
|
|
@@ -1364,7 +1411,7 @@ ${files.join("\n")}` }] };
|
|
|
1364
1411
|
function createServer(pool) {
|
|
1365
1412
|
const server = new McpServer({
|
|
1366
1413
|
name: "ssh-mcp",
|
|
1367
|
-
version: "0.
|
|
1414
|
+
version: "0.6.0"
|
|
1368
1415
|
});
|
|
1369
1416
|
registerTools(server, pool);
|
|
1370
1417
|
return server;
|