mcp-ssh-server-tool 1.0.6 → 2.0.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.
Files changed (3) hide show
  1. package/index.js +139 -478
  2. package/package.json +2 -3
  3. package/ssh_manager.js +62 -99
package/index.js CHANGED
@@ -1,352 +1,99 @@
1
- import { Client } from 'ssh2';
2
- import { v4 as uuidv4 } from 'uuid';
3
- import * as fs from 'fs';
4
-
5
- class SSHSession {
6
- constructor(sessionId, client, host, username) {
7
- this.sessionId = sessionId;
8
- this.client = client;
9
- this.host = host;
10
- this.username = username;
11
- this.connected = true;
12
- }
13
- }
14
-
15
- class SSHConnectionManager {
16
- constructor() {
17
- this.sessions = new Map();
18
- }
19
-
20
- connect(options) {
21
- return new Promise((resolve) => {
22
- const client = new Client();
23
- const sessionId = uuidv4();
24
- const port = options.port || 22;
25
- const timeout = (options.timeout || 10) * 1000;
26
-
27
- const config = {
28
- host: options.host,
29
- port: port,
30
- username: options.username,
31
- timeout: timeout,
32
- readyTimeout: timeout,
33
- };
34
-
35
- if (options.authType === 'password') {
36
- if (!options.password) {
37
- resolve({
38
- success: false,
39
- error: 'Password is required for password authentication',
40
- errorType: 'MissingPassword',
41
- });
42
- return;
43
- }
44
- config.password = options.password;
45
- } else if (options.authType === 'public_key') {
46
- if (!options.privateKeyPath) {
47
- resolve({
48
- success: false,
49
- error: 'Private key path is required for public key authentication',
50
- errorType: 'MissingPrivateKey',
51
- });
52
- return;
53
- }
54
- try {
55
- config.privateKey = fs.readFileSync(options.privateKeyPath);
56
- if (options.passphrase) {
57
- config.passphrase = options.passphrase;
58
- }
59
- } catch (err) {
60
- resolve({
61
- success: false,
62
- error: `Failed to read private key: ${err.message}`,
63
- errorType: 'PrivateKeyError',
64
- });
65
- return;
66
- }
67
- }
68
-
69
- client.on('ready', () => {
70
- const session = new SSHSession(sessionId, client, options.host, options.username);
71
- this.sessions.set(sessionId, session);
72
-
73
- resolve({
74
- success: true,
75
- sessionId,
76
- host: options.host,
77
- port,
78
- username: options.username,
79
- message: `Successfully connected to ${options.host}:${port}`,
80
- });
81
- });
82
-
83
- client.on('error', (err) => {
84
- resolve({
85
- success: false,
86
- error: err.message,
87
- errorType: 'ConnectionError',
88
- });
89
- });
90
-
91
- client.connect(config);
92
- });
93
- }
94
-
95
- execCommand(sessionId, command) {
96
- const session = this.sessions.get(sessionId);
97
-
98
- if (!session) {
99
- return {
100
- success: false,
101
- error: `Session ${sessionId} not found`,
102
- errorType: 'SessionNotFound',
103
- };
104
- }
105
-
106
- if (!session.connected) {
107
- return {
108
- success: false,
109
- error: `Session ${sessionId} is not connected`,
110
- errorType: 'SessionNotConnected',
111
- };
112
- }
113
-
114
- return new Promise((resolve) => {
115
- session.client.exec(command, (err, stream) => {
116
- if (err) {
117
- resolve({
118
- success: false,
119
- error: err.message,
120
- errorType: 'ExecError',
121
- sessionId,
122
- });
123
- return;
124
- }
125
-
126
- let stdout = '';
127
- let stderr = '';
128
-
129
- stream.on('close', (code) => {
130
- resolve({
131
- success: true,
132
- sessionId,
133
- command,
134
- stdout,
135
- stderr,
136
- exitCode: code,
137
- });
138
- });
139
-
140
- stream.on('data', (data) => {
141
- stdout += data.toString();
142
- });
143
-
144
- stream.stderr.on('data', (data) => {
145
- stderr += data.toString();
146
- });
147
- });
148
- });
149
- }
150
-
151
- disconnect(sessionId) {
152
- const session = this.sessions.get(sessionId);
153
-
154
- if (!session) {
155
- return {
156
- success: false,
157
- error: `Session ${sessionId} not found`,
158
- errorType: 'SessionNotFound',
159
- };
160
- }
161
-
162
- try {
163
- session.client.end();
164
- session.connected = false;
165
- this.sessions.delete(sessionId);
166
-
167
- return {
168
- success: true,
169
- sessionId,
170
- message: 'Disconnected successfully',
171
- };
172
- } catch (err) {
173
- return {
174
- success: false,
175
- error: err.message,
176
- errorType: 'DisconnectError',
177
- };
178
- }
179
- }
180
-
181
- listSessions() {
182
- const sessions = [];
183
-
184
- this.sessions.forEach((session) => {
185
- sessions.push({
186
- sessionId: session.sessionId,
187
- host: session.host,
188
- username: session.username,
189
- connected: session.connected,
190
- });
191
- });
192
-
193
- return {
194
- success: true,
195
- sessions,
196
- count: sessions.length,
197
- };
198
- }
199
- }
1
+ import { SSHConnectionManager } from './ssh_manager.js';
200
2
 
201
3
  const sshManager = new SSHConnectionManager();
202
4
 
203
5
  const tools = [
204
6
  {
205
7
  name: 'ssh_connect',
206
- description: 'Establish an SSH connection to a remote server',
8
+ description: 'Establish an SSH connection. Returns session_id for subsequent commands.',
207
9
  inputSchema: {
208
10
  type: 'object',
209
11
  properties: {
210
- host: {
211
- type: 'string',
212
- description: 'Server hostname or IP address',
213
- },
214
- port: {
215
- type: 'number',
216
- description: 'SSH port number',
217
- default: 22,
218
- },
219
- username: {
220
- type: 'string',
221
- description: 'SSH username',
222
- },
223
- auth_type: {
224
- type: 'string',
225
- enum: ['password', 'public_key'],
226
- description: 'Authentication type',
227
- },
228
- password: {
229
- type: 'string',
230
- description: 'Password (required if auth_type is password)',
231
- },
232
- private_key_path: {
233
- type: 'string',
234
- description: 'Path to private key file (required if auth_type is public_key)',
235
- },
236
- passphrase: {
237
- type: 'string',
238
- description: 'Passphrase for private key (optional)',
239
- },
240
- timeout: {
241
- type: 'number',
242
- description: 'Connection timeout in seconds',
243
- default: 10,
244
- },
12
+ host: { type: 'string', description: 'Server hostname or IP' },
13
+ port: { type: 'number', description: 'SSH port (default: 22)', default: 22 },
14
+ username: { type: 'string', description: 'SSH username' },
15
+ password: { type: 'string', description: 'Password (for password auth)' },
16
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
17
+ private_key_path: { type: 'string', description: 'Path to private key' },
18
+ passphrase: { type: 'string', description: 'Passphrase for private key' },
19
+ timeout: { type: 'number', description: 'Timeout in seconds (default: 10)', default: 10 },
245
20
  },
246
- required: ['host', 'username', 'auth_type'],
21
+ required: ['host', 'username'],
247
22
  },
248
23
  },
249
24
  {
250
25
  name: 'ssh_exec',
251
- description: 'Execute a command on an SSH session. Use session_id if you have an active session, or provide host/username/password to connect directly.',
26
+ description: 'Execute command(s) on SSH server. Supports direct mode (host/username) or session mode (session_id).',
252
27
  inputSchema: {
253
28
  type: 'object',
254
29
  properties: {
255
- session_id: {
256
- type: 'string',
257
- description: 'The SSH session ID from ssh_connect (optional if providing host/username/password)',
258
- },
259
- command: {
260
- type: 'string',
261
- description: 'The command to execute',
262
- },
263
- host: {
264
- type: 'string',
265
- description: 'Server hostname (optional, used if session_id not provided)',
266
- },
267
- port: {
268
- type: 'number',
269
- description: 'SSH port (default: 22)',
270
- default: 22,
271
- },
272
- username: {
273
- type: 'string',
274
- description: 'SSH username (optional)',
275
- },
276
- password: {
277
- type: 'string',
278
- description: 'SSH password (optional)',
279
- },
280
- auth_type: {
281
- type: 'string',
282
- enum: ['password', 'public_key'],
283
- description: 'Authentication type (default: password)',
284
- default: 'password',
285
- },
286
- private_key_path: {
287
- type: 'string',
288
- description: 'Private key path (for public_key auth)',
289
- },
290
- passphrase: {
291
- type: 'string',
292
- description: 'Passphrase for private key',
293
- },
30
+ host: { type: 'string', description: 'Server hostname (use for direct execution)' },
31
+ port: { type: 'number', description: 'SSH port', default: 22 },
32
+ username: { type: 'string', description: 'SSH username' },
33
+ password: { type: 'string', description: 'Password' },
34
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
35
+ private_key_path: { type: 'string', description: 'Path to private key' },
36
+ passphrase: { type: 'string', description: 'Passphrase for private key' },
37
+ timeout: { type: 'number', description: 'Timeout in seconds', default: 10 },
38
+ session_id: { type: 'string', description: 'Session ID from ssh_connect (use for session mode)' },
39
+ command: { type: 'string', description: 'Command to execute' },
40
+ commands: { type: 'array', items: { type: 'string' }, description: 'Multiple commands to execute sequentially' },
294
41
  },
295
- required: ['command'],
296
42
  },
297
43
  },
298
44
  {
299
45
  name: 'ssh_disconnect',
300
- description: 'Disconnect an SSH session',
46
+ description: 'Close an SSH session',
301
47
  inputSchema: {
302
48
  type: 'object',
303
49
  properties: {
304
- session_id: {
305
- type: 'string',
306
- description: 'The SSH session ID to disconnect',
307
- },
50
+ session_id: { type: 'string', description: 'Session ID to disconnect' },
308
51
  },
309
52
  required: ['session_id'],
310
53
  },
311
54
  },
312
55
  {
313
- name: 'ssh_list_sessions',
314
- description: 'List all active SSH sessions',
56
+ name: 'ssh_test',
57
+ description: 'Quick connection test. Connects, executes a test command, and disconnects.',
315
58
  inputSchema: {
316
59
  type: 'object',
317
- properties: {},
60
+ properties: {
61
+ host: { type: 'string', description: 'Server hostname' },
62
+ port: { type: 'number', description: 'SSH port', default: 22 },
63
+ username: { type: 'string', description: 'SSH username' },
64
+ password: { type: 'string', description: 'Password' },
65
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
66
+ private_key_path: { type: 'string', description: 'Path to private key' },
67
+ passphrase: { type: 'string', description: 'Passphrase' },
68
+ timeout: { type: 'number', description: 'Timeout in seconds', default: 10 },
69
+ test_command: { type: 'string', description: 'Test command (default: echo test)', default: 'echo test' },
70
+ },
71
+ required: ['host', 'username'],
318
72
  },
319
73
  },
74
+ {
75
+ name: 'ssh_list_sessions',
76
+ description: 'List active SSH sessions',
77
+ inputSchema: { type: 'object', properties: {} },
78
+ },
320
79
  ];
321
80
 
322
81
  function send(message) {
323
82
  process.stdout.write(JSON.stringify(message) + '\n');
324
83
  }
325
84
 
326
- let buffer = '';
327
-
328
- process.stdin.setEncoding('utf8');
329
-
330
- process.stdin.on('data', (chunk) => {
331
- buffer += chunk;
332
- let newlineIndex;
333
-
334
- while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
335
- const line = buffer.slice(0, newlineIndex);
336
- buffer = buffer.slice(newlineIndex + 1);
337
-
338
- if (line.trim()) {
339
- try {
340
- const request = JSON.parse(line);
341
- handleRequest(request);
342
- } catch (err) {
343
- console.error('Parse error:', err);
344
- }
345
- }
85
+ function parseArgs(args) {
86
+ if (!args) return {};
87
+ if (typeof args === 'string') {
88
+ try { return JSON.parse(args); } catch { return {}; }
346
89
  }
347
- });
90
+ if (typeof args.arguments === 'string') {
91
+ try { return JSON.parse(args.arguments); } catch { return args; }
92
+ }
93
+ return args;
94
+ }
348
95
 
349
- function handleRequest(request) {
96
+ async function handleRequest(request) {
350
97
  const { id, method, params } = request;
351
98
 
352
99
  if (method === 'initialize') {
@@ -356,198 +103,122 @@ function handleRequest(request) {
356
103
  result: {
357
104
  protocolVersion: '2024-11-05',
358
105
  capabilities: { tools: {} },
359
- serverInfo: {
360
- name: 'ssh-mcp-server',
361
- version: '1.0.0',
362
- },
106
+ serverInfo: { name: 'ssh-mcp-server', version: '1.0.0' },
363
107
  },
364
108
  });
365
109
  return;
366
110
  }
367
111
 
368
- if (method === 'notifications/initialized') {
369
- return;
370
- }
112
+ if (method === 'notifications/initialized') return;
371
113
 
372
114
  if (method === 'tools/list') {
373
- send({
374
- jsonrpc: '2.0',
375
- id,
376
- result: { tools },
377
- });
115
+ send({ jsonrpc: '2.0', id, result: { tools } });
378
116
  return;
379
117
  }
380
118
 
381
119
  if (method === 'tools/call') {
382
120
  const { name, arguments: args } = params;
383
-
384
- // Handle nested arguments (stringified JSON)
385
- let toolArgs = args;
386
- if (typeof args === 'string') {
387
- try {
388
- toolArgs = JSON.parse(args);
389
- } catch (e) {
390
- toolArgs = {};
391
- }
392
- } else if (args && typeof args.arguments === 'string') {
393
- try {
394
- toolArgs = JSON.parse(args.arguments);
395
- } catch (e) {
396
- toolArgs = args;
397
- }
398
- }
121
+ const toolArgs = parseArgs(args);
399
122
 
400
123
  if (name === 'ssh_connect') {
401
- sshManager.connect({
124
+ const result = await sshManager.connect({
402
125
  host: toolArgs.host,
403
126
  port: toolArgs.port,
404
127
  username: toolArgs.username,
405
- authType: toolArgs.auth_type,
406
128
  password: toolArgs.password,
407
129
  privateKeyPath: toolArgs.private_key_path,
408
130
  passphrase: toolArgs.passphrase,
409
131
  timeout: toolArgs.timeout,
410
- }).then((result) => {
411
- send({
412
- jsonrpc: '2.0',
413
- id,
414
- result: {
415
- content: [
416
- {
417
- type: 'text',
418
- text: JSON.stringify(result),
419
- },
420
- ],
421
- },
422
- });
423
132
  });
133
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
424
134
  return;
425
135
  }
426
136
 
427
137
  if (name === 'ssh_exec') {
428
- // 如果有 session_id,使用现有会话
429
- if (toolArgs.session_id) {
430
- const execResult = sshManager.execCommand(toolArgs.session_id, toolArgs.command);
431
- if (execResult && typeof execResult.then === 'function') {
432
- execResult.then((result) => {
433
- send({
434
- jsonrpc: '2.0',
435
- id,
436
- result: {
437
- content: [
438
- {
439
- type: 'text',
440
- text: JSON.stringify(result),
441
- },
442
- ],
443
- },
444
- });
445
- }).catch((err) => {
446
- send({
447
- jsonrpc: '2.0',
448
- id,
449
- result: {
450
- content: [
451
- {
452
- type: 'text',
453
- text: JSON.stringify({ success: false, error: err.message }),
454
- },
455
- ],
456
- },
457
- });
458
- });
459
- } else {
460
- send({
461
- jsonrpc: '2.0',
462
- id,
463
- result: {
464
- content: [
465
- {
466
- type: 'text',
467
- text: JSON.stringify(execResult),
468
- },
469
- ],
470
- },
471
- });
472
- }
138
+ const hasSession = toolArgs.session_id;
139
+ const hasAuth = toolArgs.host && toolArgs.username;
140
+
141
+ if (!hasSession && !hasAuth) {
142
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Either session_id or host/username required' }) }] } });
473
143
  return;
474
144
  }
475
145
 
476
- // 如果没有 session_id,但提供了认证参数,直接连接、执行、断开
477
- if (toolArgs.host && toolArgs.username) {
478
- const connResult = await sshManager.connect({
146
+ if (hasAuth && !hasSession) {
147
+ const conn = await sshManager.connect({
479
148
  host: toolArgs.host,
480
149
  port: toolArgs.port,
481
150
  username: toolArgs.username,
482
- authType: toolArgs.auth_type || 'password',
483
151
  password: toolArgs.password,
484
152
  privateKeyPath: toolArgs.private_key_path,
485
153
  passphrase: toolArgs.passphrase,
486
154
  timeout: toolArgs.timeout,
487
155
  });
488
156
 
489
- if (!connResult.success) {
490
- send({
491
- jsonrpc: '2.0',
492
- id,
493
- result: {
494
- content: [
495
- {
496
- type: 'text',
497
- text: JSON.stringify(connResult),
498
- },
499
- ],
500
- },
501
- });
157
+ if (!conn.success) {
158
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
502
159
  return;
503
160
  }
504
161
 
505
- const execResult = await sshManager.execCommand(connResult.sessionId, toolArgs.command);
506
- sshManager.disconnect(connResult.sessionId);
162
+ let result;
163
+ if (toolArgs.commands) {
164
+ result = await sshManager.execCommands(conn.sessionId, toolArgs.commands);
165
+ } else if (toolArgs.command) {
166
+ result = await sshManager.execCommand(conn.sessionId, toolArgs.command);
167
+ }
507
168
 
508
- send({
509
- jsonrpc: '2.0',
510
- id,
511
- result: {
512
- content: [
513
- {
514
- type: 'text',
515
- text: JSON.stringify(execResult),
516
- },
517
- ],
518
- },
519
- });
169
+ sshManager.disconnect(conn.sessionId);
170
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
520
171
  return;
521
172
  }
522
173
 
523
- // 既没有 session_id 也没有认证参数
524
- send({
525
- jsonrpc: '2.0',
526
- id,
527
- result: {
528
- content: [
529
- {
530
- type: 'text',
531
- text: JSON.stringify({ success: false, error: 'Either session_id or host/username is required' }),
532
- },
533
- ],
534
- },
535
- });
536
- return;
174
+ if (hasSession) {
175
+ let result;
176
+ if (toolArgs.commands) {
177
+ result = await sshManager.execCommands(toolArgs.session_id, toolArgs.commands);
178
+ } else if (toolArgs.command) {
179
+ result = await sshManager.execCommand(toolArgs.session_id, toolArgs.command);
180
+ }
181
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
182
+ return;
183
+ }
537
184
  }
538
185
 
539
186
  if (name === 'ssh_disconnect') {
540
187
  const result = sshManager.disconnect(toolArgs.session_id);
188
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
189
+ return;
190
+ }
191
+
192
+ if (name === 'ssh_test') {
193
+ const conn = await sshManager.connect({
194
+ host: toolArgs.host,
195
+ port: toolArgs.port,
196
+ username: toolArgs.username,
197
+ password: toolArgs.password,
198
+ privateKeyPath: toolArgs.private_key_path,
199
+ passphrase: toolArgs.passphrase,
200
+ timeout: toolArgs.timeout,
201
+ });
202
+
203
+ if (!conn.success) {
204
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
205
+ return;
206
+ }
207
+
208
+ const execResult = await sshManager.execCommand(conn.sessionId, toolArgs.test_command || 'echo test');
209
+ sshManager.disconnect(conn.sessionId);
210
+
541
211
  send({
542
212
  jsonrpc: '2.0',
543
213
  id,
544
214
  result: {
545
- content: [
546
- {
547
- type: 'text',
548
- text: JSON.stringify(result),
549
- },
550
- ],
215
+ content: [{
216
+ type: 'text',
217
+ text: JSON.stringify({
218
+ connection: conn,
219
+ test: execResult,
220
+ }),
221
+ }],
551
222
  },
552
223
  });
553
224
  return;
@@ -555,42 +226,32 @@ function handleRequest(request) {
555
226
 
556
227
  if (name === 'ssh_list_sessions') {
557
228
  const result = sshManager.listSessions();
558
- send({
559
- jsonrpc: '2.0',
560
- id,
561
- result: {
562
- content: [
563
- {
564
- type: 'text',
565
- text: JSON.stringify(result),
566
- },
567
- ],
568
- },
569
- });
229
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
570
230
  return;
571
231
  }
572
232
 
573
- send({
574
- jsonrpc: '2.0',
575
- id,
576
- result: {
577
- content: [
578
- {
579
- type: 'text',
580
- text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }),
581
- },
582
- ],
583
- },
584
- });
233
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }) }] } });
585
234
  return;
586
235
  }
587
236
 
588
- send({
589
- jsonrpc: '2.0',
590
- id,
591
- error: {
592
- code: -32601,
593
- message: 'Method not found',
594
- },
595
- });
237
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
596
238
  }
239
+
240
+ let buffer = '';
241
+ process.stdin.setEncoding('utf8');
242
+
243
+ process.stdin.on('data', async (chunk) => {
244
+ buffer += chunk;
245
+ let newlineIndex;
246
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
247
+ const line = buffer.slice(0, newlineIndex);
248
+ buffer = buffer.slice(newlineIndex + 1);
249
+ if (line.trim()) {
250
+ try {
251
+ await handleRequest(JSON.parse(line));
252
+ } catch (err) {
253
+ console.error('Parse error:', err.message);
254
+ }
255
+ }
256
+ }
257
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ssh-server-tool",
3
- "version": "1.0.6",
3
+ "version": "2.0.0",
4
4
  "description": "MCP Server for SSH connections and remote command execution",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -16,8 +16,7 @@
16
16
  "url": ""
17
17
  },
18
18
  "dependencies": {
19
- "ssh2": "^1.15.0",
20
- "uuid": "^9.0.0"
19
+ "ssh2": "^1.15.0"
21
20
  },
22
21
  "engines": {
23
22
  "node": ">=18.0.0"
package/ssh_manager.js CHANGED
@@ -1,86 +1,62 @@
1
1
  import { Client } from 'ssh2';
2
- import { v4 as uuidv4 } from 'uuid';
2
+ import { randomUUID } from 'crypto';
3
3
  import * as fs from 'fs';
4
+ import * as path from 'path';
4
5
 
5
6
  class SSHSession {
6
- constructor(sessionId, client, host, username) {
7
+ constructor(sessionId, client, config) {
7
8
  this.sessionId = sessionId;
8
9
  this.client = client;
9
- this.host = host;
10
- this.username = username;
11
- this.connected = true;
10
+ this.config = config;
11
+ this.createdAt = Date.now();
12
12
  }
13
13
  }
14
14
 
15
- export class SSHConnectionManager {
15
+ class SSHConnectionManager {
16
16
  constructor() {
17
17
  this.sessions = new Map();
18
18
  }
19
19
 
20
- connect(options) {
20
+ async connect(options) {
21
21
  return new Promise((resolve) => {
22
22
  const client = new Client();
23
- const sessionId = uuidv4();
24
- const port = options.port || 22;
25
- const timeout = (options.timeout || 10) * 1000;
26
-
23
+ const sessionId = randomUUID();
27
24
  const config = {
28
25
  host: options.host,
29
- port: port,
26
+ port: options.port || 22,
30
27
  username: options.username,
31
- timeout: timeout,
32
- readyTimeout: timeout,
28
+ password: options.password,
29
+ privateKey: options.privateKeyPath ? this._readPrivateKey(options.privateKeyPath) : undefined,
30
+ passphrase: options.passphrase,
31
+ readyTimeout: (options.timeout || 10) * 1000,
32
+ keepaliveInterval: 0,
33
33
  };
34
34
 
35
- if (options.authType === 'password') {
36
- if (!options.password) {
37
- resolve({
38
- success: false,
39
- error: 'Password is required for password authentication',
40
- errorType: 'MissingPassword',
41
- });
42
- return;
43
- }
44
- config.password = options.password;
45
- } else if (options.authType === 'public_key') {
46
- if (!options.privateKeyPath) {
47
- resolve({
48
- success: false,
49
- error: 'Private key path is required for public key authentication',
50
- errorType: 'MissingPrivateKey',
51
- });
52
- return;
53
- }
54
- try {
55
- config.privateKey = fs.readFileSync(options.privateKeyPath);
56
- if (options.passphrase) {
57
- config.passphrase = options.passphrase;
58
- }
59
- } catch (err) {
60
- resolve({
61
- success: false,
62
- error: `Failed to read private key: ${err.message}`,
63
- errorType: 'PrivateKeyError',
64
- });
65
- return;
66
- }
67
- }
35
+ const timeout = setTimeout(() => {
36
+ client.end();
37
+ resolve({
38
+ success: false,
39
+ error: 'Connection timeout',
40
+ errorType: 'Timeout',
41
+ });
42
+ }, (options.timeout || 10) * 1000);
68
43
 
69
44
  client.on('ready', () => {
70
- const session = new SSHSession(sessionId, client, options.host, options.username);
45
+ clearTimeout(timeout);
46
+ const session = new SSHSession(sessionId, client, { host: config.host, port: config.port, username: config.username });
71
47
  this.sessions.set(sessionId, session);
72
-
73
48
  resolve({
74
49
  success: true,
75
50
  sessionId,
76
- host: options.host,
77
- port,
78
- username: options.username,
79
- message: `Successfully connected to ${options.host}:${port}`,
51
+ host: config.host,
52
+ port: config.port,
53
+ username: config.username,
54
+ message: `Connected to ${config.host}:${config.port}`,
80
55
  });
81
56
  });
82
57
 
83
58
  client.on('error', (err) => {
59
+ clearTimeout(timeout);
84
60
  resolve({
85
61
  success: false,
86
62
  error: err.message,
@@ -92,9 +68,17 @@ export class SSHConnectionManager {
92
68
  });
93
69
  }
94
70
 
95
- execCommand(sessionId, command) {
96
- const session = this.sessions.get(sessionId);
71
+ _readPrivateKey(filePath) {
72
+ try {
73
+ const resolvedPath = path.resolve(filePath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ''));
74
+ return fs.readFileSync(resolvedPath);
75
+ } catch (err) {
76
+ throw new Error(`Failed to read private key: ${err.message}`);
77
+ }
78
+ }
97
79
 
80
+ async execCommand(sessionId, command) {
81
+ const session = this.sessions.get(sessionId);
98
82
  if (!session) {
99
83
  return {
100
84
  success: false,
@@ -103,14 +87,6 @@ export class SSHConnectionManager {
103
87
  };
104
88
  }
105
89
 
106
- if (!session.connected) {
107
- return {
108
- success: false,
109
- error: `Session ${sessionId} is not connected`,
110
- errorType: 'SessionNotConnected',
111
- };
112
- }
113
-
114
90
  return new Promise((resolve) => {
115
91
  session.client.exec(command, (err, stream) => {
116
92
  if (err) {
@@ -137,20 +113,24 @@ export class SSHConnectionManager {
137
113
  });
138
114
  });
139
115
 
140
- stream.on('data', (data) => {
141
- stdout += data.toString();
142
- });
143
-
144
- stream.stderr.on('data', (data) => {
145
- stderr += data.toString();
146
- });
116
+ stream.on('data', (data) => { stdout += data.toString(); });
117
+ stream.stderr.on('data', (data) => { stderr += data.toString(); });
147
118
  });
148
119
  });
149
120
  }
150
121
 
122
+ async execCommands(sessionId, commands) {
123
+ const results = [];
124
+ for (const cmd of commands) {
125
+ const result = await this.execCommand(sessionId, cmd);
126
+ results.push(result);
127
+ if (!result.success) break;
128
+ }
129
+ return results;
130
+ }
131
+
151
132
  disconnect(sessionId) {
152
133
  const session = this.sessions.get(sessionId);
153
-
154
134
  if (!session) {
155
135
  return {
156
136
  success: false,
@@ -161,39 +141,22 @@ export class SSHConnectionManager {
161
141
 
162
142
  try {
163
143
  session.client.end();
164
- session.connected = false;
165
144
  this.sessions.delete(sessionId);
166
-
167
- return {
168
- success: true,
169
- sessionId,
170
- message: 'Disconnected successfully',
171
- };
145
+ return { success: true, sessionId, message: 'Disconnected' };
172
146
  } catch (err) {
173
- return {
174
- success: false,
175
- error: err.message,
176
- errorType: 'DisconnectError',
177
- };
147
+ return { success: false, error: err.message, errorType: 'DisconnectError' };
178
148
  }
179
149
  }
180
150
 
181
151
  listSessions() {
182
- const sessions = [];
183
-
184
- this.sessions.forEach((session) => {
185
- sessions.push({
186
- sessionId: session.sessionId,
187
- host: session.host,
188
- username: session.username,
189
- connected: session.connected,
190
- });
191
- });
192
-
193
- return {
194
- success: true,
195
- sessions,
196
- count: sessions.length,
197
- };
152
+ const sessions = Array.from(this.sessions.values()).map(s => ({
153
+ sessionId: s.sessionId,
154
+ host: s.config.host,
155
+ username: s.config.username,
156
+ createdAt: s.createdAt,
157
+ }));
158
+ return { success: true, sessions, count: sessions.length };
198
159
  }
199
160
  }
161
+
162
+ export { SSHConnectionManager };