mcp-ssh-pty 1.0.0 → 1.2.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/README.md +161 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +538 -0
- package/dist/config.d.ts +13 -1
- package/dist/config.js +86 -8
- package/dist/index.js +30 -5
- package/dist/sanitizer.d.ts +48 -0
- package/dist/sanitizer.js +123 -0
- package/dist/shell-manager.d.ts +18 -1
- package/dist/shell-manager.js +117 -28
- package/dist/ssh-manager.d.ts +19 -0
- package/dist/ssh-manager.js +64 -2
- package/dist/tools.js +142 -217
- package/package.json +47 -46
- package/ssh-servers.example.json +18 -18
package/dist/ssh-manager.js
CHANGED
|
@@ -3,11 +3,20 @@ import { readFileSync } from "fs";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { ShellManager } from "./shell-manager.js";
|
|
6
|
+
import { getSanitizer } from "./sanitizer.js";
|
|
7
|
+
// 内置的本地服务器配置
|
|
8
|
+
export const LOCAL_SERVER = {
|
|
9
|
+
name: "local",
|
|
10
|
+
host: "localhost",
|
|
11
|
+
port: 0,
|
|
12
|
+
username: process.env.USER || process.env.USERNAME || "local",
|
|
13
|
+
};
|
|
6
14
|
export class SSHManager {
|
|
7
15
|
client = null;
|
|
8
16
|
currentServer = null;
|
|
9
17
|
isConnected = false;
|
|
10
18
|
shellManager;
|
|
19
|
+
isLocalConnection = false;
|
|
11
20
|
constructor() {
|
|
12
21
|
this.shellManager = new ShellManager();
|
|
13
22
|
}
|
|
@@ -17,15 +26,49 @@ export class SSHManager {
|
|
|
17
26
|
}
|
|
18
27
|
return path;
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* 检查是否是本地连接
|
|
31
|
+
*/
|
|
32
|
+
static isLocalServer(config) {
|
|
33
|
+
return config.name === "local" || config.host === "local";
|
|
34
|
+
}
|
|
20
35
|
async connect(config) {
|
|
21
36
|
if (this.isConnected) {
|
|
22
37
|
await this.disconnect();
|
|
23
38
|
}
|
|
39
|
+
// 检查是否是本地连接
|
|
40
|
+
if (SSHManager.isLocalServer(config)) {
|
|
41
|
+
return this.connectLocal();
|
|
42
|
+
}
|
|
43
|
+
return this.connectSSH(config);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 本地连接
|
|
47
|
+
*/
|
|
48
|
+
async connectLocal() {
|
|
49
|
+
try {
|
|
50
|
+
await this.shellManager.openLocal();
|
|
51
|
+
this.isConnected = true;
|
|
52
|
+
this.isLocalConnection = true;
|
|
53
|
+
this.currentServer = LOCAL_SERVER;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.cleanup();
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* SSH 远程连接
|
|
62
|
+
*/
|
|
63
|
+
async connectSSH(config) {
|
|
24
64
|
return new Promise((resolve, reject) => {
|
|
25
65
|
this.client = new Client();
|
|
26
66
|
this.client.on("ready", async () => {
|
|
27
67
|
this.isConnected = true;
|
|
68
|
+
this.isLocalConnection = false;
|
|
28
69
|
this.currentServer = config;
|
|
70
|
+
// 注册敏感信息到过滤器
|
|
71
|
+
this.registerSensitiveInfo(config);
|
|
29
72
|
// 连接成功后自动打开 shell
|
|
30
73
|
try {
|
|
31
74
|
await this.shellManager.open(this.client);
|
|
@@ -74,24 +117,43 @@ export class SSHManager {
|
|
|
74
117
|
async disconnect() {
|
|
75
118
|
// 先关闭 shell
|
|
76
119
|
this.shellManager.close();
|
|
77
|
-
if (this.client && this.isConnected) {
|
|
120
|
+
if (this.client && this.isConnected && !this.isLocalConnection) {
|
|
78
121
|
this.client.end();
|
|
79
122
|
}
|
|
80
123
|
this.cleanup();
|
|
81
124
|
}
|
|
82
125
|
cleanup() {
|
|
83
126
|
this.isConnected = false;
|
|
127
|
+
this.isLocalConnection = false;
|
|
84
128
|
this.currentServer = null;
|
|
85
129
|
this.client = null;
|
|
130
|
+
// 清除敏感信息
|
|
131
|
+
getSanitizer().clearSensitiveValues();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 注册服务器配置中的敏感信息
|
|
135
|
+
*/
|
|
136
|
+
registerSensitiveInfo(config) {
|
|
137
|
+
const sanitizer = getSanitizer();
|
|
138
|
+
sanitizer.addSensitiveValues([
|
|
139
|
+
config.host,
|
|
140
|
+
config.password,
|
|
141
|
+
config.passphrase,
|
|
142
|
+
config.username,
|
|
143
|
+
config.privateKeyPath,
|
|
144
|
+
]);
|
|
86
145
|
}
|
|
87
146
|
getShellManager() {
|
|
88
147
|
return this.shellManager;
|
|
89
148
|
}
|
|
149
|
+
isLocal() {
|
|
150
|
+
return this.isLocalConnection;
|
|
151
|
+
}
|
|
90
152
|
getStatus() {
|
|
91
153
|
return {
|
|
92
154
|
connected: this.isConnected,
|
|
93
155
|
serverName: this.currentServer?.name || null,
|
|
94
|
-
host: this.currentServer?.host || null,
|
|
156
|
+
host: this.isLocalConnection ? "local" : (this.currentServer?.host || null),
|
|
95
157
|
username: this.currentServer?.username || null,
|
|
96
158
|
};
|
|
97
159
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,55 +1,129 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { LOCAL_SERVER } from "./ssh-manager.js";
|
|
3
|
+
import { sanitize } from "./sanitizer.js";
|
|
4
|
+
/**
|
|
5
|
+
* 过滤 ShellResult 中的敏感信息
|
|
6
|
+
*/
|
|
7
|
+
function sanitizeResult(result) {
|
|
8
|
+
return {
|
|
9
|
+
...result,
|
|
10
|
+
output: sanitize(result.output),
|
|
11
|
+
message: sanitize(result.message),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
2
14
|
export function registerTools(server, sshManager, configManager) {
|
|
3
|
-
// 工具1: ssh - 连接管理 + 快捷命令
|
|
4
15
|
server.registerTool("ssh", {
|
|
5
|
-
description: `SSH
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
- list: 列出所有可用服务器
|
|
9
|
-
- connect: 连接服务器(需提供 server 参数)
|
|
10
|
-
- disconnect: 断开当前连接
|
|
11
|
-
- status: 查看连接状态和 shell 缓冲区行数
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
直接提供 command
|
|
15
|
-
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
description: `SSH 远程服务器连接管理和 PTY Shell 交互。
|
|
17
|
+
|
|
18
|
+
## 连接管理 (action)
|
|
19
|
+
- list: 列出所有可用服务器
|
|
20
|
+
- connect: 连接服务器(需提供 server 参数)
|
|
21
|
+
- disconnect: 断开当前连接
|
|
22
|
+
- status: 查看连接状态和 shell 缓冲区行数
|
|
23
|
+
|
|
24
|
+
## 命令执行 (command)
|
|
25
|
+
直接提供 command 参数执行命令,支持交互式程序。
|
|
26
|
+
|
|
27
|
+
## Shell 控制
|
|
28
|
+
- read: 设为 true 读取缓冲区(配合 lines/offset/clear)
|
|
29
|
+
- signal: 发送信号 SIGINT(Ctrl+C)/SIGTSTP(Ctrl+Z)/SIGQUIT
|
|
30
|
+
|
|
31
|
+
## 智能输出检测
|
|
32
|
+
- 快速命令(<2秒):检测到提示符后返回
|
|
33
|
+
- 慢速命令(2-5秒):标记 slow=true
|
|
34
|
+
- 超时命令(>5秒):截断到最近 200 行,标记 truncated=true
|
|
35
|
+
- 持续输出:输出稳定后返回,标记 waiting=true
|
|
36
|
+
|
|
37
|
+
## 返回字段
|
|
38
|
+
- output: 命令输出
|
|
39
|
+
- totalLines: 总行数
|
|
40
|
+
- complete: 是否完成(出现提示符)
|
|
41
|
+
- truncated: 是否被截断
|
|
42
|
+
- slow: 是否耗时较长
|
|
43
|
+
- waiting: 可能在等待输入或仍在运行
|
|
44
|
+
|
|
45
|
+
## 使用示例
|
|
46
|
+
|
|
47
|
+
### 基本操作
|
|
48
|
+
ssh({ action: "list" })
|
|
49
|
+
ssh({ action: "connect", server: "my-server" })
|
|
50
|
+
ssh({ command: "ls -la" })
|
|
51
|
+
ssh({ action: "status" })
|
|
52
|
+
ssh({ action: "disconnect" })
|
|
53
|
+
|
|
54
|
+
### 读取缓冲区
|
|
55
|
+
ssh({ read: true }) # 读取最近 20 行
|
|
56
|
+
ssh({ read: true, lines: -1 }) # 读取全部
|
|
57
|
+
ssh({ read: true, lines: 100 }) # 读取 100 行
|
|
58
|
+
|
|
59
|
+
### 交互式程序
|
|
60
|
+
ssh({ command: "mysql -u root -p" }) # 启动 mysql
|
|
61
|
+
ssh({ command: "password123" }) # 输入密码
|
|
62
|
+
ssh({ command: "SHOW DATABASES;" }) # 执行 SQL
|
|
63
|
+
|
|
64
|
+
### 信号控制
|
|
65
|
+
ssh({ command: "tail -f /var/log/syslog" })
|
|
66
|
+
ssh({ read: true }) # 查看输出
|
|
67
|
+
ssh({ signal: "SIGINT" }) # Ctrl+C 停止`,
|
|
42
68
|
inputSchema: {
|
|
43
69
|
action: z
|
|
44
70
|
.enum(["list", "connect", "disconnect", "status"])
|
|
45
71
|
.optional()
|
|
46
|
-
.describe("
|
|
72
|
+
.describe("连接管理操作"),
|
|
47
73
|
server: z.string().optional().describe("服务器名称(connect 时必填)"),
|
|
48
|
-
command: z.string().optional().describe("
|
|
74
|
+
command: z.string().optional().describe("要执行的命令"),
|
|
75
|
+
read: z.boolean().optional().describe("读取缓冲区"),
|
|
76
|
+
lines: z.number().optional().describe("读取行数,默认 20,-1 返回全部"),
|
|
77
|
+
offset: z.number().optional().describe("读取起始偏移,默认 0"),
|
|
78
|
+
clear: z.boolean().optional().describe("读取后清空缓冲区"),
|
|
79
|
+
signal: z
|
|
80
|
+
.enum(["SIGINT", "SIGTSTP", "SIGQUIT"])
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("发送信号:SIGINT(Ctrl+C)/SIGTSTP(Ctrl+Z)/SIGQUIT"),
|
|
49
83
|
},
|
|
50
|
-
}, async ({ action, server: serverName, command }) => {
|
|
84
|
+
}, async ({ action, server: serverName, command, read, lines, offset, clear, signal }) => {
|
|
51
85
|
try {
|
|
52
|
-
//
|
|
86
|
+
// 1. 发送信号
|
|
87
|
+
if (signal) {
|
|
88
|
+
const status = sshManager.getStatus();
|
|
89
|
+
if (!status.connected) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "未连接服务器" }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const shellManager = sshManager.getShellManager();
|
|
96
|
+
const success = shellManager.sendSignal(signal);
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: success ? `已发送 ${signal}` : `发送 ${signal} 失败`,
|
|
101
|
+
}],
|
|
102
|
+
isError: !success,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// 2. 读取缓冲区
|
|
106
|
+
if (read) {
|
|
107
|
+
const status = sshManager.getStatus();
|
|
108
|
+
if (!status.connected) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: "未连接服务器" }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const shellManager = sshManager.getShellManager();
|
|
115
|
+
const result = sanitizeResult(shellManager.read(lines, offset, clear));
|
|
116
|
+
return {
|
|
117
|
+
content: [{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify({
|
|
120
|
+
server: status.serverName,
|
|
121
|
+
...result,
|
|
122
|
+
}, null, 2),
|
|
123
|
+
}],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// 3. 执行命令
|
|
53
127
|
if (command) {
|
|
54
128
|
const status = sshManager.getStatus();
|
|
55
129
|
if (!status.connected) {
|
|
@@ -59,7 +133,7 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
59
133
|
};
|
|
60
134
|
}
|
|
61
135
|
const shellManager = sshManager.getShellManager();
|
|
62
|
-
const result = await shellManager.send(command);
|
|
136
|
+
const result = sanitizeResult(await shellManager.send(command));
|
|
63
137
|
return {
|
|
64
138
|
content: [{
|
|
65
139
|
type: "text",
|
|
@@ -72,17 +146,25 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
72
146
|
isError: !result.complete && !result.waiting,
|
|
73
147
|
};
|
|
74
148
|
}
|
|
75
|
-
//
|
|
149
|
+
// 4. 连接管理操作
|
|
76
150
|
const effectiveAction = action || "status";
|
|
77
151
|
switch (effectiveAction) {
|
|
78
152
|
case "list": {
|
|
79
153
|
const servers = configManager.listServers();
|
|
80
154
|
const status = sshManager.getStatus();
|
|
81
|
-
//
|
|
82
|
-
const list =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
155
|
+
// 添加内置的 local 服务器
|
|
156
|
+
const list = [
|
|
157
|
+
{
|
|
158
|
+
name: LOCAL_SERVER.name,
|
|
159
|
+
connected: status.serverName === LOCAL_SERVER.name,
|
|
160
|
+
type: "built-in",
|
|
161
|
+
},
|
|
162
|
+
...servers.map((s) => ({
|
|
163
|
+
name: s.name,
|
|
164
|
+
connected: status.serverName === s.name,
|
|
165
|
+
type: "configured",
|
|
166
|
+
})),
|
|
167
|
+
];
|
|
86
168
|
return {
|
|
87
169
|
content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
|
|
88
170
|
};
|
|
@@ -94,9 +176,19 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
94
176
|
isError: true,
|
|
95
177
|
};
|
|
96
178
|
}
|
|
179
|
+
// 检查是否是本地连接
|
|
180
|
+
if (serverName === "local") {
|
|
181
|
+
await sshManager.connect(LOCAL_SERVER);
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: "成功连接到本地 Shell",
|
|
186
|
+
}],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
97
189
|
const serverConfig = configManager.getServer(serverName);
|
|
98
190
|
if (!serverConfig) {
|
|
99
|
-
const available = configManager.listServers().map((s) => s.name);
|
|
191
|
+
const available = ["local", ...configManager.listServers().map((s) => s.name)];
|
|
100
192
|
return {
|
|
101
193
|
content: [{
|
|
102
194
|
type: "text",
|
|
@@ -106,7 +198,6 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
106
198
|
};
|
|
107
199
|
}
|
|
108
200
|
await sshManager.connect(serverConfig);
|
|
109
|
-
// 不暴露 IP/端口/用户名
|
|
110
201
|
return {
|
|
111
202
|
content: [{
|
|
112
203
|
type: "text",
|
|
@@ -131,7 +222,6 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
131
222
|
const status = sshManager.getStatus();
|
|
132
223
|
const shellManager = sshManager.getShellManager();
|
|
133
224
|
if (status.connected) {
|
|
134
|
-
// 不暴露 host/username
|
|
135
225
|
return {
|
|
136
226
|
content: [{
|
|
137
227
|
type: "text",
|
|
@@ -160,169 +250,4 @@ export function registerTools(server, sshManager, configManager) {
|
|
|
160
250
|
};
|
|
161
251
|
}
|
|
162
252
|
});
|
|
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
253
|
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mcp-ssh-pty",
|
|
3
|
-
"version": "1.
|
|
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
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@types/
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-ssh-pty",
|
|
3
|
+
"version": "1.2.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
|
+
}
|
package/ssh-servers.example.json
CHANGED
|
@@ -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
|
+
}
|