@pyrokine/mcp-ssh 1.0.0 → 1.1.2
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 +162 -83
- package/README_zh.md +161 -83
- package/dist/file-ops.js +36 -20
- package/dist/index.js +197 -23
- package/dist/session-manager.d.ts +62 -51
- package/dist/session-manager.js +201 -168
- package/dist/ssh-config.d.ts +39 -0
- package/dist/ssh-config.js +216 -0
- package/package.json +2 -2
- package/src/file-ops.ts +602 -577
- package/src/index.ts +991 -800
- package/src/session-manager.ts +986 -945
- package/src/ssh-config.ts +264 -0
- package/src/types.ts +89 -89
- package/tsconfig.json +7 -2
package/dist/file-ops.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSH File Operations - 文件操作
|
|
3
3
|
*/
|
|
4
|
+
import { execSync } from 'child_process';
|
|
4
5
|
import * as fs from 'fs';
|
|
5
6
|
import * as path from 'path';
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
7
|
import { sessionManager } from './session-manager.js';
|
|
8
8
|
/**
|
|
9
9
|
* 上传文件
|
|
@@ -20,12 +20,14 @@ export async function uploadFile(alias, localPath, remotePath, onProgress) {
|
|
|
20
20
|
const writeStream = sftp.createWriteStream(remotePath);
|
|
21
21
|
let settled = false;
|
|
22
22
|
const cleanup = (err) => {
|
|
23
|
-
if (settled)
|
|
23
|
+
if (settled) {
|
|
24
24
|
return;
|
|
25
|
+
}
|
|
25
26
|
settled = true;
|
|
26
27
|
sftp.end();
|
|
27
|
-
if (err)
|
|
28
|
+
if (err) {
|
|
28
29
|
reject(err);
|
|
30
|
+
}
|
|
29
31
|
};
|
|
30
32
|
let transferred = 0;
|
|
31
33
|
readStream.on('data', (chunk) => {
|
|
@@ -58,10 +60,12 @@ export async function downloadFile(alias, remotePath, localPath, onProgress) {
|
|
|
58
60
|
// 获取远程文件大小
|
|
59
61
|
const stats = await new Promise((resolve, reject) => {
|
|
60
62
|
sftp.stat(remotePath, (err, stats) => {
|
|
61
|
-
if (err)
|
|
63
|
+
if (err) {
|
|
62
64
|
reject(err);
|
|
63
|
-
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
64
67
|
resolve(stats);
|
|
68
|
+
}
|
|
65
69
|
});
|
|
66
70
|
});
|
|
67
71
|
const totalSize = stats.size;
|
|
@@ -75,12 +79,14 @@ export async function downloadFile(alias, remotePath, localPath, onProgress) {
|
|
|
75
79
|
const writeStream = fs.createWriteStream(localPath);
|
|
76
80
|
let settled = false;
|
|
77
81
|
const cleanup = (err) => {
|
|
78
|
-
if (settled)
|
|
82
|
+
if (settled) {
|
|
79
83
|
return;
|
|
84
|
+
}
|
|
80
85
|
settled = true;
|
|
81
86
|
sftp.end();
|
|
82
|
-
if (err)
|
|
87
|
+
if (err) {
|
|
83
88
|
reject(err);
|
|
89
|
+
}
|
|
84
90
|
};
|
|
85
91
|
let transferred = 0;
|
|
86
92
|
readStream.on('data', (chunk) => {
|
|
@@ -108,16 +114,17 @@ export async function downloadFile(alias, remotePath, localPath, onProgress) {
|
|
|
108
114
|
/**
|
|
109
115
|
* 读取远程文件内容
|
|
110
116
|
*/
|
|
111
|
-
export async function readFile(alias, remotePath, maxBytes = 1024 * 1024
|
|
112
|
-
) {
|
|
117
|
+
export async function readFile(alias, remotePath, maxBytes = 1024 * 1024) {
|
|
113
118
|
const sftp = await sessionManager.getSftp(alias);
|
|
114
119
|
// 获取文件大小
|
|
115
120
|
const stats = await new Promise((resolve, reject) => {
|
|
116
121
|
sftp.stat(remotePath, (err, stats) => {
|
|
117
|
-
if (err)
|
|
122
|
+
if (err) {
|
|
118
123
|
reject(err);
|
|
119
|
-
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
120
126
|
resolve(stats);
|
|
127
|
+
}
|
|
121
128
|
});
|
|
122
129
|
});
|
|
123
130
|
const actualSize = stats.size;
|
|
@@ -264,10 +271,12 @@ export async function mkdir(alias, remotePath, recursive = false) {
|
|
|
264
271
|
return new Promise((resolve, reject) => {
|
|
265
272
|
sftp.mkdir(remotePath, (err) => {
|
|
266
273
|
sftp.end();
|
|
267
|
-
if (err)
|
|
274
|
+
if (err) {
|
|
268
275
|
reject(err);
|
|
269
|
-
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
270
278
|
resolve(true);
|
|
279
|
+
}
|
|
271
280
|
});
|
|
272
281
|
});
|
|
273
282
|
}
|
|
@@ -279,10 +288,12 @@ export async function removeFile(alias, remotePath) {
|
|
|
279
288
|
return new Promise((resolve, reject) => {
|
|
280
289
|
sftp.unlink(remotePath, (err) => {
|
|
281
290
|
sftp.end();
|
|
282
|
-
if (err)
|
|
291
|
+
if (err) {
|
|
283
292
|
reject(err);
|
|
284
|
-
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
285
295
|
resolve(true);
|
|
296
|
+
}
|
|
286
297
|
});
|
|
287
298
|
});
|
|
288
299
|
}
|
|
@@ -323,7 +334,7 @@ export async function syncFiles(alias, localPath, remotePath, direction, options
|
|
|
323
334
|
* 转义 shell 路径参数
|
|
324
335
|
*/
|
|
325
336
|
function escapeShellPath(p) {
|
|
326
|
-
return `'${p.replace(/'/g,
|
|
337
|
+
return `'${p.replace(/'/g, '\'\\\'\'')}'`;
|
|
327
338
|
}
|
|
328
339
|
/**
|
|
329
340
|
* 使用 rsync 同步文件
|
|
@@ -336,7 +347,8 @@ async function syncWithRsync(alias, localPath, remotePath, direction, options) {
|
|
|
336
347
|
execSync('which rsync', { stdio: 'pipe' });
|
|
337
348
|
hasLocalRsync = true;
|
|
338
349
|
}
|
|
339
|
-
catch {
|
|
350
|
+
catch {
|
|
351
|
+
}
|
|
340
352
|
if (!hasLocalRsync) {
|
|
341
353
|
// 本地没有 rsync,回退到 SFTP
|
|
342
354
|
return syncWithSftp(alias, localPath, remotePath, direction, options);
|
|
@@ -374,13 +386,16 @@ async function syncWithRsync(alias, localPath, remotePath, direction, options) {
|
|
|
374
386
|
const result = execSync(rsyncCmd, {
|
|
375
387
|
encoding: 'utf-8',
|
|
376
388
|
timeout: 600000, // 10 分钟超时
|
|
377
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
389
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
378
390
|
});
|
|
379
391
|
// 解析 rsync 输出统计文件数
|
|
380
392
|
const lines = result.split('\n');
|
|
381
393
|
let filesTransferred = 0;
|
|
382
394
|
for (const line of lines) {
|
|
383
|
-
if (line.trim() &&
|
|
395
|
+
if (line.trim() &&
|
|
396
|
+
!line.startsWith('sending') &&
|
|
397
|
+
!line.startsWith('receiving') &&
|
|
398
|
+
!line.startsWith('total')) {
|
|
384
399
|
filesTransferred++;
|
|
385
400
|
}
|
|
386
401
|
}
|
|
@@ -409,7 +424,8 @@ async function syncWithSftp(alias, localPath, remotePath, direction, options) {
|
|
|
409
424
|
return {
|
|
410
425
|
success: true,
|
|
411
426
|
method: 'sftp',
|
|
412
|
-
output: 'Dry run mode: would transfer files via SFTP' +
|
|
427
|
+
output: 'Dry run mode: would transfer files via SFTP' +
|
|
428
|
+
(warnings.length ? `. Warning: ${warnings.join('; ')}` : ''),
|
|
413
429
|
};
|
|
414
430
|
}
|
|
415
431
|
try {
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
17
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
-
import { CallToolRequestSchema, ListToolsRequestSchema
|
|
19
|
-
import { sessionManager } from './session-manager.js';
|
|
18
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
20
19
|
import * as fileOps from './file-ops.js';
|
|
20
|
+
import { sessionManager } from './session-manager.js';
|
|
21
|
+
import { parseProxyJump, parseSSHConfig } from './ssh-config.js';
|
|
21
22
|
// 创建 MCP Server
|
|
22
23
|
const server = new Server({
|
|
23
24
|
name: 'ssh-mcp-pro',
|
|
@@ -34,28 +35,43 @@ const tools = [
|
|
|
34
35
|
name: 'ssh_connect',
|
|
35
36
|
description: `建立 SSH 连接并保持会话。支持密码、密钥认证,支持跳板机。
|
|
36
37
|
|
|
38
|
+
可通过 configHost 参数使用 ~/.ssh/config 中的配置,无需重复填写连接信息。
|
|
39
|
+
支持 Host 多别名、Host * 全局默认继承、ProxyJump(user@host:port 格式)。
|
|
40
|
+
|
|
37
41
|
示例:
|
|
38
|
-
-
|
|
42
|
+
- 使用 ssh config: ssh_connect(configHost="myserver")
|
|
39
43
|
- 密钥认证: ssh_connect(host="192.168.1.1", user="root", keyPath="/home/.ssh/id_rsa")
|
|
40
|
-
-
|
|
41
|
-
- 设置环境变量: ssh_connect(..., env={"LANG": "en_US.UTF-8"})`,
|
|
44
|
+
- 跳板机: ssh_connect(host="内网IP", user="root", keyPath="...", jumpHost={host:"跳板机IP", user:"root", keyPath:"..."})`,
|
|
42
45
|
inputSchema: {
|
|
43
46
|
type: 'object',
|
|
44
47
|
properties: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
configHost: { type: 'string', description: '使用 ~/.ssh/config 中的 Host 配置(推荐)' },
|
|
49
|
+
configPath: { type: 'string', description: 'SSH 配置文件路径(默认 ~/.ssh/config)' },
|
|
50
|
+
host: { type: 'string', description: '服务器地址(使用 configHost 时可省略)' },
|
|
51
|
+
user: { type: 'string', description: '用户名(使用 configHost 时可省略)' },
|
|
52
|
+
password: { type: 'string', description: '密码' },
|
|
48
53
|
keyPath: { type: 'string', description: 'SSH 私钥路径' },
|
|
49
|
-
port: { type: 'number', description: 'SSH 端口,默认 22'
|
|
50
|
-
alias: { type: 'string', description: '
|
|
54
|
+
port: { type: 'number', description: 'SSH 端口,默认 22' },
|
|
55
|
+
alias: { type: 'string', description: '连接别名(可选,默认使用 configHost 或 host)' },
|
|
51
56
|
env: {
|
|
52
57
|
type: 'object',
|
|
53
|
-
description: '
|
|
58
|
+
description: '环境变量',
|
|
54
59
|
additionalProperties: { type: 'string' },
|
|
55
60
|
},
|
|
56
61
|
keepaliveInterval: { type: 'number', description: '心跳间隔(毫秒),默认 30000' },
|
|
62
|
+
jumpHost: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
description: '跳板机配置',
|
|
65
|
+
properties: {
|
|
66
|
+
host: { type: 'string', description: '跳板机地址' },
|
|
67
|
+
user: { type: 'string', description: '跳板机用户名' },
|
|
68
|
+
password: { type: 'string', description: '跳板机密码' },
|
|
69
|
+
keyPath: { type: 'string', description: '跳板机私钥路径' },
|
|
70
|
+
port: { type: 'number', description: '跳板机端口,默认 22' },
|
|
71
|
+
},
|
|
72
|
+
required: ['host', 'user'],
|
|
73
|
+
},
|
|
57
74
|
},
|
|
58
|
-
required: ['host', 'user'],
|
|
59
75
|
},
|
|
60
76
|
},
|
|
61
77
|
{
|
|
@@ -117,6 +133,9 @@ const tools = [
|
|
|
117
133
|
|
|
118
134
|
适用场景: SSH 以 root 登录,但需要以其他用户(如 caros)执行命令。
|
|
119
135
|
|
|
136
|
+
默认加载目标用户的 shell 配置以获取环境变量(su -c 创建非交互式 shell,不会自动执行 rc 文件)。
|
|
137
|
+
支持 bash(.bashrc)、zsh(.zshrc) 及其他 shell(.profile)。
|
|
138
|
+
|
|
120
139
|
示例: ssh_exec_as_user(alias="server", command="whoami", targetUser="caros")`,
|
|
121
140
|
inputSchema: {
|
|
122
141
|
type: 'object',
|
|
@@ -125,6 +144,7 @@ const tools = [
|
|
|
125
144
|
command: { type: 'string', description: '要执行的命令' },
|
|
126
145
|
targetUser: { type: 'string', description: '目标用户名' },
|
|
127
146
|
timeout: { type: 'number', description: '超时(毫秒)' },
|
|
147
|
+
loadProfile: { type: 'boolean', description: '是否加载 .bashrc(默认 true)' },
|
|
128
148
|
},
|
|
129
149
|
required: ['alias', 'command', 'targetUser'],
|
|
130
150
|
},
|
|
@@ -380,7 +400,11 @@ rsync 可实现增量传输,对大目录同步效率更高。
|
|
|
380
400
|
type: 'object',
|
|
381
401
|
properties: {
|
|
382
402
|
ptyId: { type: 'string', description: 'PTY 会话 ID' },
|
|
383
|
-
mode: {
|
|
403
|
+
mode: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
enum: ['screen', 'raw'],
|
|
406
|
+
description: '输出模式:screen(当前屏幕)或 raw(原始流),默认 screen',
|
|
407
|
+
},
|
|
384
408
|
clear: { type: 'boolean', description: '(仅 raw 模式) 读取后是否清空缓冲区,默认 true' },
|
|
385
409
|
},
|
|
386
410
|
required: ['ptyId'],
|
|
@@ -480,6 +504,42 @@ rsync 可实现增量传输,对大目录同步效率更高。
|
|
|
480
504
|
properties: {},
|
|
481
505
|
},
|
|
482
506
|
},
|
|
507
|
+
// ========== SSH Config ==========
|
|
508
|
+
{
|
|
509
|
+
name: 'ssh_config_list',
|
|
510
|
+
description: `列出 ~/.ssh/config 中配置的所有 Host。
|
|
511
|
+
|
|
512
|
+
返回每个 Host 的配置信息(别名、地址、用户、端口、密钥路径等)。`,
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
configPath: { type: 'string', description: 'SSH 配置文件路径(默认 ~/.ssh/config)' },
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
// ========== 批量执行 ==========
|
|
521
|
+
{
|
|
522
|
+
name: 'ssh_exec_parallel',
|
|
523
|
+
description: `在多个已连接的会话上并行执行同一命令。
|
|
524
|
+
|
|
525
|
+
示例:
|
|
526
|
+
- ssh_exec_parallel(aliases=["server1", "server2"], command="uptime")
|
|
527
|
+
|
|
528
|
+
返回每个主机的执行结果。`,
|
|
529
|
+
inputSchema: {
|
|
530
|
+
type: 'object',
|
|
531
|
+
properties: {
|
|
532
|
+
aliases: {
|
|
533
|
+
type: 'array',
|
|
534
|
+
items: { type: 'string' },
|
|
535
|
+
description: '连接别名列表',
|
|
536
|
+
},
|
|
537
|
+
command: { type: 'string', description: '要执行的命令' },
|
|
538
|
+
timeout: { type: 'number', description: '每个命令的超时(毫秒),默认 30000' },
|
|
539
|
+
},
|
|
540
|
+
required: ['aliases', 'command'],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
483
543
|
];
|
|
484
544
|
// 注册工具列表
|
|
485
545
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -493,20 +553,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
493
553
|
switch (name) {
|
|
494
554
|
// ========== 连接管理 ==========
|
|
495
555
|
case 'ssh_connect': {
|
|
556
|
+
// 解析 configHost
|
|
557
|
+
let host = args.host;
|
|
558
|
+
let user = args.user;
|
|
559
|
+
let port = args.port;
|
|
560
|
+
let keyPath = args.keyPath;
|
|
561
|
+
const configPath = args.configPath;
|
|
562
|
+
let jumpHostResolved;
|
|
563
|
+
if (args.configHost) {
|
|
564
|
+
const allHosts = parseSSHConfig(configPath);
|
|
565
|
+
const hostConfig = allHosts.find(h => h.host === args.configHost);
|
|
566
|
+
if (!hostConfig) {
|
|
567
|
+
throw new Error(`Host '${args.configHost}' not found in SSH config`);
|
|
568
|
+
}
|
|
569
|
+
// 显式参数优先于 config 值
|
|
570
|
+
host = host || hostConfig.hostName || hostConfig.host;
|
|
571
|
+
user = user || hostConfig.user;
|
|
572
|
+
port = port || hostConfig.port;
|
|
573
|
+
keyPath = keyPath || hostConfig.identityFile;
|
|
574
|
+
// 解析 ProxyJump(支持 user@host:port 格式)
|
|
575
|
+
if (hostConfig.proxyJump) {
|
|
576
|
+
const parsed = parseProxyJump(hostConfig.proxyJump);
|
|
577
|
+
if (parsed) {
|
|
578
|
+
// 先尝试在 config 中查找对应的 Host
|
|
579
|
+
const jumpHostConfig = allHosts.find(h => h.host === parsed.host);
|
|
580
|
+
if (jumpHostConfig) {
|
|
581
|
+
// 使用 config 中的配置,但 parsed 的 user/port 优先
|
|
582
|
+
jumpHostResolved = {
|
|
583
|
+
host: jumpHostConfig.hostName || jumpHostConfig.host,
|
|
584
|
+
port: parsed.port || jumpHostConfig.port || 22,
|
|
585
|
+
username: parsed.user || jumpHostConfig.user || 'root',
|
|
586
|
+
privateKeyPath: jumpHostConfig.identityFile,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
// 直接使用 parsed 的值
|
|
591
|
+
jumpHostResolved = {
|
|
592
|
+
host: parsed.host,
|
|
593
|
+
port: parsed.port || 22,
|
|
594
|
+
username: parsed.user || 'root',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!host || !user) {
|
|
601
|
+
throw new Error('host and user are required (either directly or via configHost)');
|
|
602
|
+
}
|
|
603
|
+
// 手动指定的 jumpHost 优先级高于 ProxyJump
|
|
604
|
+
const jumpHostArg = args.jumpHost;
|
|
605
|
+
const jumpHost = jumpHostArg ? {
|
|
606
|
+
host: jumpHostArg.host,
|
|
607
|
+
port: jumpHostArg.port || 22,
|
|
608
|
+
username: jumpHostArg.user,
|
|
609
|
+
password: jumpHostArg.password,
|
|
610
|
+
privateKeyPath: jumpHostArg.keyPath,
|
|
611
|
+
} : jumpHostResolved;
|
|
496
612
|
const alias = await sessionManager.connect({
|
|
497
|
-
host
|
|
498
|
-
port:
|
|
499
|
-
username:
|
|
613
|
+
host,
|
|
614
|
+
port: port || 22,
|
|
615
|
+
username: user,
|
|
500
616
|
password: args.password,
|
|
501
|
-
privateKeyPath:
|
|
502
|
-
alias: args.alias
|
|
617
|
+
privateKeyPath: keyPath,
|
|
618
|
+
alias: args.alias ||
|
|
619
|
+
args.configHost,
|
|
503
620
|
env: args.env,
|
|
504
621
|
keepaliveInterval: args.keepaliveInterval,
|
|
622
|
+
jumpHost,
|
|
505
623
|
});
|
|
506
624
|
result = {
|
|
507
625
|
success: true,
|
|
508
626
|
alias,
|
|
509
|
-
message: `Connected to ${
|
|
627
|
+
message: `Connected to ${user}@${host}:${port || 22}${jumpHost ? ' via jump host' : ''}`,
|
|
510
628
|
};
|
|
511
629
|
break;
|
|
512
630
|
}
|
|
@@ -546,7 +664,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
546
664
|
break;
|
|
547
665
|
}
|
|
548
666
|
case 'ssh_exec_as_user': {
|
|
549
|
-
const execResult = await sessionManager.execAsUser(args.alias, args.command, args.targetUser, {
|
|
667
|
+
const execResult = await sessionManager.execAsUser(args.alias, args.command, args.targetUser, {
|
|
668
|
+
timeout: args.timeout,
|
|
669
|
+
loadProfile: args.loadProfile,
|
|
670
|
+
});
|
|
550
671
|
result = execResult;
|
|
551
672
|
break;
|
|
552
673
|
}
|
|
@@ -579,8 +700,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
579
700
|
success: false,
|
|
580
701
|
error: err.message,
|
|
581
702
|
});
|
|
582
|
-
if (stopOnError)
|
|
703
|
+
if (stopOnError) {
|
|
583
704
|
break;
|
|
705
|
+
}
|
|
584
706
|
}
|
|
585
707
|
}
|
|
586
708
|
result = {
|
|
@@ -732,7 +854,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
732
854
|
success: true,
|
|
733
855
|
forwardId,
|
|
734
856
|
type: 'local',
|
|
735
|
-
message: `Local forward: ${args.localHost ||
|
|
857
|
+
message: `Local forward: ${args.localHost ||
|
|
858
|
+
'127.0.0.1'}:${args.localPort} -> ${args.remoteHost}:${args.remotePort}`,
|
|
736
859
|
};
|
|
737
860
|
break;
|
|
738
861
|
}
|
|
@@ -742,7 +865,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
742
865
|
success: true,
|
|
743
866
|
forwardId,
|
|
744
867
|
type: 'remote',
|
|
745
|
-
message: `Remote forward: ${args.remoteHost ||
|
|
868
|
+
message: `Remote forward: ${args.remoteHost ||
|
|
869
|
+
'127.0.0.1'}:${args.remotePort} -> ${args.localHost}:${args.localPort}`,
|
|
746
870
|
};
|
|
747
871
|
break;
|
|
748
872
|
}
|
|
@@ -765,6 +889,56 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
765
889
|
};
|
|
766
890
|
break;
|
|
767
891
|
}
|
|
892
|
+
// ========== SSH Config ==========
|
|
893
|
+
case 'ssh_config_list': {
|
|
894
|
+
const hosts = parseSSHConfig(args.configPath);
|
|
895
|
+
result = {
|
|
896
|
+
success: true,
|
|
897
|
+
count: hosts.length,
|
|
898
|
+
hosts: hosts.map(h => ({
|
|
899
|
+
host: h.host,
|
|
900
|
+
hostName: h.hostName,
|
|
901
|
+
user: h.user,
|
|
902
|
+
port: h.port,
|
|
903
|
+
identityFile: h.identityFile,
|
|
904
|
+
proxyJump: h.proxyJump,
|
|
905
|
+
})),
|
|
906
|
+
};
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
// ========== 批量执行 ==========
|
|
910
|
+
case 'ssh_exec_parallel': {
|
|
911
|
+
const aliases = args.aliases;
|
|
912
|
+
const command = args.command;
|
|
913
|
+
const timeout = args.timeout;
|
|
914
|
+
const execPromises = aliases.map(async (alias) => {
|
|
915
|
+
try {
|
|
916
|
+
const execResult = await sessionManager.exec(alias, command, { timeout });
|
|
917
|
+
return {
|
|
918
|
+
alias,
|
|
919
|
+
success: execResult.success,
|
|
920
|
+
exitCode: execResult.exitCode,
|
|
921
|
+
stdout: execResult.stdout,
|
|
922
|
+
stderr: execResult.stderr,
|
|
923
|
+
duration: execResult.duration,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
return {
|
|
928
|
+
alias,
|
|
929
|
+
success: false,
|
|
930
|
+
error: err.message,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
const results = await Promise.all(execPromises);
|
|
935
|
+
result = {
|
|
936
|
+
success: results.every(r => r.success),
|
|
937
|
+
total: aliases.length,
|
|
938
|
+
results,
|
|
939
|
+
};
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
768
942
|
default:
|
|
769
943
|
throw new Error(`Unknown tool: ${name}`);
|
|
770
944
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - 会话持久化
|
|
9
9
|
*/
|
|
10
10
|
import { Client, ClientChannel, SFTPWrapper } from 'ssh2';
|
|
11
|
-
import {
|
|
11
|
+
import { ExecOptions, ExecResult, PersistedSession, PortForwardInfo, PtyOptions, PtySessionInfo, SSHConnectionConfig, SSHSessionInfo } from './types.js';
|
|
12
12
|
interface SSHSession {
|
|
13
13
|
client: Client;
|
|
14
14
|
config: SSHConnectionConfig;
|
|
@@ -37,23 +37,10 @@ export declare class SessionManager {
|
|
|
37
37
|
private maxReconnectAttempts;
|
|
38
38
|
private defaultPtyBufferSize;
|
|
39
39
|
constructor(persistPath?: string);
|
|
40
|
-
private ensurePersistDir;
|
|
41
|
-
/**
|
|
42
|
-
* 生成连接别名
|
|
43
|
-
*/
|
|
44
|
-
private generateAlias;
|
|
45
40
|
/**
|
|
46
41
|
* 建立 SSH 连接
|
|
47
42
|
*/
|
|
48
43
|
connect(config: SSHConnectionConfig): Promise<string>;
|
|
49
|
-
/**
|
|
50
|
-
* 通过跳板机转发连接
|
|
51
|
-
*/
|
|
52
|
-
private forwardConnection;
|
|
53
|
-
/**
|
|
54
|
-
* 检查连接是否存活
|
|
55
|
-
*/
|
|
56
|
-
private isAlive;
|
|
57
44
|
/**
|
|
58
45
|
* 重新连接
|
|
59
46
|
*/
|
|
@@ -74,26 +61,20 @@ export declare class SessionManager {
|
|
|
74
61
|
* 列出所有会话
|
|
75
62
|
*/
|
|
76
63
|
listSessions(): SSHSessionInfo[];
|
|
77
|
-
/**
|
|
78
|
-
* 转义 shell 参数(使用单引号方式)
|
|
79
|
-
*/
|
|
80
|
-
private escapeShellArg;
|
|
81
64
|
/**
|
|
82
65
|
* 执行命令
|
|
83
66
|
*/
|
|
84
67
|
exec(alias: string, command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
85
|
-
/**
|
|
86
|
-
* 校验用户名(只允许字母、数字、下划线、连字符)
|
|
87
|
-
*/
|
|
88
|
-
private isValidUsername;
|
|
89
|
-
/**
|
|
90
|
-
* 校验环境变量名(只允许字母、数字、下划线,不能以数字开头)
|
|
91
|
-
*/
|
|
92
|
-
private isValidEnvKey;
|
|
93
68
|
/**
|
|
94
69
|
* 以其他用户身份执行命令
|
|
70
|
+
* @param loadProfile 是否加载用户的 shell 配置(默认 true)。
|
|
71
|
+
* su -c 创建非交互式 shell,不会自动执行 rc 文件,
|
|
72
|
+
* 但大多数用户的环境变量设置在 rc 文件中,因此默认加载。
|
|
73
|
+
* 支持 bash(.bashrc)、zsh(.zshrc) 及其他 shell(.profile)。
|
|
95
74
|
*/
|
|
96
|
-
execAsUser(alias: string, command: string, targetUser: string, options?: ExecOptions
|
|
75
|
+
execAsUser(alias: string, command: string, targetUser: string, options?: ExecOptions & {
|
|
76
|
+
loadProfile?: boolean;
|
|
77
|
+
}): Promise<ExecResult>;
|
|
97
78
|
/**
|
|
98
79
|
* 使用 sudo 执行命令
|
|
99
80
|
*/
|
|
@@ -102,18 +83,10 @@ export declare class SessionManager {
|
|
|
102
83
|
* 获取 SFTP 客户端
|
|
103
84
|
*/
|
|
104
85
|
getSftp(alias: string): Promise<SFTPWrapper>;
|
|
105
|
-
/**
|
|
106
|
-
* 持久化会话信息
|
|
107
|
-
*/
|
|
108
|
-
private persistSessions;
|
|
109
86
|
/**
|
|
110
87
|
* 加载持久化的会话信息(仅用于显示,不自动重连)
|
|
111
88
|
*/
|
|
112
89
|
loadPersistedSessions(): PersistedSession[];
|
|
113
|
-
/**
|
|
114
|
-
* 生成 PTY 会话 ID
|
|
115
|
-
*/
|
|
116
|
-
private generatePtyId;
|
|
117
90
|
/**
|
|
118
91
|
* 启动持久化 PTY 会话
|
|
119
92
|
*/
|
|
@@ -122,10 +95,6 @@ export declare class SessionManager {
|
|
|
122
95
|
* 向 PTY 写入数据
|
|
123
96
|
*/
|
|
124
97
|
ptyWrite(ptyId: string, data: string): boolean;
|
|
125
|
-
/**
|
|
126
|
-
* 从终端仿真器获取当前屏幕内容
|
|
127
|
-
*/
|
|
128
|
-
private getScreenContent;
|
|
129
98
|
/**
|
|
130
99
|
* 读取 PTY 输出
|
|
131
100
|
* @param mode 'screen' 返回当前屏幕内容,'raw' 返回原始 ANSI 流
|
|
@@ -155,10 +124,6 @@ export declare class SessionManager {
|
|
|
155
124
|
* 关闭所有 PTY 会话
|
|
156
125
|
*/
|
|
157
126
|
ptyCloseAll(): number;
|
|
158
|
-
/**
|
|
159
|
-
* 生成端口转发 ID
|
|
160
|
-
*/
|
|
161
|
-
private generateForwardId;
|
|
162
127
|
/**
|
|
163
128
|
* 创建本地端口转发
|
|
164
129
|
* 本地监听 localHost:localPort,转发到远程 remoteHost:remotePort
|
|
@@ -169,6 +134,60 @@ export declare class SessionManager {
|
|
|
169
134
|
* 远程监听 remoteHost:remotePort,转发到本地 localHost:localPort
|
|
170
135
|
*/
|
|
171
136
|
forwardRemote(alias: string, remotePort: number, localHost: string, localPort: number, remoteHost?: string): Promise<string>;
|
|
137
|
+
/**
|
|
138
|
+
* 关闭端口转发
|
|
139
|
+
*/
|
|
140
|
+
forwardClose(forwardId: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* 列出所有端口转发
|
|
143
|
+
*/
|
|
144
|
+
forwardList(): PortForwardInfo[];
|
|
145
|
+
private ensurePersistDir;
|
|
146
|
+
/**
|
|
147
|
+
* 生成连接别名
|
|
148
|
+
*/
|
|
149
|
+
private generateAlias;
|
|
150
|
+
/**
|
|
151
|
+
* 通过跳板机转发连接
|
|
152
|
+
*/
|
|
153
|
+
private forwardConnection;
|
|
154
|
+
/**
|
|
155
|
+
* 检查连接是否存活
|
|
156
|
+
*/
|
|
157
|
+
private isAlive;
|
|
158
|
+
/**
|
|
159
|
+
* 转义 shell 参数(使用单引号方式)
|
|
160
|
+
*/
|
|
161
|
+
private escapeShellArg;
|
|
162
|
+
/**
|
|
163
|
+
* 校验用户名(只允许字母、数字、下划线、连字符)
|
|
164
|
+
*/
|
|
165
|
+
private isValidUsername;
|
|
166
|
+
/**
|
|
167
|
+
* 校验环境变量名(只允许字母、数字、下划线,不能以数字开头)
|
|
168
|
+
*/
|
|
169
|
+
private isValidEnvKey;
|
|
170
|
+
/**
|
|
171
|
+
* 根据用户 shell 类型生成加载配置文件的命令
|
|
172
|
+
* bash → .bashrc, zsh → .zshrc, 其他 → .profile
|
|
173
|
+
*/
|
|
174
|
+
private getLoadProfileCommand;
|
|
175
|
+
/**
|
|
176
|
+
* 持久化会话信息
|
|
177
|
+
*/
|
|
178
|
+
private persistSessions;
|
|
179
|
+
/**
|
|
180
|
+
* 生成 PTY 会话 ID
|
|
181
|
+
*/
|
|
182
|
+
private generatePtyId;
|
|
183
|
+
/**
|
|
184
|
+
* 从终端仿真器获取当前屏幕内容
|
|
185
|
+
*/
|
|
186
|
+
private getScreenContent;
|
|
187
|
+
/**
|
|
188
|
+
* 生成端口转发 ID
|
|
189
|
+
*/
|
|
190
|
+
private generateForwardId;
|
|
172
191
|
/**
|
|
173
192
|
* 确保 SSH session 有共享的 tcp connection dispatcher
|
|
174
193
|
* 所有 remote forward 共用一个 dispatcher,根据 destIP/destPort 路由
|
|
@@ -180,14 +199,6 @@ export declare class SessionManager {
|
|
|
180
199
|
* @param alias - session 的 map key
|
|
181
200
|
*/
|
|
182
201
|
private removeTcpDispatcherIfEmpty;
|
|
183
|
-
/**
|
|
184
|
-
* 关闭端口转发
|
|
185
|
-
*/
|
|
186
|
-
forwardClose(forwardId: string): boolean;
|
|
187
|
-
/**
|
|
188
|
-
* 列出所有端口转发
|
|
189
|
-
*/
|
|
190
|
-
forwardList(): PortForwardInfo[];
|
|
191
202
|
}
|
|
192
203
|
export declare const sessionManager: SessionManager;
|
|
193
204
|
export {};
|