@pyrokine/mcp-ssh 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/src/index.ts ADDED
@@ -0,0 +1,938 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SSH MCP Pro - Main Server Entry
4
+ *
5
+ * A comprehensive SSH MCP Server for Claude Code
6
+ *
7
+ * Features:
8
+ * - Multiple authentication methods (password, key, agent)
9
+ * - Connection pooling with keepalive
10
+ * - Session persistence
11
+ * - Command execution (exec, sudo, su)
12
+ * - File operations (upload, download, read, write)
13
+ * - Environment configuration
14
+ * - Jump host support
15
+ */
16
+
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import {
20
+ CallToolRequestSchema,
21
+ ListToolsRequestSchema,
22
+ Tool,
23
+ } from '@modelcontextprotocol/sdk/types.js';
24
+
25
+ import { sessionManager } from './session-manager.js';
26
+ import * as fileOps from './file-ops.js';
27
+ import { ExecOptions, PtyOptions } from './types.js';
28
+
29
+ // 创建 MCP Server
30
+ const server = new Server(
31
+ {
32
+ name: 'ssh-mcp-pro',
33
+ version: '1.0.0',
34
+ },
35
+ {
36
+ capabilities: {
37
+ tools: {},
38
+ },
39
+ }
40
+ );
41
+
42
+ // 工具定义
43
+ const tools: Tool[] = [
44
+ // ========== 连接管理 ==========
45
+ {
46
+ name: 'ssh_connect',
47
+ description: `建立 SSH 连接并保持会话。支持密码、密钥认证,支持跳板机。
48
+
49
+ 示例:
50
+ - 密码认证: ssh_connect(host="192.168.1.1", user="root", password="xxx")
51
+ - 密钥认证: ssh_connect(host="192.168.1.1", user="root", keyPath="/home/.ssh/id_rsa")
52
+ - 自定义别名: ssh_connect(..., alias="myserver")
53
+ - 设置环境变量: ssh_connect(..., env={"LANG": "en_US.UTF-8"})`,
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ host: { type: 'string', description: '服务器地址' },
58
+ user: { type: 'string', description: '用户名' },
59
+ password: { type: 'string', description: '密码(与 keyPath 二选一)' },
60
+ keyPath: { type: 'string', description: 'SSH 私钥路径' },
61
+ port: { type: 'number', description: 'SSH 端口,默认 22', default: 22 },
62
+ alias: { type: 'string', description: '连接别名(可选,用于后续引用)' },
63
+ env: {
64
+ type: 'object',
65
+ description: '环境变量,如 {"LANG": "en_US.UTF-8"}',
66
+ additionalProperties: { type: 'string' },
67
+ },
68
+ keepaliveInterval: { type: 'number', description: '心跳间隔(毫秒),默认 30000' },
69
+ },
70
+ required: ['host', 'user'],
71
+ },
72
+ },
73
+ {
74
+ name: 'ssh_disconnect',
75
+ description: '断开 SSH 连接',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ alias: { type: 'string', description: '连接别名' },
80
+ },
81
+ required: ['alias'],
82
+ },
83
+ },
84
+ {
85
+ name: 'ssh_list_sessions',
86
+ description: '列出所有活跃的 SSH 会话',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {},
90
+ },
91
+ },
92
+ {
93
+ name: 'ssh_reconnect',
94
+ description: '重新连接已断开的会话',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ alias: { type: 'string', description: '连接别名' },
99
+ },
100
+ required: ['alias'],
101
+ },
102
+ },
103
+
104
+ // ========== 命令执行 ==========
105
+ {
106
+ name: 'ssh_exec',
107
+ description: `在远程服务器执行命令。
108
+
109
+ 返回: stdout, stderr, exitCode, duration`,
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ alias: { type: 'string', description: '连接别名' },
114
+ command: { type: 'string', description: '要执行的命令' },
115
+ timeout: { type: 'number', description: '超时(毫秒),默认 30000' },
116
+ cwd: { type: 'string', description: '工作目录(可选)' },
117
+ env: {
118
+ type: 'object',
119
+ description: '额外环境变量',
120
+ additionalProperties: { type: 'string' },
121
+ },
122
+ pty: { type: 'boolean', description: '是否使用 PTY 模式(用于 top 等交互式命令)' },
123
+ },
124
+ required: ['alias', 'command'],
125
+ },
126
+ },
127
+ {
128
+ name: 'ssh_exec_as_user',
129
+ description: `以其他用户身份执行命令(通过 su 切换)。
130
+
131
+ 适用场景: SSH 以 root 登录,但需要以其他用户(如 caros)执行命令。
132
+
133
+ 示例: ssh_exec_as_user(alias="server", command="whoami", targetUser="caros")`,
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ alias: { type: 'string', description: '连接别名' },
138
+ command: { type: 'string', description: '要执行的命令' },
139
+ targetUser: { type: 'string', description: '目标用户名' },
140
+ timeout: { type: 'number', description: '超时(毫秒)' },
141
+ },
142
+ required: ['alias', 'command', 'targetUser'],
143
+ },
144
+ },
145
+ {
146
+ name: 'ssh_exec_sudo',
147
+ description: '使用 sudo 执行命令',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ alias: { type: 'string', description: '连接别名' },
152
+ command: { type: 'string', description: '要执行的命令' },
153
+ sudoPassword: { type: 'string', description: 'sudo 密码(如果需要)' },
154
+ timeout: { type: 'number', description: '超时(毫秒)' },
155
+ },
156
+ required: ['alias', 'command'],
157
+ },
158
+ },
159
+ {
160
+ name: 'ssh_exec_batch',
161
+ description: '批量执行多条命令',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ alias: { type: 'string', description: '连接别名' },
166
+ commands: {
167
+ type: 'array',
168
+ items: { type: 'string' },
169
+ description: '命令列表',
170
+ },
171
+ stopOnError: { type: 'boolean', description: '遇到错误是否停止,默认 true' },
172
+ timeout: { type: 'number', description: '每条命令的超时(毫秒)' },
173
+ },
174
+ required: ['alias', 'commands'],
175
+ },
176
+ },
177
+ {
178
+ name: 'ssh_quick_exec',
179
+ description: '一次性执行命令(自动连接、执行、断开)。适用于单次命令,不需要保持连接。',
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {
183
+ host: { type: 'string', description: '服务器地址' },
184
+ user: { type: 'string', description: '用户名' },
185
+ command: { type: 'string', description: '要执行的命令' },
186
+ password: { type: 'string', description: '密码' },
187
+ keyPath: { type: 'string', description: '密钥路径' },
188
+ port: { type: 'number', description: '端口', default: 22 },
189
+ timeout: { type: 'number', description: '超时(毫秒)' },
190
+ },
191
+ required: ['host', 'user', 'command'],
192
+ },
193
+ },
194
+
195
+ // ========== 文件操作 ==========
196
+ {
197
+ name: 'ssh_upload',
198
+ description: '上传本地文件到远程服务器',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ alias: { type: 'string', description: '连接别名' },
203
+ localPath: { type: 'string', description: '本地文件路径' },
204
+ remotePath: { type: 'string', description: '远程目标路径' },
205
+ },
206
+ required: ['alias', 'localPath', 'remotePath'],
207
+ },
208
+ },
209
+ {
210
+ name: 'ssh_download',
211
+ description: '从远程服务器下载文件',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ alias: { type: 'string', description: '连接别名' },
216
+ remotePath: { type: 'string', description: '远程文件路径' },
217
+ localPath: { type: 'string', description: '本地保存路径' },
218
+ },
219
+ required: ['alias', 'remotePath', 'localPath'],
220
+ },
221
+ },
222
+ {
223
+ name: 'ssh_read_file',
224
+ description: '读取远程文件内容',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ alias: { type: 'string', description: '连接别名' },
229
+ remotePath: { type: 'string', description: '远程文件路径' },
230
+ maxBytes: { type: 'number', description: '最大读取字节数,默认 1MB' },
231
+ },
232
+ required: ['alias', 'remotePath'],
233
+ },
234
+ },
235
+ {
236
+ name: 'ssh_write_file',
237
+ description: '写入内容到远程文件',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ alias: { type: 'string', description: '连接别名' },
242
+ remotePath: { type: 'string', description: '远程文件路径' },
243
+ content: { type: 'string', description: '要写入的内容' },
244
+ append: { type: 'boolean', description: '是否追加模式,默认覆盖' },
245
+ },
246
+ required: ['alias', 'remotePath', 'content'],
247
+ },
248
+ },
249
+ {
250
+ name: 'ssh_list_dir',
251
+ description: '列出远程目录内容',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ alias: { type: 'string', description: '连接别名' },
256
+ remotePath: { type: 'string', description: '远程目录路径' },
257
+ showHidden: { type: 'boolean', description: '是否显示隐藏文件' },
258
+ },
259
+ required: ['alias', 'remotePath'],
260
+ },
261
+ },
262
+ {
263
+ name: 'ssh_file_info',
264
+ description: '获取远程文件信息(大小、权限、修改时间等)',
265
+ inputSchema: {
266
+ type: 'object',
267
+ properties: {
268
+ alias: { type: 'string', description: '连接别名' },
269
+ remotePath: { type: 'string', description: '远程路径' },
270
+ },
271
+ required: ['alias', 'remotePath'],
272
+ },
273
+ },
274
+ {
275
+ name: 'ssh_mkdir',
276
+ description: '创建远程目录',
277
+ inputSchema: {
278
+ type: 'object',
279
+ properties: {
280
+ alias: { type: 'string', description: '连接别名' },
281
+ remotePath: { type: 'string', description: '远程目录路径' },
282
+ recursive: { type: 'boolean', description: '是否递归创建,默认 false' },
283
+ },
284
+ required: ['alias', 'remotePath'],
285
+ },
286
+ },
287
+ {
288
+ name: 'ssh_sync',
289
+ description: `智能文件同步(支持目录递归)。
290
+
291
+ 优先使用 rsync(如果本地和远程都安装了),否则回退到 SFTP。
292
+ rsync 可实现增量传输,对大目录同步效率更高。
293
+
294
+ 用途:
295
+ - 同步本地目录到远程
296
+ - 从远程同步目录到本地
297
+ - 支持排除特定文件/目录
298
+
299
+ 示例:
300
+ - 上传目录: ssh_sync(alias="server", localPath="/local/dir", remotePath="/remote/dir", direction="upload")
301
+ - 下载目录: ssh_sync(alias="server", localPath="/local/dir", remotePath="/remote/dir", direction="download")
302
+ - 排除文件: ssh_sync(..., exclude=["*.log", "node_modules"])`,
303
+ inputSchema: {
304
+ type: 'object',
305
+ properties: {
306
+ alias: { type: 'string', description: '连接别名' },
307
+ localPath: { type: 'string', description: '本地路径' },
308
+ remotePath: { type: 'string', description: '远程路径' },
309
+ direction: {
310
+ type: 'string',
311
+ enum: ['upload', 'download'],
312
+ description: '同步方向:upload(本地到远程)或 download(远程到本地)',
313
+ },
314
+ delete: { type: 'boolean', description: '删除目标端多余文件(类似 rsync --delete)' },
315
+ dryRun: { type: 'boolean', description: '仅显示将执行的操作,不实际传输' },
316
+ exclude: {
317
+ type: 'array',
318
+ items: { type: 'string' },
319
+ description: '排除模式列表(支持 * 和 ? 通配符)',
320
+ },
321
+ recursive: { type: 'boolean', description: '递归同步目录,默认 true' },
322
+ },
323
+ required: ['alias', 'localPath', 'remotePath', 'direction'],
324
+ },
325
+ },
326
+
327
+ // ========== PTY 会话(持久化交互式终端) ==========
328
+ {
329
+ name: 'ssh_pty_start',
330
+ description: `启动持久化 PTY 会话,支持 top、htop、tmux 等交互式命令。
331
+
332
+ 特点:
333
+ - 输出缓冲区持续收集数据
334
+ - 可通过 ssh_pty_read 轮询读取最新输出
335
+ - 可通过 ssh_pty_write 发送按键/命令
336
+
337
+ 示例:
338
+ - 启动 top: ssh_pty_start(alias="server", command="top")
339
+ - 启动 tmux: ssh_pty_start(alias="server", command="tmux new -s work")`,
340
+ inputSchema: {
341
+ type: 'object',
342
+ properties: {
343
+ alias: { type: 'string', description: '连接别名' },
344
+ command: { type: 'string', description: '要执行的命令' },
345
+ rows: { type: 'number', description: '终端行数,默认 24' },
346
+ cols: { type: 'number', description: '终端列数,默认 80' },
347
+ term: { type: 'string', description: '终端类型,默认 xterm-256color' },
348
+ cwd: { type: 'string', description: '工作目录' },
349
+ env: {
350
+ type: 'object',
351
+ description: '环境变量',
352
+ additionalProperties: { type: 'string' },
353
+ },
354
+ bufferSize: { type: 'number', description: '输出缓冲区大小(字节),默认 1MB' },
355
+ },
356
+ required: ['alias', 'command'],
357
+ },
358
+ },
359
+ {
360
+ name: 'ssh_pty_write',
361
+ description: `向 PTY 写入数据(按键、命令)。
362
+
363
+ 常用控制序列:
364
+ - 回车: "\\r" 或 "\\n"
365
+ - Ctrl+C: "\\x03"
366
+ - Ctrl+D: "\\x04"
367
+ - Ctrl+Z: "\\x1a"
368
+ - 上箭头: "\\x1b[A"
369
+ - 下箭头: "\\x1b[B"
370
+
371
+ 示例:
372
+ - 发送命令: ssh_pty_write(ptyId="xxx", data="ls -la\\r")
373
+ - 退出 top: ssh_pty_write(ptyId="xxx", data="q")`,
374
+ inputSchema: {
375
+ type: 'object',
376
+ properties: {
377
+ ptyId: { type: 'string', description: 'PTY 会话 ID' },
378
+ data: { type: 'string', description: '要写入的数据' },
379
+ },
380
+ required: ['ptyId', 'data'],
381
+ },
382
+ },
383
+ {
384
+ name: 'ssh_pty_read',
385
+ description: `读取 PTY 输出。
386
+
387
+ 两种模式:
388
+ - screen(默认):返回当前屏幕内容(解析后的纯文本,适合 top/btop/htop 等全屏刷新工具)
389
+ - raw:返回原始 ANSI 流(包含转义序列,适合需要完整终端数据的场景)
390
+
391
+ 示例:
392
+ - 获取 top 当前画面: ssh_pty_read(ptyId="xxx")
393
+ - 获取原始输出: ssh_pty_read(ptyId="xxx", mode="raw")`,
394
+ inputSchema: {
395
+ type: 'object',
396
+ properties: {
397
+ ptyId: { type: 'string', description: 'PTY 会话 ID' },
398
+ mode: { type: 'string', enum: ['screen', 'raw'], description: '输出模式:screen(当前屏幕)或 raw(原始流),默认 screen' },
399
+ clear: { type: 'boolean', description: '(仅 raw 模式) 读取后是否清空缓冲区,默认 true' },
400
+ },
401
+ required: ['ptyId'],
402
+ },
403
+ },
404
+ {
405
+ name: 'ssh_pty_resize',
406
+ description: '调整 PTY 窗口大小',
407
+ inputSchema: {
408
+ type: 'object',
409
+ properties: {
410
+ ptyId: { type: 'string', description: 'PTY 会话 ID' },
411
+ rows: { type: 'number', description: '新的行数' },
412
+ cols: { type: 'number', description: '新的列数' },
413
+ },
414
+ required: ['ptyId', 'rows', 'cols'],
415
+ },
416
+ },
417
+ {
418
+ name: 'ssh_pty_close',
419
+ description: '关闭 PTY 会话',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ ptyId: { type: 'string', description: 'PTY 会话 ID' },
424
+ },
425
+ required: ['ptyId'],
426
+ },
427
+ },
428
+ {
429
+ name: 'ssh_pty_list',
430
+ description: '列出所有 PTY 会话',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {},
434
+ },
435
+ },
436
+
437
+ // ========== 端口转发 ==========
438
+ {
439
+ name: 'ssh_forward_local',
440
+ description: `创建本地端口转发(类似 ssh -L)。
441
+
442
+ 本地监听指定端口,将连接转发到远程主机。
443
+
444
+ 用途:访问远程内网服务
445
+ 示例:ssh_forward_local(alias="server", localPort=8080, remoteHost="10.0.0.1", remotePort=80)
446
+ 效果:访问本地 localhost:8080 会转发到远程内网的 10.0.0.1:80`,
447
+ inputSchema: {
448
+ type: 'object',
449
+ properties: {
450
+ alias: { type: 'string', description: '连接别名' },
451
+ localPort: { type: 'number', description: '本地监听端口' },
452
+ remoteHost: { type: 'string', description: '远程目标主机' },
453
+ remotePort: { type: 'number', description: '远程目标端口' },
454
+ localHost: { type: 'string', description: '本地监听地址,默认 127.0.0.1' },
455
+ },
456
+ required: ['alias', 'localPort', 'remoteHost', 'remotePort'],
457
+ },
458
+ },
459
+ {
460
+ name: 'ssh_forward_remote',
461
+ description: `创建远程端口转发(类似 ssh -R)。
462
+
463
+ 远程监听指定端口,将连接转发到本地。
464
+
465
+ 用途:将本地服务暴露到远程
466
+ 示例:ssh_forward_remote(alias="server", remotePort=8080, localHost="127.0.0.1", localPort=3000)
467
+ 效果:远程访问 localhost:8080 会转发到本地的 127.0.0.1:3000`,
468
+ inputSchema: {
469
+ type: 'object',
470
+ properties: {
471
+ alias: { type: 'string', description: '连接别名' },
472
+ remotePort: { type: 'number', description: '远程监听端口' },
473
+ localHost: { type: 'string', description: '本地目标地址' },
474
+ localPort: { type: 'number', description: '本地目标端口' },
475
+ remoteHost: { type: 'string', description: '远程监听地址,默认 127.0.0.1' },
476
+ },
477
+ required: ['alias', 'remotePort', 'localHost', 'localPort'],
478
+ },
479
+ },
480
+ {
481
+ name: 'ssh_forward_close',
482
+ description: '关闭端口转发',
483
+ inputSchema: {
484
+ type: 'object',
485
+ properties: {
486
+ forwardId: { type: 'string', description: '端口转发 ID' },
487
+ },
488
+ required: ['forwardId'],
489
+ },
490
+ },
491
+ {
492
+ name: 'ssh_forward_list',
493
+ description: '列出所有端口转发',
494
+ inputSchema: {
495
+ type: 'object',
496
+ properties: {},
497
+ },
498
+ },
499
+ ];
500
+
501
+ // 注册工具列表
502
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
503
+ tools,
504
+ }));
505
+
506
+ // 处理工具调用
507
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
508
+ const { name, arguments: args = {} } = request.params;
509
+
510
+ try {
511
+ let result: unknown;
512
+
513
+ switch (name) {
514
+ // ========== 连接管理 ==========
515
+ case 'ssh_connect': {
516
+ const alias = await sessionManager.connect({
517
+ host: args.host as string,
518
+ port: (args.port as number) || 22,
519
+ username: args.user as string,
520
+ password: args.password as string | undefined,
521
+ privateKeyPath: args.keyPath as string | undefined,
522
+ alias: args.alias as string | undefined,
523
+ env: args.env as Record<string, string> | undefined,
524
+ keepaliveInterval: args.keepaliveInterval as number | undefined,
525
+ });
526
+ result = {
527
+ success: true,
528
+ alias,
529
+ message: `Connected to ${args.user}@${args.host}:${args.port || 22}`,
530
+ };
531
+ break;
532
+ }
533
+
534
+ case 'ssh_disconnect': {
535
+ const success = sessionManager.disconnect(args.alias as string);
536
+ result = {
537
+ success,
538
+ message: success
539
+ ? `Disconnected from ${args.alias}`
540
+ : `Session ${args.alias} not found`,
541
+ };
542
+ break;
543
+ }
544
+
545
+ case 'ssh_list_sessions': {
546
+ const sessions = sessionManager.listSessions();
547
+ result = {
548
+ success: true,
549
+ count: sessions.length,
550
+ sessions,
551
+ };
552
+ break;
553
+ }
554
+
555
+ case 'ssh_reconnect': {
556
+ await sessionManager.reconnect(args.alias as string);
557
+ result = { success: true, message: `Reconnected to ${args.alias}` };
558
+ break;
559
+ }
560
+
561
+ // ========== 命令执行 ==========
562
+ case 'ssh_exec': {
563
+ const execResult = await sessionManager.exec(
564
+ args.alias as string,
565
+ args.command as string,
566
+ {
567
+ timeout: args.timeout as number | undefined,
568
+ cwd: args.cwd as string | undefined,
569
+ env: args.env as Record<string, string> | undefined,
570
+ pty: args.pty as boolean | undefined,
571
+ }
572
+ );
573
+ result = execResult;
574
+ break;
575
+ }
576
+
577
+ case 'ssh_exec_as_user': {
578
+ const execResult = await sessionManager.execAsUser(
579
+ args.alias as string,
580
+ args.command as string,
581
+ args.targetUser as string,
582
+ { timeout: args.timeout as number | undefined }
583
+ );
584
+ result = execResult;
585
+ break;
586
+ }
587
+
588
+ case 'ssh_exec_sudo': {
589
+ const execResult = await sessionManager.execSudo(
590
+ args.alias as string,
591
+ args.command as string,
592
+ args.sudoPassword as string | undefined,
593
+ { timeout: args.timeout as number | undefined }
594
+ );
595
+ result = execResult;
596
+ break;
597
+ }
598
+
599
+ case 'ssh_exec_batch': {
600
+ const commands = args.commands as string[];
601
+ const stopOnError = args.stopOnError !== false;
602
+ const timeout = args.timeout as number | undefined;
603
+ const results: any[] = [];
604
+
605
+ for (let i = 0; i < commands.length; i++) {
606
+ try {
607
+ const execResult = await sessionManager.exec(
608
+ args.alias as string,
609
+ commands[i],
610
+ { timeout }
611
+ );
612
+ results.push({
613
+ index: i,
614
+ command: commands[i],
615
+ ...execResult,
616
+ });
617
+ if (execResult.exitCode !== 0 && stopOnError) {
618
+ break;
619
+ }
620
+ } catch (err: any) {
621
+ results.push({
622
+ index: i,
623
+ command: commands[i],
624
+ success: false,
625
+ error: err.message,
626
+ });
627
+ if (stopOnError) break;
628
+ }
629
+ }
630
+
631
+ result = {
632
+ success: results.every((r) => r.success),
633
+ total: commands.length,
634
+ executed: results.length,
635
+ results,
636
+ };
637
+ break;
638
+ }
639
+
640
+ case 'ssh_quick_exec': {
641
+ const tempAlias = `_quick_${Date.now()}`;
642
+ try {
643
+ await sessionManager.connect({
644
+ host: args.host as string,
645
+ port: (args.port as number) || 22,
646
+ username: args.user as string,
647
+ password: args.password as string | undefined,
648
+ privateKeyPath: args.keyPath as string | undefined,
649
+ alias: tempAlias,
650
+ });
651
+ const execResult = await sessionManager.exec(
652
+ tempAlias,
653
+ args.command as string,
654
+ { timeout: args.timeout as number | undefined }
655
+ );
656
+ result = execResult;
657
+ } finally {
658
+ sessionManager.disconnect(tempAlias);
659
+ }
660
+ break;
661
+ }
662
+
663
+ // ========== 文件操作 ==========
664
+ case 'ssh_upload': {
665
+ const uploadResult = await fileOps.uploadFile(
666
+ args.alias as string,
667
+ args.localPath as string,
668
+ args.remotePath as string
669
+ );
670
+ result = { ...uploadResult, message: `Uploaded to ${args.remotePath}` };
671
+ break;
672
+ }
673
+
674
+ case 'ssh_download': {
675
+ const downloadResult = await fileOps.downloadFile(
676
+ args.alias as string,
677
+ args.remotePath as string,
678
+ args.localPath as string
679
+ );
680
+ result = { ...downloadResult, message: `Downloaded to ${args.localPath}` };
681
+ break;
682
+ }
683
+
684
+ case 'ssh_read_file': {
685
+ const readResult = await fileOps.readFile(
686
+ args.alias as string,
687
+ args.remotePath as string,
688
+ args.maxBytes as number | undefined
689
+ );
690
+ result = { success: true, ...readResult };
691
+ break;
692
+ }
693
+
694
+ case 'ssh_write_file': {
695
+ const writeResult = await fileOps.writeFile(
696
+ args.alias as string,
697
+ args.remotePath as string,
698
+ args.content as string,
699
+ args.append as boolean | undefined
700
+ );
701
+ result = writeResult;
702
+ break;
703
+ }
704
+
705
+ case 'ssh_list_dir': {
706
+ const files = await fileOps.listDir(
707
+ args.alias as string,
708
+ args.remotePath as string,
709
+ args.showHidden as boolean | undefined
710
+ );
711
+ result = {
712
+ success: true,
713
+ path: args.remotePath,
714
+ count: files.length,
715
+ files,
716
+ };
717
+ break;
718
+ }
719
+
720
+ case 'ssh_file_info': {
721
+ const info = await fileOps.getFileInfo(
722
+ args.alias as string,
723
+ args.remotePath as string
724
+ );
725
+ result = { success: true, ...info };
726
+ break;
727
+ }
728
+
729
+ case 'ssh_mkdir': {
730
+ const success = await fileOps.mkdir(
731
+ args.alias as string,
732
+ args.remotePath as string,
733
+ args.recursive as boolean | undefined
734
+ );
735
+ result = { success, path: args.remotePath };
736
+ break;
737
+ }
738
+
739
+ case 'ssh_sync': {
740
+ const syncResult = await fileOps.syncFiles(
741
+ args.alias as string,
742
+ args.localPath as string,
743
+ args.remotePath as string,
744
+ args.direction as 'upload' | 'download',
745
+ {
746
+ delete: args.delete as boolean | undefined,
747
+ dryRun: args.dryRun as boolean | undefined,
748
+ exclude: args.exclude as string[] | undefined,
749
+ recursive: args.recursive as boolean | undefined,
750
+ }
751
+ );
752
+ result = {
753
+ ...syncResult,
754
+ direction: args.direction,
755
+ localPath: args.localPath,
756
+ remotePath: args.remotePath,
757
+ };
758
+ break;
759
+ }
760
+
761
+ // ========== PTY 会话 ==========
762
+ case 'ssh_pty_start': {
763
+ const ptyId = await sessionManager.ptyStart(
764
+ args.alias as string,
765
+ args.command as string,
766
+ {
767
+ rows: args.rows as number | undefined,
768
+ cols: args.cols as number | undefined,
769
+ term: args.term as string | undefined,
770
+ cwd: args.cwd as string | undefined,
771
+ env: args.env as Record<string, string> | undefined,
772
+ bufferSize: args.bufferSize as number | undefined,
773
+ }
774
+ );
775
+ result = {
776
+ success: true,
777
+ ptyId,
778
+ message: `PTY session started: ${args.command}`,
779
+ };
780
+ break;
781
+ }
782
+
783
+ case 'ssh_pty_write': {
784
+ const success = sessionManager.ptyWrite(
785
+ args.ptyId as string,
786
+ args.data as string
787
+ );
788
+ result = { success, ptyId: args.ptyId };
789
+ break;
790
+ }
791
+
792
+ case 'ssh_pty_read': {
793
+ const readResult = sessionManager.ptyRead(
794
+ args.ptyId as string,
795
+ {
796
+ mode: (args.mode as 'screen' | 'raw') || 'screen',
797
+ clear: args.clear !== false,
798
+ }
799
+ );
800
+ result = {
801
+ success: true,
802
+ ptyId: args.ptyId,
803
+ mode: args.mode || 'screen',
804
+ ...readResult,
805
+ };
806
+ break;
807
+ }
808
+
809
+ case 'ssh_pty_resize': {
810
+ const success = sessionManager.ptyResize(
811
+ args.ptyId as string,
812
+ args.rows as number,
813
+ args.cols as number
814
+ );
815
+ result = { success, ptyId: args.ptyId };
816
+ break;
817
+ }
818
+
819
+ case 'ssh_pty_close': {
820
+ const success = sessionManager.ptyClose(args.ptyId as string);
821
+ result = {
822
+ success,
823
+ message: success
824
+ ? `PTY session closed: ${args.ptyId}`
825
+ : `PTY session not found: ${args.ptyId}`,
826
+ };
827
+ break;
828
+ }
829
+
830
+ case 'ssh_pty_list': {
831
+ const ptySessions = sessionManager.ptyList();
832
+ result = {
833
+ success: true,
834
+ count: ptySessions.length,
835
+ sessions: ptySessions,
836
+ };
837
+ break;
838
+ }
839
+
840
+ // ========== 端口转发 ==========
841
+ case 'ssh_forward_local': {
842
+ const forwardId = await sessionManager.forwardLocal(
843
+ args.alias as string,
844
+ args.localPort as number,
845
+ args.remoteHost as string,
846
+ args.remotePort as number,
847
+ (args.localHost as string) || '127.0.0.1'
848
+ );
849
+ result = {
850
+ success: true,
851
+ forwardId,
852
+ type: 'local',
853
+ message: `Local forward: ${args.localHost || '127.0.0.1'}:${args.localPort} -> ${args.remoteHost}:${args.remotePort}`,
854
+ };
855
+ break;
856
+ }
857
+
858
+ case 'ssh_forward_remote': {
859
+ const forwardId = await sessionManager.forwardRemote(
860
+ args.alias as string,
861
+ args.remotePort as number,
862
+ args.localHost as string,
863
+ args.localPort as number,
864
+ (args.remoteHost as string) || '127.0.0.1'
865
+ );
866
+ result = {
867
+ success: true,
868
+ forwardId,
869
+ type: 'remote',
870
+ message: `Remote forward: ${args.remoteHost || '127.0.0.1'}:${args.remotePort} -> ${args.localHost}:${args.localPort}`,
871
+ };
872
+ break;
873
+ }
874
+
875
+ case 'ssh_forward_close': {
876
+ const success = sessionManager.forwardClose(args.forwardId as string);
877
+ result = {
878
+ success,
879
+ message: success
880
+ ? `Forward closed: ${args.forwardId}`
881
+ : `Forward not found: ${args.forwardId}`,
882
+ };
883
+ break;
884
+ }
885
+
886
+ case 'ssh_forward_list': {
887
+ const forwards = sessionManager.forwardList();
888
+ result = {
889
+ success: true,
890
+ count: forwards.length,
891
+ forwards,
892
+ };
893
+ break;
894
+ }
895
+
896
+ default:
897
+ throw new Error(`Unknown tool: ${name}`);
898
+ }
899
+
900
+ return {
901
+ content: [
902
+ {
903
+ type: 'text',
904
+ text: JSON.stringify(result, null, 2),
905
+ },
906
+ ],
907
+ };
908
+ } catch (error: any) {
909
+ return {
910
+ content: [
911
+ {
912
+ type: 'text',
913
+ text: JSON.stringify(
914
+ {
915
+ success: false,
916
+ error: error.message || String(error),
917
+ },
918
+ null,
919
+ 2
920
+ ),
921
+ },
922
+ ],
923
+ isError: true,
924
+ };
925
+ }
926
+ });
927
+
928
+ // 启动服务器
929
+ async function main() {
930
+ const transport = new StdioServerTransport();
931
+ await server.connect(transport);
932
+ console.error('SSH MCP Pro server started');
933
+ }
934
+
935
+ main().catch((error) => {
936
+ console.error('Fatal error:', error);
937
+ process.exit(1);
938
+ });