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