mcp-ssh-server-tool 1.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.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # SSH MCP Server
2
+
3
+ 一个用于 SSH 连接和远程命令执行的 MCP (Model Context Protocol) 服务器。
4
+
5
+ ## 功能
6
+
7
+ - 🔐 **密码认证** - 使用用户名和密码连接到 SSH 服务器
8
+ - 🔑 **公钥认证** - 使用私钥文件连接到 SSH 服务器
9
+ - 💻 **远程命令执行** - 在连接的服务器上执行命令并获取输出
10
+ - 📊 **会话管理** - 管理多个 SSH 连接会话
11
+
12
+ ## MCP 工具
13
+
14
+ ### ssh_connect
15
+ 建立 SSH 连接
16
+
17
+ **参数:**
18
+ - `host`: 服务器地址 (必需)
19
+ - `port`: 端口号 (默认: 22)
20
+ - `username`: 用户名 (必需)
21
+ - `auth_type`: 认证方式 - "password" | "public_key" (必需)
22
+ - `password`: 密码 (auth_type 为 password 时必需)
23
+ - `private_key_path`: 私钥路径 (auth_type 为 public_key 时必需)
24
+ - `passphrase`: 私钥密码 (可选)
25
+ - `timeout`: 连接超时 (默认: 10秒)
26
+
27
+ ### ssh_exec
28
+ 在 SSH 会话中执行命令
29
+
30
+ **参数:**
31
+ - `session_id`: SSH 会话 ID (必需)
32
+ - `command`: 要执行的命令 (必需)
33
+
34
+ **返回:**
35
+ - `stdout`: 标准输出
36
+ - `stderr`: 标准错误
37
+ - `exit_code`: 退出码
38
+
39
+ ### ssh_disconnect
40
+ 断开 SSH 连接
41
+
42
+ **参数:**
43
+ - `session_id`: SSH 会话 ID (必需)
44
+
45
+ ### ssh_list_sessions
46
+ 列出所有活动的 SSH 会话
47
+
48
+ ## 安装
49
+
50
+ ### 步骤 1: 安装依赖
51
+
52
+ 在终端中运行:
53
+ ```bash
54
+ pip install paramiko mcp
55
+ ```
56
+
57
+ ### 步骤 2: 安装 SSH MCP Server
58
+
59
+ ```bash
60
+ pip install -e .
61
+ ```
62
+
63
+ ## 配置
64
+
65
+ ### 重要: 确保依赖已安装
66
+
67
+ 在使用 MCP 之前,请确保你的 AI 客户端使用的 Python 环境中已安装依赖:
68
+
69
+ ```bash
70
+ pip install paramiko mcp
71
+ ```
72
+
73
+ ### Claude Desktop 配置
74
+
75
+ 在 Claude Desktop 的配置文件中添加:
76
+
77
+ **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "ssh-mcp-server": {
83
+ "command": "python",
84
+ "args": ["-m", "mcp_ssh_server.index"],
85
+ "env": {}
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Cursor / VS Code 配置
92
+
93
+ 在 `.cursor/mcp.json` 或 `.vscode/mcp.json` 中添加相同配置:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "ssh-mcp-server": {
99
+ "command": "python",
100
+ "args": ["-m", "mcp_ssh_server.index"],
101
+ "env": {}
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### 故障排除
108
+
109
+ 如果遇到 `Connection closed` 错误:
110
+
111
+ 1. **确认依赖已安装**:
112
+ ```bash
113
+ python -c "import paramiko; import mcp; print('OK')"
114
+ ```
115
+
116
+ 2. **检查 Python 路径**: 在终端中运行以下命令获取 Python 路径,并在配置中使用完整路径:
117
+ ```bash
118
+ where python # Windows
119
+ which python # Linux/Mac
120
+ ```
121
+
122
+ 3. **使用完整路径配置**:
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "ssh-mcp-server": {
127
+ "command": "C:\\Users\\你的用户名\\anaconda3\\python.exe",
128
+ "args": ["-m", "mcp_ssh_server.index"],
129
+ "env": {}
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## 使用示例
136
+
137
+ ### 1. 密码认证连接
138
+
139
+ ```json
140
+ {
141
+ "host": "192.168.1.100",
142
+ "port": 22,
143
+ "username": "admin",
144
+ "auth_type": "password",
145
+ "password": "your_password"
146
+ }
147
+ ```
148
+
149
+ ### 2. 公钥认证连接
150
+
151
+ ```json
152
+ {
153
+ "host": "192.168.1.100",
154
+ "port": 22,
155
+ "username": "admin",
156
+ "auth_type": "public_key",
157
+ "private_key_path": "C:\\Users\\you\\.ssh\\id_rsa"
158
+ }
159
+ ```
160
+
161
+ ### 3. 执行命令
162
+
163
+ ```json
164
+ {
165
+ "session_id": "<从 ssh_connect 返回的 session_id>",
166
+ "command": "ls -la /home"
167
+ }
168
+ ```
169
+
170
+ ### 4. 断开连接
171
+
172
+ ```json
173
+ {
174
+ "session_id": "<session_id>"
175
+ }
176
+ ```
177
+
178
+ ## 项目结构
179
+
180
+ ```
181
+ SSH-MCP/
182
+ ├── mcp_ssh_server/
183
+ │ ├── __init__.py
184
+ │ ├── index.py # MCP Server 主入口
185
+ │ └── ssh_manager.py # SSH 连接管理器
186
+ ├── pyproject.toml # Python 项目配置
187
+ ├── mcp.json # MCP 配置文件示例
188
+ └── README.md # 使用说明
189
+ ```
190
+
191
+ ## 依赖
192
+
193
+ - Python >= 3.10
194
+ - paramiko >= 3.4.0
195
+ - mcp >= 1.1.0
196
+
197
+ ## License
198
+
199
+ MIT
package/index.js ADDED
@@ -0,0 +1,500 @@
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
+ }
200
+
201
+ const sshManager = new SSHConnectionManager();
202
+
203
+ const tools = [
204
+ {
205
+ name: 'ssh_connect',
206
+ description: 'Establish an SSH connection to a remote server',
207
+ inputSchema: {
208
+ type: 'object',
209
+ 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
+ },
245
+ },
246
+ required: ['host', 'username', 'auth_type'],
247
+ },
248
+ },
249
+ {
250
+ name: 'ssh_exec',
251
+ description: 'Execute a command on an active SSH session',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ session_id: {
256
+ type: 'string',
257
+ description: 'The SSH session ID from ssh_connect',
258
+ },
259
+ command: {
260
+ type: 'string',
261
+ description: 'The command to execute',
262
+ },
263
+ },
264
+ required: ['session_id', 'command'],
265
+ },
266
+ },
267
+ {
268
+ name: 'ssh_disconnect',
269
+ description: 'Disconnect an SSH session',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ session_id: {
274
+ type: 'string',
275
+ description: 'The SSH session ID to disconnect',
276
+ },
277
+ },
278
+ required: ['session_id'],
279
+ },
280
+ },
281
+ {
282
+ name: 'ssh_list_sessions',
283
+ description: 'List all active SSH sessions',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {},
287
+ },
288
+ },
289
+ ];
290
+
291
+ function send(message) {
292
+ process.stdout.write(JSON.stringify(message) + '\n');
293
+ }
294
+
295
+ let buffer = '';
296
+
297
+ process.stdin.setEncoding('utf8');
298
+
299
+ process.stdin.on('data', (chunk) => {
300
+ buffer += chunk;
301
+ let newlineIndex;
302
+
303
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
304
+ const line = buffer.slice(0, newlineIndex);
305
+ buffer = buffer.slice(newlineIndex + 1);
306
+
307
+ if (line.trim()) {
308
+ try {
309
+ const request = JSON.parse(line);
310
+ handleRequest(request);
311
+ } catch (err) {
312
+ console.error('Parse error:', err);
313
+ }
314
+ }
315
+ }
316
+ });
317
+
318
+ function handleRequest(request) {
319
+ const { id, method, params } = request;
320
+
321
+ if (method === 'initialize') {
322
+ send({
323
+ jsonrpc: '2.0',
324
+ id,
325
+ result: {
326
+ protocolVersion: '2024-11-05',
327
+ capabilities: { tools: {} },
328
+ serverInfo: {
329
+ name: 'ssh-mcp-server',
330
+ version: '1.0.0',
331
+ },
332
+ },
333
+ });
334
+ return;
335
+ }
336
+
337
+ if (method === 'notifications/initialized') {
338
+ return;
339
+ }
340
+
341
+ if (method === 'tools/list') {
342
+ send({
343
+ jsonrpc: '2.0',
344
+ id,
345
+ result: { tools },
346
+ });
347
+ return;
348
+ }
349
+
350
+ if (method === 'tools/call') {
351
+ const { name, arguments: args } = params;
352
+
353
+ // Handle nested arguments (stringified JSON)
354
+ let toolArgs = args;
355
+ if (typeof args === 'string') {
356
+ try {
357
+ toolArgs = JSON.parse(args);
358
+ } catch (e) {
359
+ toolArgs = {};
360
+ }
361
+ } else if (args && typeof args.arguments === 'string') {
362
+ try {
363
+ toolArgs = JSON.parse(args.arguments);
364
+ } catch (e) {
365
+ toolArgs = args;
366
+ }
367
+ }
368
+
369
+ if (name === 'ssh_connect') {
370
+ sshManager.connect({
371
+ host: toolArgs.host,
372
+ port: toolArgs.port,
373
+ username: toolArgs.username,
374
+ authType: toolArgs.auth_type,
375
+ password: toolArgs.password,
376
+ privateKeyPath: toolArgs.private_key_path,
377
+ passphrase: toolArgs.passphrase,
378
+ timeout: toolArgs.timeout,
379
+ }).then((result) => {
380
+ send({
381
+ jsonrpc: '2.0',
382
+ id,
383
+ result: {
384
+ content: [
385
+ {
386
+ type: 'text',
387
+ text: JSON.stringify(result),
388
+ },
389
+ ],
390
+ },
391
+ });
392
+ });
393
+ return;
394
+ }
395
+
396
+ if (name === 'ssh_exec') {
397
+ const execResult = sshManager.execCommand(toolArgs.session_id, toolArgs.command);
398
+ if (execResult && typeof execResult.then === 'function') {
399
+ execResult.then((result) => {
400
+ send({
401
+ jsonrpc: '2.0',
402
+ id,
403
+ result: {
404
+ content: [
405
+ {
406
+ type: 'text',
407
+ text: JSON.stringify(result),
408
+ },
409
+ ],
410
+ },
411
+ });
412
+ }).catch((err) => {
413
+ send({
414
+ jsonrpc: '2.0',
415
+ id,
416
+ result: {
417
+ content: [
418
+ {
419
+ type: 'text',
420
+ text: JSON.stringify({ success: false, error: err.message }),
421
+ },
422
+ ],
423
+ },
424
+ });
425
+ });
426
+ } else {
427
+ send({
428
+ jsonrpc: '2.0',
429
+ id,
430
+ result: {
431
+ content: [
432
+ {
433
+ type: 'text',
434
+ text: JSON.stringify(execResult),
435
+ },
436
+ ],
437
+ },
438
+ });
439
+ }
440
+ return;
441
+ }
442
+
443
+ if (name === 'ssh_disconnect') {
444
+ const result = sshManager.disconnect(toolArgs.session_id);
445
+ send({
446
+ jsonrpc: '2.0',
447
+ id,
448
+ result: {
449
+ content: [
450
+ {
451
+ type: 'text',
452
+ text: JSON.stringify(result),
453
+ },
454
+ ],
455
+ },
456
+ });
457
+ return;
458
+ }
459
+
460
+ if (name === 'ssh_list_sessions') {
461
+ const result = sshManager.listSessions();
462
+ send({
463
+ jsonrpc: '2.0',
464
+ id,
465
+ result: {
466
+ content: [
467
+ {
468
+ type: 'text',
469
+ text: JSON.stringify(result),
470
+ },
471
+ ],
472
+ },
473
+ });
474
+ return;
475
+ }
476
+
477
+ send({
478
+ jsonrpc: '2.0',
479
+ id,
480
+ result: {
481
+ content: [
482
+ {
483
+ type: 'text',
484
+ text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }),
485
+ },
486
+ ],
487
+ },
488
+ });
489
+ return;
490
+ }
491
+
492
+ send({
493
+ jsonrpc: '2.0',
494
+ id,
495
+ error: {
496
+ code: -32601,
497
+ message: 'Method not found',
498
+ },
499
+ });
500
+ }
package/mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "ssh-mcp-server": {
4
+ "command": "npx",
5
+ "args": ["-y", "mcp-ssh-server-tool"],
6
+ "env": {},
7
+ "description": "SSH MCP Server - Connect to remote servers via SSH and execute commands"
8
+ }
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "mcp-ssh-server-tool",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for SSH connections and remote command execution",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-ssh-server-tool": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": ["mcp", "ssh", "server", "mcp-server", "remote-execution"],
15
+ "author": "",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": ""
20
+ },
21
+ "dependencies": {
22
+ "ssh2": "^1.15.0",
23
+ "uuid": "^9.0.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "preferGlobal": true
29
+ }
package/ssh_manager.js ADDED
@@ -0,0 +1,199 @@
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
+ export 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
+ }