@yawlabs/ssh-mcp 0.8.0 → 0.9.1
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 +464 -375
- package/dist/server.d.ts +40 -39
- package/dist/server.js +114 -43
- package/package.json +6 -6
package/dist/server.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ declare class ConnectionPool {
|
|
|
43
43
|
private idleTtlMs;
|
|
44
44
|
private maxPoolSize;
|
|
45
45
|
private _connectCount;
|
|
46
|
+
private drained;
|
|
46
47
|
constructor(options?: PoolOptions);
|
|
47
48
|
acquire(config: SSHConfig): Promise<Client>;
|
|
48
49
|
release(client: Client): void;
|
|
@@ -57,8 +58,6 @@ declare class ConnectionPool {
|
|
|
57
58
|
get connectCount(): number;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
declare function registerTools(server: McpServer, pool?: ConnectionPool): void;
|
|
61
|
-
|
|
62
61
|
interface DiagnosticResult {
|
|
63
62
|
status: "ok" | "warning" | "error";
|
|
64
63
|
message: string;
|
|
@@ -77,43 +76,6 @@ declare function checkConnectivity(host: string, port?: number): DiagnosticResul
|
|
|
77
76
|
declare function checkSshConfig(host: string): DiagnosticResult;
|
|
78
77
|
declare function diagnose(host: string, port?: number): DiagnosticReport;
|
|
79
78
|
|
|
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
79
|
interface KeyInfo {
|
|
118
80
|
name: string;
|
|
119
81
|
path: string;
|
|
@@ -167,6 +129,45 @@ declare function testConnection(host: string, port?: number): {
|
|
|
167
129
|
message: string;
|
|
168
130
|
};
|
|
169
131
|
|
|
132
|
+
interface MultiExecResult {
|
|
133
|
+
host: string;
|
|
134
|
+
stdout: string;
|
|
135
|
+
stderr: string;
|
|
136
|
+
code: number;
|
|
137
|
+
error?: string;
|
|
138
|
+
}
|
|
139
|
+
interface MultiExecHost {
|
|
140
|
+
host: string;
|
|
141
|
+
port?: number;
|
|
142
|
+
username?: string;
|
|
143
|
+
privateKeyPath?: string;
|
|
144
|
+
password?: string;
|
|
145
|
+
}
|
|
146
|
+
declare function multiExec(pool: ConnectionPool, hosts: MultiExecHost[], command: string, timeoutMs?: number): Promise<MultiExecResult[]>;
|
|
147
|
+
interface FindOptions {
|
|
148
|
+
path: string;
|
|
149
|
+
name?: string;
|
|
150
|
+
type?: "f" | "d" | "l";
|
|
151
|
+
maxdepth?: number;
|
|
152
|
+
minsize?: string;
|
|
153
|
+
maxsize?: string;
|
|
154
|
+
newer?: string;
|
|
155
|
+
}
|
|
156
|
+
declare function find(client: Client, options: FindOptions, timeoutMs?: number): Promise<string[]>;
|
|
157
|
+
declare function tail(client: Client, path: string, lines?: number, grep?: string, timeoutMs?: number): Promise<string>;
|
|
158
|
+
interface ServiceStatus {
|
|
159
|
+
name: string;
|
|
160
|
+
active: boolean;
|
|
161
|
+
status: string;
|
|
162
|
+
description?: string;
|
|
163
|
+
since?: string;
|
|
164
|
+
pid?: number;
|
|
165
|
+
raw: string;
|
|
166
|
+
}
|
|
167
|
+
declare function serviceStatus(client: Client, serviceName: string, timeoutMs?: number): Promise<ServiceStatus>;
|
|
168
|
+
|
|
169
|
+
declare function registerTools(server: McpServer, pool?: ConnectionPool): void;
|
|
170
|
+
|
|
170
171
|
declare function createServer(pool?: ConnectionPool): McpServer;
|
|
171
172
|
|
|
172
173
|
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,11 +259,63 @@ function diagnose(host, port = 22) {
|
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
// src/env.ts
|
|
259
|
-
import {
|
|
262
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
263
|
+
import { appendFileSync, existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
260
264
|
import { homedir as homedir2 } from "os";
|
|
261
265
|
import { join as join2 } from "path";
|
|
266
|
+
|
|
267
|
+
// src/ssh-config.ts
|
|
268
|
+
function parseSshConfigOutput(stdout) {
|
|
269
|
+
const all = {};
|
|
270
|
+
const identityFiles = [];
|
|
271
|
+
for (const line of stdout.split("\n")) {
|
|
272
|
+
const spaceIdx = line.indexOf(" ");
|
|
273
|
+
if (spaceIdx > 0) {
|
|
274
|
+
const key = line.substring(0, spaceIdx);
|
|
275
|
+
const value = line.substring(spaceIdx + 1);
|
|
276
|
+
if (key === "identityfile") {
|
|
277
|
+
identityFiles.push(value);
|
|
278
|
+
} else {
|
|
279
|
+
all[key] = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { all, identityFiles };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/env.ts
|
|
287
|
+
function runArgsWithEnv(cmd, args, extraEnv) {
|
|
288
|
+
const env = {};
|
|
289
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
290
|
+
if (typeof v === "string") env[k] = v;
|
|
291
|
+
}
|
|
292
|
+
for (const [k, v] of Object.entries(extraEnv)) {
|
|
293
|
+
if (v === void 0) {
|
|
294
|
+
delete env[k];
|
|
295
|
+
} else {
|
|
296
|
+
env[k] = v;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const stdout = execFileSync2(cmd, args, {
|
|
301
|
+
env,
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
timeout: 1e4,
|
|
304
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
305
|
+
});
|
|
306
|
+
return { stdout: stdout.trim(), ok: true };
|
|
307
|
+
} catch (e) {
|
|
308
|
+
const err = e;
|
|
309
|
+
const so = err.stdout?.toString().trim() || "";
|
|
310
|
+
const se = err.stderr?.toString().trim() || "";
|
|
311
|
+
const output = [so, se].filter(Boolean).join("\n") || err.message || "";
|
|
312
|
+
return { stdout: output, ok: false };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
262
315
|
function probeAgent(socket, agentLabel) {
|
|
263
|
-
const
|
|
316
|
+
const isWindowsNamedPipe = socket.startsWith("\\\\.\\pipe\\");
|
|
317
|
+
const extraEnv = isWindowsNamedPipe ? { SSH_AUTH_SOCK: void 0 } : { SSH_AUTH_SOCK: socket };
|
|
318
|
+
const { stdout, ok } = runArgsWithEnv("ssh-add", ["-l"], extraEnv);
|
|
264
319
|
const noIdentities = stdout.includes("no identities") || stdout.includes("The agent has no identities");
|
|
265
320
|
if (!ok && !noIdentities) return null;
|
|
266
321
|
const keys = ok && !noIdentities ? stdout.split("\n").filter(Boolean) : [];
|
|
@@ -273,6 +328,7 @@ function probeAgent(socket, agentLabel) {
|
|
|
273
328
|
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
329
|
};
|
|
275
330
|
}
|
|
331
|
+
var startedAgentPid = null;
|
|
276
332
|
function ensureAgent() {
|
|
277
333
|
const sock = process.env.SSH_AUTH_SOCK;
|
|
278
334
|
if (sock) {
|
|
@@ -289,7 +345,10 @@ function ensureAgent() {
|
|
|
289
345
|
const pidMatch = stdout.match(/SSH_AGENT_PID=([^;]+)/);
|
|
290
346
|
if (sockMatch) {
|
|
291
347
|
process.env.SSH_AUTH_SOCK = sockMatch[1];
|
|
292
|
-
if (pidMatch)
|
|
348
|
+
if (pidMatch) {
|
|
349
|
+
process.env.SSH_AGENT_PID = pidMatch[1];
|
|
350
|
+
startedAgentPid = Number.parseInt(pidMatch[1], 10);
|
|
351
|
+
}
|
|
293
352
|
return {
|
|
294
353
|
running: true,
|
|
295
354
|
reachable: true,
|
|
@@ -330,6 +389,13 @@ function detectKeyType(filePath, fileName) {
|
|
|
330
389
|
if (content.includes("RSA PRIVATE KEY")) return "rsa";
|
|
331
390
|
if (content.includes("EC PRIVATE KEY")) return "ecdsa";
|
|
332
391
|
if (content.includes("DSA PRIVATE KEY")) return "dsa";
|
|
392
|
+
if (content.includes("OPENSSH PRIVATE KEY")) {
|
|
393
|
+
const { stdout, ok } = runArgs("ssh-keygen", ["-l", "-f", filePath]);
|
|
394
|
+
if (ok) {
|
|
395
|
+
const match = stdout.match(/\(([^)]+)\)\s*$/);
|
|
396
|
+
if (match) return match[1].toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
333
399
|
} catch {
|
|
334
400
|
}
|
|
335
401
|
return "unknown";
|
|
@@ -404,20 +470,7 @@ function configLookup(host) {
|
|
|
404
470
|
if (!ok) {
|
|
405
471
|
return { error: `Failed to resolve SSH config for ${host}: ${stdout}` };
|
|
406
472
|
}
|
|
407
|
-
const all =
|
|
408
|
-
const identityFiles = [];
|
|
409
|
-
for (const line of stdout.split("\n")) {
|
|
410
|
-
const spaceIdx = line.indexOf(" ");
|
|
411
|
-
if (spaceIdx > 0) {
|
|
412
|
-
const key = line.substring(0, spaceIdx);
|
|
413
|
-
const value = line.substring(spaceIdx + 1);
|
|
414
|
-
if (key === "identityfile") {
|
|
415
|
-
identityFiles.push(value);
|
|
416
|
-
} else {
|
|
417
|
-
all[key] = value;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
473
|
+
const { all, identityFiles } = parseSshConfigOutput(stdout);
|
|
421
474
|
return {
|
|
422
475
|
hostname: all.hostname || host,
|
|
423
476
|
user: all.user || "",
|
|
@@ -548,26 +601,13 @@ function resolveFromSshConfig(host) {
|
|
|
548
601
|
try {
|
|
549
602
|
const { stdout, ok } = runArgs("ssh", ["-G", host]);
|
|
550
603
|
if (!ok) return null;
|
|
551
|
-
const
|
|
552
|
-
const identityFiles = [];
|
|
553
|
-
for (const line of stdout.split("\n")) {
|
|
554
|
-
const spaceIdx = line.indexOf(" ");
|
|
555
|
-
if (spaceIdx > 0) {
|
|
556
|
-
const key = line.substring(0, spaceIdx);
|
|
557
|
-
const value = line.substring(spaceIdx + 1);
|
|
558
|
-
if (key === "identityfile") {
|
|
559
|
-
identityFiles.push(value);
|
|
560
|
-
} else {
|
|
561
|
-
config[key] = value;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
604
|
+
const { all, identityFiles } = parseSshConfigOutput(stdout);
|
|
565
605
|
return {
|
|
566
|
-
hostname:
|
|
567
|
-
user:
|
|
568
|
-
port:
|
|
606
|
+
hostname: all.hostname || host,
|
|
607
|
+
user: all.user || "",
|
|
608
|
+
port: all.port || "22",
|
|
569
609
|
identityFiles,
|
|
570
|
-
proxyJump:
|
|
610
|
+
proxyJump: all.proxyjump && all.proxyjump !== "none" ? all.proxyjump : void 0
|
|
571
611
|
};
|
|
572
612
|
} catch {
|
|
573
613
|
return null;
|
|
@@ -746,6 +786,7 @@ var DEFAULT_MAX_EXEC_BYTES = 10 * 1024 * 1024;
|
|
|
746
786
|
function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTES) {
|
|
747
787
|
return new Promise((resolve, reject) => {
|
|
748
788
|
let settled = false;
|
|
789
|
+
let activeStream = null;
|
|
749
790
|
const settle = (fn) => {
|
|
750
791
|
if (settled) return;
|
|
751
792
|
settled = true;
|
|
@@ -753,6 +794,16 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
|
|
|
753
794
|
fn();
|
|
754
795
|
};
|
|
755
796
|
const timer = setTimeout(() => {
|
|
797
|
+
if (activeStream) {
|
|
798
|
+
try {
|
|
799
|
+
activeStream.signal("TERM");
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
activeStream.close();
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
756
807
|
settle(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)));
|
|
757
808
|
}, timeoutMs);
|
|
758
809
|
client.exec(command, (err, stream) => {
|
|
@@ -760,6 +811,7 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
|
|
|
760
811
|
settle(() => reject(err));
|
|
761
812
|
return;
|
|
762
813
|
}
|
|
814
|
+
activeStream = stream;
|
|
763
815
|
const stdoutChunks = [];
|
|
764
816
|
const stderrChunks = [];
|
|
765
817
|
let stdoutBytes = 0;
|
|
@@ -935,7 +987,7 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
935
987
|
`Invalid maxsize format: "${options.maxsize}". Expected: digits followed by optional k/M/G/T/P (e.g. "10M", "500k")`
|
|
936
988
|
);
|
|
937
989
|
}
|
|
938
|
-
const args = [shellQuote(options.path)];
|
|
990
|
+
const args = ["--", shellQuote(options.path)];
|
|
939
991
|
if (options.maxdepth !== void 0) args.push("-maxdepth", String(options.maxdepth));
|
|
940
992
|
if (options.type) args.push("-type", options.type);
|
|
941
993
|
if (options.name) args.push("-name", shellQuote(options.name));
|
|
@@ -950,9 +1002,9 @@ async function find(client, options, timeoutMs = 3e4) {
|
|
|
950
1002
|
return result.stdout.split("\n").filter(Boolean);
|
|
951
1003
|
}
|
|
952
1004
|
async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
|
|
953
|
-
let command = `tail -n ${lines} ${shellQuote(path)}`;
|
|
1005
|
+
let command = `tail -n ${lines} -- ${shellQuote(path)}`;
|
|
954
1006
|
if (grep) {
|
|
955
|
-
command += ` | grep -i ${shellQuote(grep)}`;
|
|
1007
|
+
command += ` | grep -i -e ${shellQuote(grep)}`;
|
|
956
1008
|
}
|
|
957
1009
|
const result = await exec(client, command, timeoutMs);
|
|
958
1010
|
if (result.stderr.trim()) {
|
|
@@ -961,16 +1013,17 @@ async function tail(client, path, lines = 100, grep, timeoutMs = 3e4) {
|
|
|
961
1013
|
return result.stdout;
|
|
962
1014
|
}
|
|
963
1015
|
async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
|
|
964
|
-
const result = await exec(client, `systemctl status ${shellQuote(serviceName)} 2>&1`, timeoutMs);
|
|
1016
|
+
const result = await exec(client, `systemctl status -- ${shellQuote(serviceName)} 2>&1`, timeoutMs);
|
|
965
1017
|
const raw = result.stdout;
|
|
966
1018
|
const activeMatch = raw.match(/Active:\s+(\S+)\s+\(([^)]+)\)/);
|
|
967
1019
|
const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
|
|
968
1020
|
const pidMatch = raw.match(/Main PID:\s+(\d+)/);
|
|
969
1021
|
const sinceMatch = raw.match(/since\s+(.+?);/);
|
|
1022
|
+
const fallbackStatus = result.code === 0 ? "active" : "inactive";
|
|
970
1023
|
return {
|
|
971
1024
|
name: serviceName,
|
|
972
1025
|
active: activeMatch?.[1] === "active",
|
|
973
|
-
status: activeMatch ? `${activeMatch[1]} (${activeMatch[2]})` :
|
|
1026
|
+
status: activeMatch ? `${activeMatch[1]} (${activeMatch[2]})` : fallbackStatus,
|
|
974
1027
|
description: descMatch?.[1]?.trim(),
|
|
975
1028
|
since: sinceMatch?.[1]?.trim(),
|
|
976
1029
|
pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
|
|
@@ -989,6 +1042,10 @@ var ConnectionPool = class {
|
|
|
989
1042
|
// Total number of successful connects ever made by this pool. Useful for
|
|
990
1043
|
// introspection and for tests that want to prove connection reuse.
|
|
991
1044
|
_connectCount = 0;
|
|
1045
|
+
// Once drained, the pool stays drained — new acquires reject and any in-flight
|
|
1046
|
+
// factory closes the freshly-connected client instead of registering it.
|
|
1047
|
+
// Consumers must construct a new pool to use again.
|
|
1048
|
+
drained = false;
|
|
992
1049
|
constructor(options) {
|
|
993
1050
|
this.idleTtlMs = options?.idleTtlMs ?? 6e4;
|
|
994
1051
|
this.maxPoolSize = options?.maxPoolSize ?? 100;
|
|
@@ -1000,6 +1057,9 @@ var ConnectionPool = class {
|
|
|
1000
1057
|
const MAX_ACQUIRE_ATTEMPTS = 3;
|
|
1001
1058
|
let lastErr;
|
|
1002
1059
|
for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {
|
|
1060
|
+
if (this.drained) {
|
|
1061
|
+
throw new Error("ConnectionPool was drained");
|
|
1062
|
+
}
|
|
1003
1063
|
const existing = this.entries.get(key);
|
|
1004
1064
|
if (existing && !existing.dead) {
|
|
1005
1065
|
existing.refCount++;
|
|
@@ -1035,6 +1095,13 @@ var ConnectionPool = class {
|
|
|
1035
1095
|
pending = (async () => {
|
|
1036
1096
|
try {
|
|
1037
1097
|
const client2 = await connectWithProxy(resolved);
|
|
1098
|
+
if (this.drained) {
|
|
1099
|
+
try {
|
|
1100
|
+
client2.end();
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
throw new Error("ConnectionPool was drained while connecting");
|
|
1104
|
+
}
|
|
1038
1105
|
this._connectCount++;
|
|
1039
1106
|
const entry2 = { client: client2, key, refCount: 0, idleTimer: null, dead: false };
|
|
1040
1107
|
const markDead = () => {
|
|
@@ -1121,6 +1188,7 @@ ${diag}`);
|
|
|
1121
1188
|
}
|
|
1122
1189
|
}
|
|
1123
1190
|
drain() {
|
|
1191
|
+
this.drained = true;
|
|
1124
1192
|
for (const entry of this.entries.values()) {
|
|
1125
1193
|
if (entry.idleTimer) {
|
|
1126
1194
|
clearTimeout(entry.idleTimer);
|
|
@@ -1131,6 +1199,7 @@ ${diag}`);
|
|
|
1131
1199
|
}
|
|
1132
1200
|
}
|
|
1133
1201
|
this.entries.clear();
|
|
1202
|
+
this.pending.clear();
|
|
1134
1203
|
}
|
|
1135
1204
|
get size() {
|
|
1136
1205
|
return this.entries.size;
|
|
@@ -1525,10 +1594,12 @@ ${files.join("\n")}` }] };
|
|
|
1525
1594
|
}
|
|
1526
1595
|
|
|
1527
1596
|
// src/server.ts
|
|
1597
|
+
var pkgPath = join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
1598
|
+
var { version } = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
1528
1599
|
function createServer(pool) {
|
|
1529
1600
|
const server = new McpServer({
|
|
1530
1601
|
name: "ssh-mcp",
|
|
1531
|
-
version
|
|
1602
|
+
version
|
|
1532
1603
|
});
|
|
1533
1604
|
registerTools(server, pool);
|
|
1534
1605
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/ssh-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "MCP server for SSH operations with built-in diagnostics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"ssh2": "^1.16.0",
|
|
51
|
-
"zod": "^3.
|
|
51
|
+
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^
|
|
55
|
-
"@types/node": "^
|
|
54
|
+
"@biomejs/biome": "^2.4.13",
|
|
55
|
+
"@types/node": "^25.6.0",
|
|
56
56
|
"@types/ssh2": "^1.15.4",
|
|
57
57
|
"tsup": "^8.5.1",
|
|
58
|
-
"typescript": "^
|
|
59
|
-
"vitest": "^
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
|
+
"vitest": "^4.1.5"
|
|
60
60
|
}
|
|
61
61
|
}
|