@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/src/index.ts CHANGED
@@ -14,38 +14,33 @@
14
14
  * - Jump host support
15
15
  */
16
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
- 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
- name: 'ssh-mcp-pro',
34
- version: '1.0.0',
35
- },
36
- {
37
- capabilities: {
38
- tools: {},
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
- name: 'ssh_connect',
48
- description: `建立 SSH 连接并保持会话。支持密码、密钥认证,支持跳板机。
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
- inputSchema: {
58
- type: 'object',
59
- properties: {
60
- configHost: { type: 'string', description: '使用 ~/.ssh/config 中的 Host 配置(推荐)' },
61
- configPath: { type: 'string', description: 'SSH 配置文件路径(默认 ~/.ssh/config)' },
62
- host: { type: 'string', description: '服务器地址(使用 configHost 时可省略)' },
63
- user: { type: 'string', description: '用户名(使用 configHost 时可省略)' },
64
- password: { type: 'string', description: '密码' },
65
- keyPath: { type: 'string', description: 'SSH 私钥路径' },
66
- port: { type: 'number', description: 'SSH 端口,默认 22' },
67
- alias: { type: 'string', description: '连接别名(可选,默认使用 configHost 或 host)' },
68
- env: {
69
- type: 'object',
70
- description: '环境变量',
71
- additionalProperties: { type: 'string' },
72
- },
73
- keepaliveInterval: { type: 'number', description: '心跳间隔(毫秒),默认 30000' },
74
- jumpHost: {
75
- type: 'object',
76
- description: '跳板机配置',
77
- properties: {
78
- host: { type: 'string', description: '跳板机地址' },
79
- user: { type: 'string', description: '跳板机用户名' },
80
- password: { type: 'string', description: '跳板机密码' },
81
- keyPath: { type: 'string', description: '跳板机私钥路径' },
82
- port: { type: 'number', description: '跳板机端口,默认 22' },
83
- },
84
- required: ['host', 'user'],
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
- name: 'ssh_disconnect',
91
- description: '断开 SSH 连接',
92
- inputSchema: {
93
- type: 'object',
94
- properties: {
95
- alias: { type: 'string', description: '连接别名' },
96
- },
97
- required: ['alias'],
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
- name: 'ssh_list_sessions',
102
- description: '列出所有活跃的 SSH 会话',
103
- inputSchema: {
104
- type: 'object',
105
- properties: {},
95
+ {
96
+ name: 'ssh_list_sessions',
97
+ description: '列出所有活跃的 SSH 会话',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {},
101
+ },
106
102
  },
107
- },
108
- {
109
- name: 'ssh_reconnect',
110
- description: '重新连接已断开的会话',
111
- inputSchema: {
112
- type: 'object',
113
- properties: {
114
- alias: { type: 'string', description: '连接别名' },
115
- },
116
- required: ['alias'],
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
- name: 'ssh_exec',
123
- description: `在远程服务器执行命令。
115
+ // ========== 命令执行 ==========
116
+ {
117
+ name: 'ssh_exec',
118
+ description: `在远程服务器执行命令。
124
119
 
125
120
  返回: stdout, stderr, exitCode, duration`,
126
- inputSchema: {
127
- type: 'object',
128
- properties: {
129
- alias: { type: 'string', description: '连接别名' },
130
- command: { type: 'string', description: '要执行的命令' },
131
- timeout: { type: 'number', description: '超时(毫秒),默认 30000' },
132
- cwd: { type: 'string', description: '工作目录(可选)' },
133
- env: {
134
- type: 'object',
135
- description: '额外环境变量',
136
- additionalProperties: { type: 'string' },
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
- name: 'ssh_exec_as_user',
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
- inputSchema: {
151
- type: 'object',
152
- properties: {
153
- alias: { type: 'string', description: '连接别名' },
154
- command: { type: 'string', description: '要执行的命令' },
155
- targetUser: { type: 'string', description: '目标用户名' },
156
- timeout: { type: 'number', description: '超时(毫秒)' },
157
- },
158
- required: ['alias', 'command', 'targetUser'],
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
- name: 'ssh_exec_sudo',
163
- description: '使用 sudo 执行命令',
164
- inputSchema: {
165
- type: 'object',
166
- properties: {
167
- alias: { type: 'string', description: '连接别名' },
168
- command: { type: 'string', description: '要执行的命令' },
169
- sudoPassword: { type: 'string', description: 'sudo 密码(如果需要)' },
170
- timeout: { type: 'number', description: '超时(毫秒)' },
171
- },
172
- required: ['alias', 'command'],
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
- name: 'ssh_exec_batch',
177
- description: '批量执行多条命令',
178
- inputSchema: {
179
- type: 'object',
180
- properties: {
181
- alias: { type: 'string', description: '连接别名' },
182
- commands: {
183
- type: 'array',
184
- items: { type: 'string' },
185
- description: '命令列表',
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
- name: 'ssh_quick_exec',
195
- description: '一次性执行命令(自动连接、执行、断开)。适用于单次命令,不需要保持连接。',
196
- inputSchema: {
197
- type: 'object',
198
- properties: {
199
- host: { type: 'string', description: '服务器地址' },
200
- user: { type: 'string', description: '用户名' },
201
- command: { type: 'string', description: '要执行的命令' },
202
- password: { type: 'string', description: '密码' },
203
- keyPath: { type: 'string', description: '密钥路径' },
204
- port: { type: 'number', description: '端口', default: 22 },
205
- timeout: { type: 'number', description: '超时(毫秒)' },
206
- },
207
- required: ['host', 'user', 'command'],
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
- name: 'ssh_upload',
214
- description: '上传本地文件到远程服务器',
215
- inputSchema: {
216
- type: 'object',
217
- properties: {
218
- alias: { type: 'string', description: '连接别名' },
219
- localPath: { type: 'string', description: '本地文件路径' },
220
- remotePath: { type: 'string', description: '远程目标路径' },
221
- },
222
- required: ['alias', 'localPath', 'remotePath'],
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
- name: 'ssh_download',
227
- description: '从远程服务器下载文件',
228
- inputSchema: {
229
- type: 'object',
230
- properties: {
231
- alias: { type: 'string', description: '连接别名' },
232
- remotePath: { type: 'string', description: '远程文件路径' },
233
- localPath: { type: 'string', description: '本地保存路径' },
234
- },
235
- required: ['alias', 'remotePath', 'localPath'],
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
- name: 'ssh_read_file',
240
- description: '读取远程文件内容',
241
- inputSchema: {
242
- type: 'object',
243
- properties: {
244
- alias: { type: 'string', description: '连接别名' },
245
- remotePath: { type: 'string', description: '远程文件路径' },
246
- maxBytes: { type: 'number', description: '最大读取字节数,默认 1MB' },
247
- },
248
- required: ['alias', 'remotePath'],
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
- name: 'ssh_write_file',
253
- description: '写入内容到远程文件',
254
- inputSchema: {
255
- type: 'object',
256
- properties: {
257
- alias: { type: 'string', description: '连接别名' },
258
- remotePath: { type: 'string', description: '远程文件路径' },
259
- content: { type: 'string', description: '要写入的内容' },
260
- append: { type: 'boolean', description: '是否追加模式,默认覆盖' },
261
- },
262
- required: ['alias', 'remotePath', 'content'],
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
- name: 'ssh_list_dir',
267
- description: '列出远程目录内容',
268
- inputSchema: {
269
- type: 'object',
270
- properties: {
271
- alias: { type: 'string', description: '连接别名' },
272
- remotePath: { type: 'string', description: '远程目录路径' },
273
- showHidden: { type: 'boolean', description: '是否显示隐藏文件' },
274
- },
275
- required: ['alias', 'remotePath'],
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
- name: 'ssh_file_info',
280
- description: '获取远程文件信息(大小、权限、修改时间等)',
281
- inputSchema: {
282
- type: 'object',
283
- properties: {
284
- alias: { type: 'string', description: '连接别名' },
285
- remotePath: { type: 'string', description: '远程路径' },
286
- },
287
- required: ['alias', 'remotePath'],
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
- name: 'ssh_mkdir',
292
- description: '创建远程目录',
293
- inputSchema: {
294
- type: 'object',
295
- properties: {
296
- alias: { type: 'string', description: '连接别名' },
297
- remotePath: { type: 'string', description: '远程目录路径' },
298
- recursive: { type: 'boolean', description: '是否递归创建,默认 false' },
299
- },
300
- required: ['alias', 'remotePath'],
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
- name: 'ssh_sync',
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
- inputSchema: {
320
- type: 'object',
321
- properties: {
322
- alias: { type: 'string', description: '连接别名' },
323
- localPath: { type: 'string', description: '本地路径' },
324
- remotePath: { type: 'string', description: '远程路径' },
325
- direction: {
326
- type: 'string',
327
- enum: ['upload', 'download'],
328
- description: '同步方向:upload(本地到远程)或 download(远程到本地)',
329
- },
330
- delete: { type: 'boolean', description: '删除目标端多余文件(类似 rsync --delete)' },
331
- dryRun: { type: 'boolean', description: '仅显示将执行的操作,不实际传输' },
332
- exclude: {
333
- type: 'array',
334
- items: { type: 'string' },
335
- description: '排除模式列表(支持 * 和 ? 通配符)',
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
- // ========== PTY 会话(持久化交互式终端) ==========
344
- {
345
- name: 'ssh_pty_start',
346
- description: `启动持久化 PTY 会话,支持 top、htop、tmux 等交互式命令。
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
- inputSchema: {
357
- type: 'object',
358
- properties: {
359
- alias: { type: 'string', description: '连接别名' },
360
- command: { type: 'string', description: '要执行的命令' },
361
- rows: { type: 'number', description: '终端行数,默认 24' },
362
- cols: { type: 'number', description: '终端列数,默认 80' },
363
- term: { type: 'string', description: '终端类型,默认 xterm-256color' },
364
- cwd: { type: 'string', description: '工作目录' },
365
- env: {
366
- type: 'object',
367
- description: '环境变量',
368
- additionalProperties: { type: 'string' },
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
- name: 'ssh_pty_write',
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
- inputSchema: {
391
- type: 'object',
392
- properties: {
393
- ptyId: { type: 'string', description: 'PTY 会话 ID' },
394
- data: { type: 'string', description: '要写入的数据' },
395
- },
396
- required: ['ptyId', 'data'],
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
- name: 'ssh_pty_read',
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
- inputSchema: {
411
- type: 'object',
412
- properties: {
413
- ptyId: { type: 'string', description: 'PTY 会话 ID' },
414
- mode: { type: 'string', enum: ['screen', 'raw'], description: '输出模式:screen(当前屏幕)或 raw(原始流),默认 screen' },
415
- clear: { type: 'boolean', description: '(仅 raw 模式) 读取后是否清空缓冲区,默认 true' },
416
- },
417
- required: ['ptyId'],
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
- name: 'ssh_pty_resize',
422
- description: '调整 PTY 窗口大小',
423
- inputSchema: {
424
- type: 'object',
425
- properties: {
426
- ptyId: { type: 'string', description: 'PTY 会话 ID' },
427
- rows: { type: 'number', description: '新的行数' },
428
- cols: { type: 'number', description: '新的列数' },
429
- },
430
- required: ['ptyId', 'rows', 'cols'],
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
- name: 'ssh_pty_close',
435
- description: '关闭 PTY 会话',
436
- inputSchema: {
437
- type: 'object',
438
- properties: {
439
- ptyId: { type: 'string', description: 'PTY 会话 ID' },
440
- },
441
- required: ['ptyId'],
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
- name: 'ssh_pty_list',
446
- description: '列出所有 PTY 会话',
447
- inputSchema: {
448
- type: 'object',
449
- properties: {},
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
- name: 'ssh_forward_local',
456
- description: `创建本地端口转发(类似 ssh -L)。
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
- inputSchema: {
464
- type: 'object',
465
- properties: {
466
- alias: { type: 'string', description: '连接别名' },
467
- localPort: { type: 'number', description: '本地监听端口' },
468
- remoteHost: { type: 'string', description: '远程目标主机' },
469
- remotePort: { type: 'number', description: '远程目标端口' },
470
- localHost: { type: 'string', description: '本地监听地址,默认 127.0.0.1' },
471
- },
472
- required: ['alias', 'localPort', 'remoteHost', 'remotePort'],
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
- name: 'ssh_forward_remote',
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
- inputSchema: {
485
- type: 'object',
486
- properties: {
487
- alias: { type: 'string', description: '连接别名' },
488
- remotePort: { type: 'number', description: '远程监听端口' },
489
- localHost: { type: 'string', description: '本地目标地址' },
490
- localPort: { type: 'number', description: '本地目标端口' },
491
- remoteHost: { type: 'string', description: '远程监听地址,默认 127.0.0.1' },
492
- },
493
- required: ['alias', 'remotePort', 'localHost', 'localPort'],
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
- name: 'ssh_forward_close',
498
- description: '关闭端口转发',
499
- inputSchema: {
500
- type: 'object',
501
- properties: {
502
- forwardId: { type: 'string', description: '端口转发 ID' },
503
- },
504
- required: ['forwardId'],
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
- name: 'ssh_forward_list',
509
- description: '列出所有端口转发',
510
- inputSchema: {
511
- type: 'object',
512
- properties: {},
510
+ {
511
+ name: 'ssh_forward_list',
512
+ description: '列出所有端口转发',
513
+ inputSchema: {
514
+ type: 'object',
515
+ properties: {},
516
+ },
513
517
  },
514
- },
515
518
 
516
- // ========== SSH Config ==========
517
- {
518
- name: 'ssh_config_list',
519
- description: `列出 ~/.ssh/config 中配置的所有 Host。
519
+ // ========== SSH Config ==========
520
+ {
521
+ name: 'ssh_config_list',
522
+ description: `列出 ~/.ssh/config 中配置的所有 Host。
520
523
 
521
524
  返回每个 Host 的配置信息(别名、地址、用户、端口、密钥路径等)。`,
522
- inputSchema: {
523
- type: 'object',
524
- properties: {
525
- configPath: { type: 'string', description: 'SSH 配置文件路径(默认 ~/.ssh/config)' },
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
- name: 'ssh_exec_parallel',
533
- description: `在多个已连接的会话上并行执行同一命令。
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
- inputSchema: {
540
- type: 'object',
541
- properties: {
542
- aliases: {
543
- type: 'array',
544
- items: { type: 'string' },
545
- description: '连接别名列表',
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
- tools,
558
- }));
560
+ tools,
561
+ }))
559
562
 
560
563
  // 处理工具调用
561
564
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
562
- const { name, arguments: args = {} } = request.params;
563
-
564
- try {
565
- let result: unknown;
566
-
567
- switch (name) {
568
- // ========== 连接管理 ==========
569
- case 'ssh_connect': {
570
- // 解析 configHost
571
- let host = args.host as string | undefined;
572
- let user = args.user as string | undefined;
573
- let port = args.port as number | undefined;
574
- let keyPath = args.keyPath as string | undefined;
575
- const configPath = args.configPath as string | undefined;
576
- let jumpHostResolved: { host: string; port: number; username: string; password?: string; privateKeyPath?: string } | undefined;
577
-
578
- if (args.configHost) {
579
- const allHosts = parseSSHConfig(configPath);
580
- const hostConfig = allHosts.find(h => h.host === args.configHost);
581
- if (!hostConfig) {
582
- throw new Error(`Host '${args.configHost}' not found in SSH config`);
583
- }
584
- // 显式参数优先于 config 值
585
- host = host || hostConfig.hostName || hostConfig.host;
586
- user = user || hostConfig.user;
587
- port = port || hostConfig.port;
588
- keyPath = keyPath || hostConfig.identityFile;
589
-
590
- // 解析 ProxyJump(支持 user@host:port 格式)
591
- if (hostConfig.proxyJump) {
592
- const parsed = parseProxyJump(hostConfig.proxyJump);
593
- if (parsed) {
594
- // 先尝试在 config 中查找对应的 Host
595
- const jumpHostConfig = allHosts.find(h => h.host === parsed.host);
596
- if (jumpHostConfig) {
597
- // 使用 config 中的配置,但 parsed 的 user/port 优先
598
- jumpHostResolved = {
599
- host: jumpHostConfig.hostName || jumpHostConfig.host,
600
- port: parsed.port || jumpHostConfig.port || 22,
601
- username: parsed.user || jumpHostConfig.user || 'root',
602
- privateKeyPath: jumpHostConfig.identityFile,
603
- };
604
- } else {
605
- // 直接使用 parsed 的值
606
- jumpHostResolved = {
607
- host: parsed.host,
608
- port: parsed.port || 22,
609
- username: parsed.user || 'root',
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
- if (!host || !user) {
617
- throw new Error('host and user are required (either directly or via configHost)');
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
- // 手动指定的 jumpHost 优先级高于 ProxyJump
621
- const jumpHostArg = args.jumpHost as { host: string; user: string; password?: string; keyPath?: string; port?: number } | undefined;
622
- const jumpHost = jumpHostArg ? {
623
- host: jumpHostArg.host,
624
- port: jumpHostArg.port || 22,
625
- username: jumpHostArg.user,
626
- password: jumpHostArg.password,
627
- privateKeyPath: jumpHostArg.keyPath,
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
- } catch (err: any) {
736
- results.push({
737
- index: i,
738
- command: commands[i],
739
- success: false,
740
- error: err.message,
741
- });
742
- if (stopOnError) break;
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
- result = {
747
- success: results.every((r) => r.success),
748
- total: commands.length,
749
- executed: results.length,
750
- results,
751
- };
752
- break;
753
- }
754
-
755
- case 'ssh_quick_exec': {
756
- const tempAlias = `_quick_${Date.now()}`;
757
- try {
758
- await sessionManager.connect({
759
- host: args.host as string,
760
- port: (args.port as number) || 22,
761
- username: args.user as string,
762
- password: args.password as string | undefined,
763
- privateKeyPath: args.keyPath as string | undefined,
764
- alias: tempAlias,
765
- });
766
- const execResult = await sessionManager.exec(
767
- tempAlias,
768
- args.command as string,
769
- { timeout: args.timeout as number | undefined }
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
- const transport = new StdioServerTransport();
1099
- await server.connect(transport);
1100
- console.error('SSH MCP Pro server started');
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
- console.error('Fatal error:', error);
1105
- process.exit(1);
1106
- });
1127
+ console.error('Fatal error:', error)
1128
+ process.exit(1)
1129
+ })