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