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/README.md +149 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +536 -0
- package/dist/config.d.ts +13 -1
- package/dist/config.js +86 -8
- package/dist/index.js +30 -1
- package/dist/sanitizer.d.ts +48 -0
- package/dist/sanitizer.js +123 -0
- package/dist/shell-manager.d.ts +3 -0
- package/dist/shell-manager.js +7 -2
- package/dist/ssh-manager.d.ts +4 -0
- package/dist/ssh-manager.js +18 -0
- package/dist/tools.js +117 -212
- package/package.json +47 -46
- package/ssh-servers.example.json +18 -18
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# mcp-ssh-pty
|
|
2
|
+
|
|
3
|
+
MCP Server for SSH remote command execution with PTY shell support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g mcp-ssh-pty
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Add to Claude Code
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add ssh -- npx -y mcp-ssh-pty
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI Commands
|
|
18
|
+
|
|
19
|
+
### List servers
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mcp-ssh-pty list # Auto-detect config level
|
|
23
|
+
mcp-ssh-pty list --local # Project level only
|
|
24
|
+
mcp-ssh-pty list --global # User level only
|
|
25
|
+
mcp-ssh-pty list --all # Show both levels
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Add server
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Interactive mode (will ask for config level)
|
|
32
|
+
mcp-ssh-pty add
|
|
33
|
+
|
|
34
|
+
# Save to project level
|
|
35
|
+
mcp-ssh-pty add my-server -l -H 192.168.1.100 -u root -k ~/.ssh/id_rsa
|
|
36
|
+
|
|
37
|
+
# Save to user level
|
|
38
|
+
mcp-ssh-pty add my-server -g -H 192.168.1.100 -u root -p mypassword
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Remove server
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
mcp-ssh-pty remove my-server
|
|
45
|
+
mcp-ssh-pty remove --local # From project level
|
|
46
|
+
mcp-ssh-pty remove --global # From user level
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Test connection
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mcp-ssh-pty test my-server
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Interactive configuration
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mcp-ssh-pty config
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
? 选择配置级别:
|
|
63
|
+
❯ 📁 项目级别 (已存在)
|
|
64
|
+
🌐 用户级别 (新建)
|
|
65
|
+
|
|
66
|
+
? 选择操作:
|
|
67
|
+
❯ 📋 查看所有服务器
|
|
68
|
+
➕ 添加服务器
|
|
69
|
+
✏️ 编辑服务器
|
|
70
|
+
🗑️ 删除服务器
|
|
71
|
+
🔌 测试连接
|
|
72
|
+
🔄 切换配置级别
|
|
73
|
+
📁 显示配置文件路径
|
|
74
|
+
🚪 退出
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
### Config file locations
|
|
80
|
+
|
|
81
|
+
| Level | Path | Priority |
|
|
82
|
+
|-------|------|----------|
|
|
83
|
+
| Project | `./.claude/ssh-servers.json` | High |
|
|
84
|
+
| User | `~/.claude/ssh-servers.json` | Low |
|
|
85
|
+
| Custom | `SSH_MCP_CONFIG_PATH` env | Highest |
|
|
86
|
+
|
|
87
|
+
Project level config overrides user level when exists.
|
|
88
|
+
|
|
89
|
+
### Config format
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"servers": [
|
|
94
|
+
{
|
|
95
|
+
"name": "my-server",
|
|
96
|
+
"host": "192.168.1.100",
|
|
97
|
+
"port": 22,
|
|
98
|
+
"username": "root",
|
|
99
|
+
"privateKeyPath": "~/.ssh/id_rsa"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## MCP Usage
|
|
106
|
+
|
|
107
|
+
### Connection
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
ssh({ action: "list" })
|
|
111
|
+
ssh({ action: "connect", server: "my-server" })
|
|
112
|
+
ssh({ action: "status" })
|
|
113
|
+
ssh({ action: "disconnect" })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Command Execution
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
ssh({ command: "ls -la" })
|
|
120
|
+
ssh({ command: "whoami" })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Interactive Programs
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
ssh({ command: "mysql -u root -p" })
|
|
127
|
+
ssh({ command: "password123" })
|
|
128
|
+
ssh({ command: "SHOW DATABASES;" })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Read Buffer
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
ssh({ read: true }) # Last 20 lines
|
|
135
|
+
ssh({ read: true, lines: -1 }) # All
|
|
136
|
+
ssh({ read: true, lines: 100 }) # 100 lines
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Signal Control
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
ssh({ command: "tail -f /var/log/syslog" })
|
|
143
|
+
ssh({ read: true })
|
|
144
|
+
ssh({ signal: "SIGINT" }) # Ctrl+C
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { select, input, password, confirm } from "@inquirer/prompts";
|
|
4
|
+
import { ConfigManager } from "./config.js";
|
|
5
|
+
import { SSHManager } from "./ssh-manager.js";
|
|
6
|
+
/**
|
|
7
|
+
* 获取 ConfigManager 实例
|
|
8
|
+
*/
|
|
9
|
+
function getConfigManager(scope) {
|
|
10
|
+
return new ConfigManager(undefined, scope);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 格式化服务器信息显示(隐藏敏感信息)
|
|
14
|
+
*/
|
|
15
|
+
function formatServer(server) {
|
|
16
|
+
const auth = server.privateKeyPath ? `key:${server.privateKeyPath}` : "password";
|
|
17
|
+
return `${server.name} (${server.username}@${server.host}:${server.port || 22}) [${auth}]`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 格式化 scope 显示
|
|
21
|
+
*/
|
|
22
|
+
function formatScope(scope) {
|
|
23
|
+
return scope === "local" ? "📁 项目级别" : "🌐 用户级别";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* list 命令
|
|
27
|
+
*/
|
|
28
|
+
async function listServers(options) {
|
|
29
|
+
if (options.all) {
|
|
30
|
+
// 显示两个级别的配置
|
|
31
|
+
console.log("=== 项目级别 (local) ===");
|
|
32
|
+
console.log(`路径: ${ConfigManager.getLocalPath()}`);
|
|
33
|
+
if (ConfigManager.localConfigExists()) {
|
|
34
|
+
const localManager = new ConfigManager(undefined, "local");
|
|
35
|
+
const localServers = localManager.listServers();
|
|
36
|
+
if (localServers.length === 0) {
|
|
37
|
+
console.log("(空)\n");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
localServers.forEach((s, i) => console.log(` ${i + 1}. ${formatServer(s)}`));
|
|
41
|
+
console.log("");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log("(不存在)\n");
|
|
46
|
+
}
|
|
47
|
+
console.log("=== 用户级别 (global) ===");
|
|
48
|
+
console.log(`路径: ${ConfigManager.getGlobalPath()}`);
|
|
49
|
+
if (ConfigManager.globalConfigExists()) {
|
|
50
|
+
const globalManager = new ConfigManager(undefined, "global");
|
|
51
|
+
const globalServers = globalManager.listServers();
|
|
52
|
+
if (globalServers.length === 0) {
|
|
53
|
+
console.log("(空)");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
globalServers.forEach((s, i) => console.log(` ${i + 1}. ${formatServer(s)}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log("(不存在)");
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const scope = options.local ? "local" : options.global ? "global" : undefined;
|
|
65
|
+
const configManager = getConfigManager(scope);
|
|
66
|
+
const servers = configManager.listServers();
|
|
67
|
+
console.log(`配置级别: ${formatScope(configManager.getScope())}`);
|
|
68
|
+
console.log(`配置文件: ${configManager.getConfigPath()}\n`);
|
|
69
|
+
if (servers.length === 0) {
|
|
70
|
+
console.log("没有配置任何服务器");
|
|
71
|
+
console.log("\n使用 'mcp-ssh-pty add' 添加服务器");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log("已配置的服务器:\n");
|
|
75
|
+
servers.forEach((server, index) => {
|
|
76
|
+
console.log(` ${index + 1}. ${formatServer(server)}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* add 命令 - 支持参数和交互式
|
|
81
|
+
*/
|
|
82
|
+
async function addServer(name, options) {
|
|
83
|
+
// 确定配置级别
|
|
84
|
+
let scope;
|
|
85
|
+
if (options?.local) {
|
|
86
|
+
scope = "local";
|
|
87
|
+
}
|
|
88
|
+
else if (options?.global) {
|
|
89
|
+
scope = "global";
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// 交互式选择
|
|
93
|
+
scope = await select({
|
|
94
|
+
message: "保存到哪个级别?",
|
|
95
|
+
choices: [
|
|
96
|
+
{ name: "📁 项目级别 (当前目录/.claude/)", value: "local" },
|
|
97
|
+
{ name: "🌐 用户级别 (~/.claude/)", value: "global" },
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const configManager = getConfigManager(scope);
|
|
102
|
+
let serverName = name;
|
|
103
|
+
let host = options?.host;
|
|
104
|
+
let port = options?.port ? parseInt(options.port) : 22;
|
|
105
|
+
let username = options?.user;
|
|
106
|
+
let authPassword = options?.password;
|
|
107
|
+
let privateKeyPath = options?.key;
|
|
108
|
+
let passphrase = options?.passphrase;
|
|
109
|
+
// 交互式获取缺失的参数
|
|
110
|
+
if (!serverName) {
|
|
111
|
+
serverName = await input({
|
|
112
|
+
message: "服务器名称:",
|
|
113
|
+
validate: (v) => v.trim() ? true : "名称不能为空",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!host) {
|
|
117
|
+
host = await input({
|
|
118
|
+
message: "主机地址:",
|
|
119
|
+
validate: (v) => v.trim() ? true : "地址不能为空",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (!options?.port) {
|
|
123
|
+
const portStr = await input({
|
|
124
|
+
message: "端口:",
|
|
125
|
+
default: "22",
|
|
126
|
+
});
|
|
127
|
+
port = parseInt(portStr) || 22;
|
|
128
|
+
}
|
|
129
|
+
if (!username) {
|
|
130
|
+
username = await input({
|
|
131
|
+
message: "用户名:",
|
|
132
|
+
validate: (v) => v.trim() ? true : "用户名不能为空",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// 认证方式
|
|
136
|
+
if (!authPassword && !privateKeyPath) {
|
|
137
|
+
const authType = await select({
|
|
138
|
+
message: "认证方式:",
|
|
139
|
+
choices: [
|
|
140
|
+
{ name: "私钥", value: "key" },
|
|
141
|
+
{ name: "密码", value: "password" },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
if (authType === "key") {
|
|
145
|
+
privateKeyPath = await input({
|
|
146
|
+
message: "私钥路径:",
|
|
147
|
+
default: "~/.ssh/id_rsa",
|
|
148
|
+
});
|
|
149
|
+
const needPassphrase = await confirm({
|
|
150
|
+
message: "私钥是否有密码保护?",
|
|
151
|
+
default: false,
|
|
152
|
+
});
|
|
153
|
+
if (needPassphrase) {
|
|
154
|
+
passphrase = await password({
|
|
155
|
+
message: "私钥密码:",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
authPassword = await password({
|
|
161
|
+
message: "登录密码:",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const server = {
|
|
166
|
+
name: serverName,
|
|
167
|
+
host,
|
|
168
|
+
port,
|
|
169
|
+
username,
|
|
170
|
+
};
|
|
171
|
+
if (privateKeyPath) {
|
|
172
|
+
server.privateKeyPath = privateKeyPath;
|
|
173
|
+
if (passphrase) {
|
|
174
|
+
server.passphrase = passphrase;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (authPassword) {
|
|
178
|
+
server.password = authPassword;
|
|
179
|
+
}
|
|
180
|
+
// 检查是否已存在
|
|
181
|
+
const existing = configManager.getServer(serverName);
|
|
182
|
+
if (existing) {
|
|
183
|
+
const overwrite = await confirm({
|
|
184
|
+
message: `服务器 '${serverName}' 已存在,是否覆盖?`,
|
|
185
|
+
default: false,
|
|
186
|
+
});
|
|
187
|
+
if (!overwrite) {
|
|
188
|
+
console.log("已取消");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
configManager.addServer(server);
|
|
193
|
+
console.log(`\n✓ 服务器 '${serverName}' 已添加`);
|
|
194
|
+
console.log(` 级别: ${formatScope(scope)}`);
|
|
195
|
+
console.log(` 配置文件: ${configManager.getConfigPath()}`);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* remove 命令
|
|
199
|
+
*/
|
|
200
|
+
async function removeServer(name, options) {
|
|
201
|
+
const scope = options?.local ? "local" : options?.global ? "global" : undefined;
|
|
202
|
+
const configManager = getConfigManager(scope);
|
|
203
|
+
const servers = configManager.listServers();
|
|
204
|
+
console.log(`配置级别: ${formatScope(configManager.getScope())}\n`);
|
|
205
|
+
if (servers.length === 0) {
|
|
206
|
+
console.log("没有配置任何服务器");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let serverName = name;
|
|
210
|
+
if (!serverName) {
|
|
211
|
+
serverName = await select({
|
|
212
|
+
message: "选择要删除的服务器:",
|
|
213
|
+
choices: servers.map((s) => ({
|
|
214
|
+
name: formatServer(s),
|
|
215
|
+
value: s.name,
|
|
216
|
+
})),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const server = configManager.getServer(serverName);
|
|
220
|
+
if (!server) {
|
|
221
|
+
console.log(`服务器 '${serverName}' 不存在`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const confirmed = await confirm({
|
|
225
|
+
message: `确定要删除服务器 '${serverName}' 吗?`,
|
|
226
|
+
default: false,
|
|
227
|
+
});
|
|
228
|
+
if (!confirmed) {
|
|
229
|
+
console.log("已取消");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
configManager.removeServer(serverName);
|
|
233
|
+
console.log(`\n✓ 服务器 '${serverName}' 已删除`);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* test 命令 - 测试连接
|
|
237
|
+
*/
|
|
238
|
+
async function testServer(name, options) {
|
|
239
|
+
const scope = options?.local ? "local" : options?.global ? "global" : undefined;
|
|
240
|
+
const configManager = getConfigManager(scope);
|
|
241
|
+
const servers = configManager.listServers();
|
|
242
|
+
if (servers.length === 0) {
|
|
243
|
+
console.log("没有配置任何服务器");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let serverName = name;
|
|
247
|
+
if (!serverName) {
|
|
248
|
+
serverName = await select({
|
|
249
|
+
message: "选择要测试的服务器:",
|
|
250
|
+
choices: servers.map((s) => ({
|
|
251
|
+
name: formatServer(s),
|
|
252
|
+
value: s.name,
|
|
253
|
+
})),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const server = configManager.getServer(serverName);
|
|
257
|
+
if (!server) {
|
|
258
|
+
console.log(`服务器 '${serverName}' 不存在`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.log(`\n测试连接到 '${serverName}'...`);
|
|
262
|
+
const sshManager = new SSHManager();
|
|
263
|
+
try {
|
|
264
|
+
await sshManager.connect(server);
|
|
265
|
+
console.log("✓ 连接成功!");
|
|
266
|
+
// 执行简单命令测试
|
|
267
|
+
const shellManager = sshManager.getShellManager();
|
|
268
|
+
const result = await shellManager.send("echo 'SSH connection test successful'");
|
|
269
|
+
if (result.complete) {
|
|
270
|
+
console.log("✓ Shell 正常工作");
|
|
271
|
+
}
|
|
272
|
+
await sshManager.disconnect();
|
|
273
|
+
console.log("✓ 已断开连接");
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
console.log(`✗ 连接失败: ${message}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* config 命令 - 交互式配置
|
|
283
|
+
*/
|
|
284
|
+
async function interactiveConfig() {
|
|
285
|
+
// 先选择配置级别
|
|
286
|
+
const scope = await select({
|
|
287
|
+
message: "选择配置级别:",
|
|
288
|
+
choices: [
|
|
289
|
+
{
|
|
290
|
+
name: `📁 项目级别 (${ConfigManager.localConfigExists() ? "已存在" : "新建"})`,
|
|
291
|
+
value: "local",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: `🌐 用户级别 (${ConfigManager.globalConfigExists() ? "已存在" : "新建"})`,
|
|
295
|
+
value: "global",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
const configManager = getConfigManager(scope);
|
|
300
|
+
console.log(`\n当前配置: ${formatScope(scope)}`);
|
|
301
|
+
console.log(`路径: ${configManager.getConfigPath()}\n`);
|
|
302
|
+
while (true) {
|
|
303
|
+
const action = await select({
|
|
304
|
+
message: "选择操作:",
|
|
305
|
+
choices: [
|
|
306
|
+
{ name: "📋 查看所有服务器", value: "list" },
|
|
307
|
+
{ name: "➕ 添加服务器", value: "add" },
|
|
308
|
+
{ name: "✏️ 编辑服务器", value: "edit" },
|
|
309
|
+
{ name: "🗑️ 删除服务器", value: "remove" },
|
|
310
|
+
{ name: "🔌 测试连接", value: "test" },
|
|
311
|
+
{ name: "🔄 切换配置级别", value: "switch" },
|
|
312
|
+
{ name: "📁 显示配置文件路径", value: "path" },
|
|
313
|
+
{ name: "🚪 退出", value: "exit" },
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
console.log("");
|
|
317
|
+
switch (action) {
|
|
318
|
+
case "list": {
|
|
319
|
+
const servers = configManager.listServers();
|
|
320
|
+
if (servers.length === 0) {
|
|
321
|
+
console.log("没有配置任何服务器");
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
servers.forEach((s, i) => console.log(` ${i + 1}. ${formatServer(s)}`));
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case "add":
|
|
329
|
+
await addServerToManager(configManager);
|
|
330
|
+
break;
|
|
331
|
+
case "edit": {
|
|
332
|
+
const servers = configManager.listServers();
|
|
333
|
+
if (servers.length === 0) {
|
|
334
|
+
console.log("没有配置任何服务器");
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
const editName = await select({
|
|
338
|
+
message: "选择要编辑的服务器:",
|
|
339
|
+
choices: servers.map((s) => ({
|
|
340
|
+
name: formatServer(s),
|
|
341
|
+
value: s.name,
|
|
342
|
+
})),
|
|
343
|
+
});
|
|
344
|
+
const server = configManager.getServer(editName);
|
|
345
|
+
if (server) {
|
|
346
|
+
console.log("\n当前配置:");
|
|
347
|
+
console.log(` 名称: ${server.name}`);
|
|
348
|
+
console.log(` 主机: ${server.host}`);
|
|
349
|
+
console.log(` 端口: ${server.port || 22}`);
|
|
350
|
+
console.log(` 用户: ${server.username}`);
|
|
351
|
+
console.log(` 认证: ${server.privateKeyPath ? `私钥(${server.privateKeyPath})` : "密码"}`);
|
|
352
|
+
console.log("\n重新输入新配置:\n");
|
|
353
|
+
await addServerToManager(configManager, server.name);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "remove": {
|
|
358
|
+
const servers = configManager.listServers();
|
|
359
|
+
if (servers.length === 0) {
|
|
360
|
+
console.log("没有配置任何服务器");
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
const removeName = await select({
|
|
364
|
+
message: "选择要删除的服务器:",
|
|
365
|
+
choices: servers.map((s) => ({
|
|
366
|
+
name: formatServer(s),
|
|
367
|
+
value: s.name,
|
|
368
|
+
})),
|
|
369
|
+
});
|
|
370
|
+
const confirmed = await confirm({
|
|
371
|
+
message: `确定要删除服务器 '${removeName}' 吗?`,
|
|
372
|
+
default: false,
|
|
373
|
+
});
|
|
374
|
+
if (confirmed) {
|
|
375
|
+
configManager.removeServer(removeName);
|
|
376
|
+
console.log(`✓ 服务器 '${removeName}' 已删除`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
console.log("已取消");
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "test": {
|
|
384
|
+
const servers = configManager.listServers();
|
|
385
|
+
if (servers.length === 0) {
|
|
386
|
+
console.log("没有配置任何服务器");
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
const testName = await select({
|
|
390
|
+
message: "选择要测试的服务器:",
|
|
391
|
+
choices: servers.map((s) => ({
|
|
392
|
+
name: formatServer(s),
|
|
393
|
+
value: s.name,
|
|
394
|
+
})),
|
|
395
|
+
});
|
|
396
|
+
const server = configManager.getServer(testName);
|
|
397
|
+
if (server) {
|
|
398
|
+
console.log(`\n测试连接到 '${testName}'...`);
|
|
399
|
+
const sshManager = new SSHManager();
|
|
400
|
+
try {
|
|
401
|
+
await sshManager.connect(server);
|
|
402
|
+
console.log("✓ 连接成功!");
|
|
403
|
+
await sshManager.disconnect();
|
|
404
|
+
console.log("✓ 已断开连接");
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
+
console.log(`✗ 连接失败: ${message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case "switch":
|
|
414
|
+
// 递归调用,切换级别
|
|
415
|
+
return interactiveConfig();
|
|
416
|
+
case "path":
|
|
417
|
+
console.log(`配置级别: ${formatScope(scope)}`);
|
|
418
|
+
console.log(`配置文件路径: ${configManager.getConfigPath()}`);
|
|
419
|
+
console.log(`文件是否存在: ${configManager.configExists() ? "是" : "否"}`);
|
|
420
|
+
break;
|
|
421
|
+
case "exit":
|
|
422
|
+
console.log("再见!");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
console.log("");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 向指定 ConfigManager 添加服务器
|
|
430
|
+
*/
|
|
431
|
+
async function addServerToManager(configManager, existingName) {
|
|
432
|
+
const serverName = existingName || await input({
|
|
433
|
+
message: "服务器名称:",
|
|
434
|
+
validate: (v) => v.trim() ? true : "名称不能为空",
|
|
435
|
+
});
|
|
436
|
+
const host = await input({
|
|
437
|
+
message: "主机地址:",
|
|
438
|
+
validate: (v) => v.trim() ? true : "地址不能为空",
|
|
439
|
+
});
|
|
440
|
+
const portStr = await input({
|
|
441
|
+
message: "端口:",
|
|
442
|
+
default: "22",
|
|
443
|
+
});
|
|
444
|
+
const port = parseInt(portStr) || 22;
|
|
445
|
+
const username = await input({
|
|
446
|
+
message: "用户名:",
|
|
447
|
+
validate: (v) => v.trim() ? true : "用户名不能为空",
|
|
448
|
+
});
|
|
449
|
+
const authType = await select({
|
|
450
|
+
message: "认证方式:",
|
|
451
|
+
choices: [
|
|
452
|
+
{ name: "私钥", value: "key" },
|
|
453
|
+
{ name: "密码", value: "password" },
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
const server = {
|
|
457
|
+
name: serverName,
|
|
458
|
+
host,
|
|
459
|
+
port,
|
|
460
|
+
username,
|
|
461
|
+
};
|
|
462
|
+
if (authType === "key") {
|
|
463
|
+
server.privateKeyPath = await input({
|
|
464
|
+
message: "私钥路径:",
|
|
465
|
+
default: "~/.ssh/id_rsa",
|
|
466
|
+
});
|
|
467
|
+
const needPassphrase = await confirm({
|
|
468
|
+
message: "私钥是否有密码保护?",
|
|
469
|
+
default: false,
|
|
470
|
+
});
|
|
471
|
+
if (needPassphrase) {
|
|
472
|
+
server.passphrase = await password({
|
|
473
|
+
message: "私钥密码:",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
server.password = await password({
|
|
479
|
+
message: "登录密码:",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
configManager.addServer(server);
|
|
483
|
+
console.log(`\n✓ 服务器 '${serverName}' 已保存`);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* 创建 CLI 程序
|
|
487
|
+
*/
|
|
488
|
+
export function createCLI() {
|
|
489
|
+
const program = new Command();
|
|
490
|
+
program
|
|
491
|
+
.name("mcp-ssh-pty")
|
|
492
|
+
.description("SSH MCP Server with PTY shell support")
|
|
493
|
+
.version("1.0.0");
|
|
494
|
+
program
|
|
495
|
+
.command("list")
|
|
496
|
+
.description("列出所有已配置的服务器")
|
|
497
|
+
.option("-l, --local", "只显示项目级别配置")
|
|
498
|
+
.option("-g, --global", "只显示用户级别配置")
|
|
499
|
+
.option("-a, --all", "显示所有级别配置")
|
|
500
|
+
.action(listServers);
|
|
501
|
+
program
|
|
502
|
+
.command("add [name]")
|
|
503
|
+
.description("添加新服务器")
|
|
504
|
+
.option("-l, --local", "保存到项目级别")
|
|
505
|
+
.option("-g, --global", "保存到用户级别")
|
|
506
|
+
.option("-H, --host <host>", "主机地址")
|
|
507
|
+
.option("-P, --port <port>", "端口", "22")
|
|
508
|
+
.option("-u, --user <user>", "用户名")
|
|
509
|
+
.option("-p, --password <password>", "密码")
|
|
510
|
+
.option("-k, --key <path>", "私钥路径")
|
|
511
|
+
.option("--passphrase <passphrase>", "私钥密码")
|
|
512
|
+
.action(addServer);
|
|
513
|
+
program
|
|
514
|
+
.command("remove [name]")
|
|
515
|
+
.alias("rm")
|
|
516
|
+
.description("删除服务器")
|
|
517
|
+
.option("-l, --local", "从项目级别删除")
|
|
518
|
+
.option("-g, --global", "从用户级别删除")
|
|
519
|
+
.action(removeServer);
|
|
520
|
+
program
|
|
521
|
+
.command("test [name]")
|
|
522
|
+
.description("测试服务器连接")
|
|
523
|
+
.option("-l, --local", "使用项目级别配置")
|
|
524
|
+
.option("-g, --global", "使用用户级别配置")
|
|
525
|
+
.action(testServer);
|
|
526
|
+
program
|
|
527
|
+
.command("config")
|
|
528
|
+
.description("交互式配置管理")
|
|
529
|
+
.action(interactiveConfig);
|
|
530
|
+
return program;
|
|
531
|
+
}
|
|
532
|
+
// 如果直接运行此文件
|
|
533
|
+
export async function runCLI() {
|
|
534
|
+
const program = createCLI();
|
|
535
|
+
await program.parseAsync(process.argv);
|
|
536
|
+
}
|