mcp-ssh-pty 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -1,55 +1,128 @@
1
1
  import { z } from "zod";
2
+ import { sanitize } from "./sanitizer.js";
3
+ /**
4
+ * 过滤 ShellResult 中的敏感信息
5
+ */
6
+ function sanitizeResult(result) {
7
+ return {
8
+ ...result,
9
+ output: sanitize(result.output),
10
+ message: sanitize(result.message),
11
+ };
12
+ }
2
13
  export function registerTools(server, sshManager, configManager) {
3
- // 工具1: ssh - 连接管理 + 快捷命令
4
14
  server.registerTool("ssh", {
5
- description: `SSH 远程服务器连接管理和命令执行。使用持久 PTY Shell,支持交互式操作。
6
-
7
- ## 操作类型 (action)
8
- - list: 列出所有可用服务器
9
- - connect: 连接服务器(需提供 server 参数)
10
- - disconnect: 断开当前连接
11
- - status: 查看连接状态和 shell 缓冲区行数
12
-
13
- ## 快捷命令 (command)
14
- 直接提供 command 参数可执行命令,无需指定 action。
15
-
16
- ## 智能输出检测
17
- 命令执行后自动检测完成状态:
18
- - 快速命令(<2秒):检测到提示符后返回完整输出
19
- - 慢速命令(2-5秒):返回完整输出,标记 slow=true
20
- - 超时命令(>5秒):自动截断到最近 200 行,标记 truncated=true
21
- - 持续输出(如 tail -f):输出稳定后返回,标记 waiting=true
22
-
23
- ## 返回字段说明
24
- - output: 命令输出内容
25
- - totalLines: 总行数
26
- - complete: 是否检测到命令完成(出现提示符)
27
- - truncated: 是否被截断(超时时)
28
- - slow: 是否耗时较长
29
- - waiting: 命令可能仍在运行或等待输入
30
- - message: 状态描述
31
-
32
- ## 使用示例
33
- 1. 列出服务器: ssh({ action: "list" })
34
- 2. 连接: ssh({ action: "connect", server: "my-server" })
35
- 3. 执行命令: ssh({ command: "ls -la" })
36
- 4. 查看状态: ssh({ action: "status" })
37
- 5. 断开: ssh({ action: "disconnect" })
38
-
39
- ## 注意事项
40
- - 如果 truncated=true,使用 ssh_shell read 获取完整输出
41
- - 如果 waiting=true,命令可能需要输入或是持续运行的程序,使用 ssh_shell 继续交互`,
15
+ description: `SSH 远程服务器连接管理和 PTY Shell 交互。
16
+
17
+ ## 连接管理 (action)
18
+ - list: 列出所有可用服务器
19
+ - connect: 连接服务器(需提供 server 参数)
20
+ - disconnect: 断开当前连接
21
+ - status: 查看连接状态和 shell 缓冲区行数
22
+
23
+ ## 命令执行 (command)
24
+ 直接提供 command 参数执行命令,支持交互式程序。
25
+
26
+ ## Shell 控制
27
+ - read: 设为 true 读取缓冲区(配合 lines/offset/clear)
28
+ - signal: 发送信号 SIGINT(Ctrl+C)/SIGTSTP(Ctrl+Z)/SIGQUIT
29
+
30
+ ## 智能输出检测
31
+ - 快速命令(<2秒):检测到提示符后返回
32
+ - 慢速命令(2-5秒):标记 slow=true
33
+ - 超时命令(>5秒):截断到最近 200 行,标记 truncated=true
34
+ - 持续输出:输出稳定后返回,标记 waiting=true
35
+
36
+ ## 返回字段
37
+ - output: 命令输出
38
+ - totalLines: 总行数
39
+ - complete: 是否完成(出现提示符)
40
+ - truncated: 是否被截断
41
+ - slow: 是否耗时较长
42
+ - waiting: 可能在等待输入或仍在运行
43
+
44
+ ## 使用示例
45
+
46
+ ### 基本操作
47
+ ssh({ action: "list" })
48
+ ssh({ action: "connect", server: "my-server" })
49
+ ssh({ command: "ls -la" })
50
+ ssh({ action: "status" })
51
+ ssh({ action: "disconnect" })
52
+
53
+ ### 读取缓冲区
54
+ ssh({ read: true }) # 读取最近 20 行
55
+ ssh({ read: true, lines: -1 }) # 读取全部
56
+ ssh({ read: true, lines: 100 }) # 读取 100 行
57
+
58
+ ### 交互式程序
59
+ ssh({ command: "mysql -u root -p" }) # 启动 mysql
60
+ ssh({ command: "password123" }) # 输入密码
61
+ ssh({ command: "SHOW DATABASES;" }) # 执行 SQL
62
+
63
+ ### 信号控制
64
+ ssh({ command: "tail -f /var/log/syslog" })
65
+ ssh({ read: true }) # 查看输出
66
+ ssh({ signal: "SIGINT" }) # Ctrl+C 停止`,
42
67
  inputSchema: {
43
68
  action: z
44
69
  .enum(["list", "connect", "disconnect", "status"])
45
70
  .optional()
46
- .describe("操作类型:list/connect/disconnect/status"),
71
+ .describe("连接管理操作"),
47
72
  server: z.string().optional().describe("服务器名称(connect 时必填)"),
48
- command: z.string().optional().describe("要执行的命令(直接执行,无需 action)"),
73
+ command: z.string().optional().describe("要执行的命令"),
74
+ read: z.boolean().optional().describe("读取缓冲区"),
75
+ lines: z.number().optional().describe("读取行数,默认 20,-1 返回全部"),
76
+ offset: z.number().optional().describe("读取起始偏移,默认 0"),
77
+ clear: z.boolean().optional().describe("读取后清空缓冲区"),
78
+ signal: z
79
+ .enum(["SIGINT", "SIGTSTP", "SIGQUIT"])
80
+ .optional()
81
+ .describe("发送信号:SIGINT(Ctrl+C)/SIGTSTP(Ctrl+Z)/SIGQUIT"),
49
82
  },
50
- }, async ({ action, server: serverName, command }) => {
83
+ }, async ({ action, server: serverName, command, read, lines, offset, clear, signal }) => {
51
84
  try {
52
- // 快捷命令模式
85
+ // 1. 发送信号
86
+ if (signal) {
87
+ const status = sshManager.getStatus();
88
+ if (!status.connected) {
89
+ return {
90
+ content: [{ type: "text", text: "未连接服务器" }],
91
+ isError: true,
92
+ };
93
+ }
94
+ const shellManager = sshManager.getShellManager();
95
+ const success = shellManager.sendSignal(signal);
96
+ return {
97
+ content: [{
98
+ type: "text",
99
+ text: success ? `已发送 ${signal}` : `发送 ${signal} 失败`,
100
+ }],
101
+ isError: !success,
102
+ };
103
+ }
104
+ // 2. 读取缓冲区
105
+ if (read) {
106
+ const status = sshManager.getStatus();
107
+ if (!status.connected) {
108
+ return {
109
+ content: [{ type: "text", text: "未连接服务器" }],
110
+ isError: true,
111
+ };
112
+ }
113
+ const shellManager = sshManager.getShellManager();
114
+ const result = sanitizeResult(shellManager.read(lines, offset, clear));
115
+ return {
116
+ content: [{
117
+ type: "text",
118
+ text: JSON.stringify({
119
+ server: status.serverName,
120
+ ...result,
121
+ }, null, 2),
122
+ }],
123
+ };
124
+ }
125
+ // 3. 执行命令
53
126
  if (command) {
54
127
  const status = sshManager.getStatus();
55
128
  if (!status.connected) {
@@ -59,7 +132,7 @@ export function registerTools(server, sshManager, configManager) {
59
132
  };
60
133
  }
61
134
  const shellManager = sshManager.getShellManager();
62
- const result = await shellManager.send(command);
135
+ const result = sanitizeResult(await shellManager.send(command));
63
136
  return {
64
137
  content: [{
65
138
  type: "text",
@@ -72,13 +145,12 @@ export function registerTools(server, sshManager, configManager) {
72
145
  isError: !result.complete && !result.waiting,
73
146
  };
74
147
  }
75
- // 默认 action 为 status
148
+ // 4. 连接管理操作
76
149
  const effectiveAction = action || "status";
77
150
  switch (effectiveAction) {
78
151
  case "list": {
79
152
  const servers = configManager.listServers();
80
153
  const status = sshManager.getStatus();
81
- // 只返回服务器名称,不暴露 IP/端口/用户名
82
154
  const list = servers.map((s) => ({
83
155
  name: s.name,
84
156
  connected: status.serverName === s.name,
@@ -106,7 +178,6 @@ export function registerTools(server, sshManager, configManager) {
106
178
  };
107
179
  }
108
180
  await sshManager.connect(serverConfig);
109
- // 不暴露 IP/端口/用户名
110
181
  return {
111
182
  content: [{
112
183
  type: "text",
@@ -131,7 +202,6 @@ export function registerTools(server, sshManager, configManager) {
131
202
  const status = sshManager.getStatus();
132
203
  const shellManager = sshManager.getShellManager();
133
204
  if (status.connected) {
134
- // 不暴露 host/username
135
205
  return {
136
206
  content: [{
137
207
  type: "text",
@@ -160,169 +230,4 @@ export function registerTools(server, sshManager, configManager) {
160
230
  };
161
231
  }
162
232
  });
163
- // 工具2: ssh_shell - PTY Shell 会话控制
164
- server.registerTool("ssh_shell", {
165
- description: `PTY Shell 高级会话控制。用于交互式程序、长输出处理、信号发送等场景。
166
-
167
- ## 操作类型 (action)
168
-
169
- ### send - 发送命令或输入
170
- 发送命令到 shell,智能等待完成。适用于:
171
- - 需要多次交互的程序(如 mysql, python 交互模式)
172
- - 需要发送特定输入(如回答 y/n 提示)
173
- 参数:input(必填)- 要发送的内容
174
-
175
- ### read - 读取缓冲区
176
- 获取 shell 输出缓冲区内容。适用于:
177
- - 命令被截断后获取完整输出
178
- - 查看持续运行命令的最新输出
179
- - 分页读取大量输出
180
- 参数:
181
- - lines: 返回行数(默认全部)
182
- - offset: 起始偏移(默认 0)
183
- - clear: 读取后清空缓冲区(默认 false)
184
-
185
- ### signal - 发送信号
186
- 向当前进程发送控制信号:
187
- - SIGINT: Ctrl+C,中断当前命令
188
- - SIGTSTP: Ctrl+Z,暂停进程
189
- - SIGQUIT: Ctrl+\,退出进程
190
- 参数:signal(必填)
191
-
192
- ### close - 关闭 shell
193
- 关闭当前 shell 会话,但保持 SSH 连接。
194
-
195
- ## 使用场景示例
196
-
197
- ### 场景1:查看大文件
198
- \`\`\`
199
- ssh({ command: "cat /var/log/syslog" })
200
- # 返回 truncated=true,只有最近 200 行
201
- ssh_shell({ action: "read", lines: 500, offset: 0 })
202
- # 获取前 500 行
203
- \`\`\`
204
-
205
- ### 场景2:交互式程序
206
- \`\`\`
207
- ssh({ command: "mysql -u root -p" })
208
- # 返回 waiting=true,等待密码
209
- ssh_shell({ action: "send", input: "password123" })
210
- # 发送密码
211
- ssh_shell({ action: "send", input: "SHOW DATABASES;" })
212
- # 执行 SQL
213
- \`\`\`
214
-
215
- ### 场景3:监控日志
216
- \`\`\`
217
- ssh({ command: "tail -f /var/log/nginx/access.log" })
218
- # 返回 waiting=true
219
- # ... 等待一段时间 ...
220
- ssh_shell({ action: "read" })
221
- # 查看新日志
222
- ssh_shell({ action: "signal", signal: "SIGINT" })
223
- # 停止 tail
224
- \`\`\`
225
-
226
- ### 场景4:vim 等全屏程序
227
- \`\`\`
228
- ssh_shell({ action: "send", input: "vim config.txt" })
229
- ssh_shell({ action: "send", input: "i" }) # 进入插入模式
230
- ssh_shell({ action: "send", input: "new content" })
231
- ssh_shell({ action: "send", input: "\\x1b" }) # ESC 键
232
- ssh_shell({ action: "send", input: ":wq" }) # 保存退出
233
- \`\`\``,
234
- inputSchema: {
235
- action: z
236
- .enum(["send", "read", "signal", "close"])
237
- .describe("操作类型:send/read/signal/close"),
238
- input: z.string().optional().describe("send 时的命令或输入内容"),
239
- signal: z
240
- .enum(["SIGINT", "SIGTSTP", "SIGQUIT"])
241
- .optional()
242
- .describe("signal 时的信号类型:SIGINT(Ctrl+C)/SIGTSTP(Ctrl+Z)/SIGQUIT"),
243
- lines: z.number().optional().describe("read 时返回的行数,默认全部"),
244
- offset: z.number().optional().describe("read 时的起始行偏移,默认 0"),
245
- clear: z.boolean().optional().describe("read 后是否清空缓冲区,默认 false"),
246
- },
247
- }, async ({ action, input, signal, lines, offset, clear }) => {
248
- try {
249
- const status = sshManager.getStatus();
250
- if (!status.connected) {
251
- return {
252
- content: [{ type: "text", text: "未连接服务器,请先使用 ssh 工具连接" }],
253
- isError: true,
254
- };
255
- }
256
- const shellManager = sshManager.getShellManager();
257
- if (!shellManager.isOpen()) {
258
- return {
259
- content: [{ type: "text", text: "Shell 未打开" }],
260
- isError: true,
261
- };
262
- }
263
- switch (action) {
264
- case "send": {
265
- if (!input) {
266
- return {
267
- content: [{ type: "text", text: "缺少 input 参数" }],
268
- isError: true,
269
- };
270
- }
271
- const result = await shellManager.send(input);
272
- return {
273
- content: [{
274
- type: "text",
275
- text: JSON.stringify({
276
- server: status.serverName,
277
- input,
278
- ...result,
279
- }, null, 2),
280
- }],
281
- isError: !result.complete && !result.waiting,
282
- };
283
- }
284
- case "read": {
285
- const result = shellManager.read(lines, offset, clear);
286
- return {
287
- content: [{
288
- type: "text",
289
- text: JSON.stringify({
290
- server: status.serverName,
291
- ...result,
292
- }, null, 2),
293
- }],
294
- };
295
- }
296
- case "signal": {
297
- if (!signal) {
298
- return {
299
- content: [{ type: "text", text: "缺少 signal 参数" }],
300
- isError: true,
301
- };
302
- }
303
- const success = shellManager.sendSignal(signal);
304
- return {
305
- content: [{
306
- type: "text",
307
- text: success ? `已发送 ${signal}` : `发送 ${signal} 失败`,
308
- }],
309
- isError: !success,
310
- };
311
- }
312
- case "close": {
313
- shellManager.close();
314
- return {
315
- content: [{ type: "text", text: "Shell 已关闭" }],
316
- };
317
- }
318
- }
319
- }
320
- catch (error) {
321
- const message = error instanceof Error ? error.message : String(error);
322
- return {
323
- content: [{ type: "text", text: `错误: ${message}` }],
324
- isError: true,
325
- };
326
- }
327
- });
328
233
  }
package/package.json CHANGED
@@ -1,46 +1,47 @@
1
- {
2
- "name": "mcp-ssh-pty",
3
- "version": "1.0.0",
4
- "description": "MCP Server for SSH remote command execution with PTY shell support",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "mcp-ssh-pty": "./dist/index.js"
9
- },
10
- "scripts": {
11
- "build": "tsc",
12
- "start": "node dist/index.js",
13
- "dev": "tsc --watch",
14
- "prepublishOnly": "npm run build"
15
- },
16
- "keywords": [
17
- "mcp",
18
- "ssh",
19
- "claude",
20
- "anthropic",
21
- "ai",
22
- "remote",
23
- "shell",
24
- "pty"
25
- ],
26
- "author": "me",
27
- "license": "MIT",
28
-
29
- "files": [
30
- "dist",
31
- "ssh-servers.example.json"
32
- ],
33
- "dependencies": {
34
- "@modelcontextprotocol/sdk": "^1.0.0",
35
- "ssh2": "^1.16.0",
36
- "zod": "^3.24.0"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^22.0.0",
40
- "@types/ssh2": "^1.15.0",
41
- "typescript": "^5.7.0"
42
- },
43
- "engines": {
44
- "node": ">=18.0.0"
45
- }
46
- }
1
+ {
2
+ "name": "mcp-ssh-pty",
3
+ "version": "1.1.0",
4
+ "description": "MCP Server for SSH remote command execution with PTY shell support",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-ssh-pty": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsc --watch",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "ssh",
19
+ "claude",
20
+ "anthropic",
21
+ "ai",
22
+ "remote",
23
+ "shell",
24
+ "pty"
25
+ ],
26
+ "author": "me",
27
+ "license": "MIT",
28
+ "files": [
29
+ "dist",
30
+ "ssh-servers.example.json"
31
+ ],
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^8.1.0",
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "commander": "^14.0.2",
36
+ "ssh2": "^1.16.0",
37
+ "zod": "^3.24.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "@types/ssh2": "^1.15.0",
42
+ "typescript": "^5.7.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ }
47
+ }
@@ -1,18 +1,18 @@
1
- {
2
- "servers": [
3
- {
4
- "name": "my-server",
5
- "host": "192.168.1.100",
6
- "port": 22,
7
- "username": "root",
8
- "privateKeyPath": "~/.ssh/id_rsa"
9
- },
10
- {
11
- "name": "dev-server",
12
- "host": "10.0.0.50",
13
- "port": 22,
14
- "username": "ubuntu",
15
- "password": "your-password"
16
- }
17
- ]
18
- }
1
+ {
2
+ "servers": [
3
+ {
4
+ "name": "my-server",
5
+ "host": "192.168.1.100",
6
+ "port": 22,
7
+ "username": "root",
8
+ "privateKeyPath": "~/.ssh/id_rsa"
9
+ },
10
+ {
11
+ "name": "dev-server",
12
+ "host": "10.0.0.50",
13
+ "port": 22,
14
+ "username": "ubuntu",
15
+ "password": "your-password"
16
+ }
17
+ ]
18
+ }