mcp-ssh-server-tool 1.0.7 → 2.0.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.
Files changed (3) hide show
  1. package/index.js +144 -477
  2. package/package.json +5 -4
  3. package/ssh_manager.js +62 -99
package/index.js CHANGED
@@ -1,350 +1,100 @@
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: '建立 SSH 连接(仅在需要连续执行多个命令时使用,单次执行请使用 ssh_exec)',
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: '服务器地址或 IP' },
13
+ port: { type: 'number', description: 'SSH 端口(默认: 22)', default: 22 },
14
+ username: { type: 'string', description: 'SSH 用户名' },
15
+ password: { type: 'string', description: '密码(密码认证时必填)' },
16
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: '认证方式' },
17
+ private_key_path: { type: 'string', description: '私钥路径(公钥认证时使用,二选一)' },
18
+ private_key_content: { type: 'string', description: '私钥文本内容(公钥认证时使用,二选一)' },
19
+ passphrase: { type: 'string', description: '私钥密码(可选)' },
20
+ timeout: { type: 'number', description: '超时时间(秒,默认: 10)', default: 10 },
245
21
  },
246
- required: ['host', 'username', 'auth_type'],
22
+ required: ['host', 'username'],
247
23
  },
248
24
  },
249
25
  {
250
26
  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.',
27
+ description: '【推荐】执行 SSH 命令。单次执行直接传入 host/username/password 会自动连接、执行、断开。多次连续执行时才需要先用 ssh_connect 获取 session_id。',
252
28
  inputSchema: {
253
29
  type: 'object',
254
30
  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
- },
31
+ host: { type: 'string', description: '服务器地址(单次执行时必填)' },
32
+ port: { type: 'number', description: 'SSH 端口(默认: 22)', default: 22 },
33
+ username: { type: 'string', description: 'SSH 用户名(单次执行时必填)' },
34
+ password: { type: 'string', description: '密码(单次执行时必填)' },
35
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: '认证方式(默认: password)', default: 'password' },
36
+ private_key_path: { type: 'string', description: '私钥路径(公钥认证时使用,二选一)' },
37
+ private_key_content: { type: 'string', description: '私钥文本内容(公钥认证时使用,二选一)' },
38
+ passphrase: { type: 'string', description: '私钥密码' },
39
+ timeout: { type: 'number', description: '超时时间(秒)', default: 10 },
40
+ session_id: { type: 'string', description: '会话 ID(连续执行多命令时使用,先调用 ssh_connect 获取)' },
41
+ command: { type: 'string', description: '要执行的命令(单个命令)' },
42
+ commands: { type: 'array', items: { type: 'string' }, description: '要执行的命令列表(多个命令按顺序执行)' },
294
43
  },
295
- required: ['command'],
296
44
  },
297
45
  },
298
46
  {
299
47
  name: 'ssh_disconnect',
300
- description: 'Disconnect an SSH session',
48
+ description: '关闭 SSH 会话(使用 ssh_connect 后需要手动断开)',
301
49
  inputSchema: {
302
50
  type: 'object',
303
51
  properties: {
304
- session_id: {
305
- type: 'string',
306
- description: 'The SSH session ID to disconnect',
307
- },
52
+ session_id: { type: 'string', description: '要断开的会话 ID'},
308
53
  },
309
54
  required: ['session_id'],
310
55
  },
311
56
  },
312
57
  {
313
- name: 'ssh_list_sessions',
314
- description: 'List all active SSH sessions',
58
+ name: 'ssh_test',
59
+ description: '快速测试 SSH 连接。连接成功后执行测试命令,然后自动断开。',
315
60
  inputSchema: {
316
61
  type: 'object',
317
- properties: {},
62
+ properties: {
63
+ host: { type: 'string', description: '服务器地址' },
64
+ port: { type: 'number', description: 'SSH 端口', default: 22 },
65
+ username: { type: 'string', description: 'SSH 用户名' },
66
+ password: { type: 'string', description: '密码' },
67
+ auth_type: { type: 'string', enum: ['password', 'public_key'], description: '认证方式' },
68
+ private_key_path: { type: 'string', description: '私钥路径(公钥认证时使用,二选一)' },
69
+ private_key_content: { type: 'string', description: '私钥文本内容(公钥认证时使用,二选一)' },
70
+ passphrase: { type: 'string', description: '私钥密码' },
71
+ timeout: { type: 'number', description: '超时时间(秒)', default: 10 },
72
+ test_command: { type: 'string', description: '测试命令(默认: echo test)', default: 'echo test' },
73
+ },
74
+ required: ['host', 'username'],
318
75
  },
319
76
  },
77
+ {
78
+ name: 'ssh_list_sessions',
79
+ description: '列出当前活跃的 SSH 会话',
80
+ inputSchema: { type: 'object', properties: {} },
81
+ },
320
82
  ];
321
83
 
322
84
  function send(message) {
323
85
  process.stdout.write(JSON.stringify(message) + '\n');
324
86
  }
325
87
 
326
- let buffer = '';
327
-
328
- process.stdin.setEncoding('utf8');
329
-
330
- process.stdin.on('data', async (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
- await handleRequest(request);
342
- } catch (err) {
343
- console.error('Parse error:', err);
344
- }
345
- }
88
+ function parseArgs(args) {
89
+ if (!args) return {};
90
+ if (typeof args === 'string') {
91
+ try { return JSON.parse(args); } catch { return {}; }
346
92
  }
347
- });
93
+ if (typeof args.arguments === 'string') {
94
+ try { return JSON.parse(args.arguments); } catch { return args; }
95
+ }
96
+ return args;
97
+ }
348
98
 
349
99
  async function handleRequest(request) {
350
100
  const { id, method, params } = request;
@@ -356,198 +106,125 @@ async function handleRequest(request) {
356
106
  result: {
357
107
  protocolVersion: '2024-11-05',
358
108
  capabilities: { tools: {} },
359
- serverInfo: {
360
- name: 'ssh-mcp-server',
361
- version: '1.0.0',
362
- },
109
+ serverInfo: { name: 'ssh-mcp-server', version: '1.0.0' },
363
110
  },
364
111
  });
365
112
  return;
366
113
  }
367
114
 
368
- if (method === 'notifications/initialized') {
369
- return;
370
- }
115
+ if (method === 'notifications/initialized') return;
371
116
 
372
117
  if (method === 'tools/list') {
373
- send({
374
- jsonrpc: '2.0',
375
- id,
376
- result: { tools },
377
- });
118
+ send({ jsonrpc: '2.0', id, result: { tools } });
378
119
  return;
379
120
  }
380
121
 
381
122
  if (method === 'tools/call') {
382
123
  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
- }
124
+ const toolArgs = parseArgs(args);
399
125
 
400
126
  if (name === 'ssh_connect') {
401
- sshManager.connect({
127
+ const result = await sshManager.connect({
402
128
  host: toolArgs.host,
403
129
  port: toolArgs.port,
404
130
  username: toolArgs.username,
405
- authType: toolArgs.auth_type,
406
131
  password: toolArgs.password,
407
132
  privateKeyPath: toolArgs.private_key_path,
133
+ privateKeyContent: toolArgs.private_key_content,
408
134
  passphrase: toolArgs.passphrase,
409
135
  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
136
  });
137
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
424
138
  return;
425
139
  }
426
140
 
427
141
  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
- }
142
+ const hasSession = toolArgs.session_id;
143
+ const hasAuth = toolArgs.host && toolArgs.username;
144
+
145
+ if (!hasSession && !hasAuth) {
146
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Either session_id or host/username required' }) }] } });
473
147
  return;
474
148
  }
475
149
 
476
- // 如果没有 session_id,但提供了认证参数,直接连接、执行、断开
477
- if (toolArgs.host && toolArgs.username) {
478
- const connResult = await sshManager.connect({
150
+ if (hasAuth && !hasSession) {
151
+ const conn = await sshManager.connect({
479
152
  host: toolArgs.host,
480
153
  port: toolArgs.port,
481
154
  username: toolArgs.username,
482
- authType: toolArgs.auth_type || 'password',
483
155
  password: toolArgs.password,
484
156
  privateKeyPath: toolArgs.private_key_path,
157
+ privateKeyContent: toolArgs.private_key_content,
485
158
  passphrase: toolArgs.passphrase,
486
159
  timeout: toolArgs.timeout,
487
160
  });
488
161
 
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
- });
162
+ if (!conn.success) {
163
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
502
164
  return;
503
165
  }
504
166
 
505
- const execResult = await sshManager.execCommand(connResult.sessionId, toolArgs.command);
506
- sshManager.disconnect(connResult.sessionId);
167
+ let result;
168
+ if (toolArgs.commands) {
169
+ result = await sshManager.execCommands(conn.sessionId, toolArgs.commands);
170
+ } else if (toolArgs.command) {
171
+ result = await sshManager.execCommand(conn.sessionId, toolArgs.command);
172
+ }
507
173
 
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
- });
174
+ sshManager.disconnect(conn.sessionId);
175
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
520
176
  return;
521
177
  }
522
178
 
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;
179
+ if (hasSession) {
180
+ let result;
181
+ if (toolArgs.commands) {
182
+ result = await sshManager.execCommands(toolArgs.session_id, toolArgs.commands);
183
+ } else if (toolArgs.command) {
184
+ result = await sshManager.execCommand(toolArgs.session_id, toolArgs.command);
185
+ }
186
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
187
+ return;
188
+ }
537
189
  }
538
190
 
539
191
  if (name === 'ssh_disconnect') {
540
192
  const result = sshManager.disconnect(toolArgs.session_id);
193
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
194
+ return;
195
+ }
196
+
197
+ if (name === 'ssh_test') {
198
+ const conn = await sshManager.connect({
199
+ host: toolArgs.host,
200
+ port: toolArgs.port,
201
+ username: toolArgs.username,
202
+ password: toolArgs.password,
203
+ privateKeyPath: toolArgs.private_key_path,
204
+ privateKeyContent: toolArgs.private_key_content,
205
+ passphrase: toolArgs.passphrase,
206
+ timeout: toolArgs.timeout,
207
+ });
208
+
209
+ if (!conn.success) {
210
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
211
+ return;
212
+ }
213
+
214
+ const execResult = await sshManager.execCommand(conn.sessionId, toolArgs.test_command || 'echo test');
215
+ sshManager.disconnect(conn.sessionId);
216
+
541
217
  send({
542
218
  jsonrpc: '2.0',
543
219
  id,
544
220
  result: {
545
- content: [
546
- {
547
- type: 'text',
548
- text: JSON.stringify(result),
549
- },
550
- ],
221
+ content: [{
222
+ type: 'text',
223
+ text: JSON.stringify({
224
+ connection: conn,
225
+ test: execResult,
226
+ }),
227
+ }],
551
228
  },
552
229
  });
553
230
  return;
@@ -555,42 +232,32 @@ async function handleRequest(request) {
555
232
 
556
233
  if (name === 'ssh_list_sessions') {
557
234
  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
- });
235
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
570
236
  return;
571
237
  }
572
238
 
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
- });
239
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }) }] } });
585
240
  return;
586
241
  }
587
242
 
588
- send({
589
- jsonrpc: '2.0',
590
- id,
591
- error: {
592
- code: -32601,
593
- message: 'Method not found',
594
- },
595
- });
243
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
596
244
  }
245
+
246
+ let buffer = '';
247
+ process.stdin.setEncoding('utf8');
248
+
249
+ process.stdin.on('data', async (chunk) => {
250
+ buffer += chunk;
251
+ let newlineIndex;
252
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
253
+ const line = buffer.slice(0, newlineIndex);
254
+ buffer = buffer.slice(newlineIndex + 1);
255
+ if (line.trim()) {
256
+ try {
257
+ await handleRequest(JSON.parse(line));
258
+ } catch (err) {
259
+ console.error('Parse error:', err.message);
260
+ }
261
+ }
262
+ }
263
+ });
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "mcp-ssh-server-tool",
3
- "version": "1.0.7",
3
+ "version": "2.0.1",
4
4
  "description": "MCP Server for SSH connections and remote command execution",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "bin": "bin/cli.js",
7
+ "bin": {
8
+ "mcp-ssh-server-tool": "bin/cli.js"
9
+ },
8
10
  "scripts": {
9
11
  "start": "node index.js"
10
12
  },
@@ -16,8 +18,7 @@
16
18
  "url": ""
17
19
  },
18
20
  "dependencies": {
19
- "ssh2": "^1.15.0",
20
- "uuid": "^9.0.0"
21
+ "ssh2": "^1.15.0"
21
22
  },
22
23
  "engines": {
23
24
  "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.privateKeyContent || (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 };