mcp-ssh-server-tool 1.0.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 +199 -0
- package/index.js +500 -0
- package/mcp.json +10 -0
- package/package.json +29 -0
- package/ssh_manager.js +199 -0
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SSH MCP Server
|
|
2
|
+
|
|
3
|
+
一个用于 SSH 连接和远程命令执行的 MCP (Model Context Protocol) 服务器。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 🔐 **密码认证** - 使用用户名和密码连接到 SSH 服务器
|
|
8
|
+
- 🔑 **公钥认证** - 使用私钥文件连接到 SSH 服务器
|
|
9
|
+
- 💻 **远程命令执行** - 在连接的服务器上执行命令并获取输出
|
|
10
|
+
- 📊 **会话管理** - 管理多个 SSH 连接会话
|
|
11
|
+
|
|
12
|
+
## MCP 工具
|
|
13
|
+
|
|
14
|
+
### ssh_connect
|
|
15
|
+
建立 SSH 连接
|
|
16
|
+
|
|
17
|
+
**参数:**
|
|
18
|
+
- `host`: 服务器地址 (必需)
|
|
19
|
+
- `port`: 端口号 (默认: 22)
|
|
20
|
+
- `username`: 用户名 (必需)
|
|
21
|
+
- `auth_type`: 认证方式 - "password" | "public_key" (必需)
|
|
22
|
+
- `password`: 密码 (auth_type 为 password 时必需)
|
|
23
|
+
- `private_key_path`: 私钥路径 (auth_type 为 public_key 时必需)
|
|
24
|
+
- `passphrase`: 私钥密码 (可选)
|
|
25
|
+
- `timeout`: 连接超时 (默认: 10秒)
|
|
26
|
+
|
|
27
|
+
### ssh_exec
|
|
28
|
+
在 SSH 会话中执行命令
|
|
29
|
+
|
|
30
|
+
**参数:**
|
|
31
|
+
- `session_id`: SSH 会话 ID (必需)
|
|
32
|
+
- `command`: 要执行的命令 (必需)
|
|
33
|
+
|
|
34
|
+
**返回:**
|
|
35
|
+
- `stdout`: 标准输出
|
|
36
|
+
- `stderr`: 标准错误
|
|
37
|
+
- `exit_code`: 退出码
|
|
38
|
+
|
|
39
|
+
### ssh_disconnect
|
|
40
|
+
断开 SSH 连接
|
|
41
|
+
|
|
42
|
+
**参数:**
|
|
43
|
+
- `session_id`: SSH 会话 ID (必需)
|
|
44
|
+
|
|
45
|
+
### ssh_list_sessions
|
|
46
|
+
列出所有活动的 SSH 会话
|
|
47
|
+
|
|
48
|
+
## 安装
|
|
49
|
+
|
|
50
|
+
### 步骤 1: 安装依赖
|
|
51
|
+
|
|
52
|
+
在终端中运行:
|
|
53
|
+
```bash
|
|
54
|
+
pip install paramiko mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 步骤 2: 安装 SSH MCP Server
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 配置
|
|
64
|
+
|
|
65
|
+
### 重要: 确保依赖已安装
|
|
66
|
+
|
|
67
|
+
在使用 MCP 之前,请确保你的 AI 客户端使用的 Python 环境中已安装依赖:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install paramiko mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Claude Desktop 配置
|
|
74
|
+
|
|
75
|
+
在 Claude Desktop 的配置文件中添加:
|
|
76
|
+
|
|
77
|
+
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"ssh-mcp-server": {
|
|
83
|
+
"command": "python",
|
|
84
|
+
"args": ["-m", "mcp_ssh_server.index"],
|
|
85
|
+
"env": {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Cursor / VS Code 配置
|
|
92
|
+
|
|
93
|
+
在 `.cursor/mcp.json` 或 `.vscode/mcp.json` 中添加相同配置:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"ssh-mcp-server": {
|
|
99
|
+
"command": "python",
|
|
100
|
+
"args": ["-m", "mcp_ssh_server.index"],
|
|
101
|
+
"env": {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 故障排除
|
|
108
|
+
|
|
109
|
+
如果遇到 `Connection closed` 错误:
|
|
110
|
+
|
|
111
|
+
1. **确认依赖已安装**:
|
|
112
|
+
```bash
|
|
113
|
+
python -c "import paramiko; import mcp; print('OK')"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
2. **检查 Python 路径**: 在终端中运行以下命令获取 Python 路径,并在配置中使用完整路径:
|
|
117
|
+
```bash
|
|
118
|
+
where python # Windows
|
|
119
|
+
which python # Linux/Mac
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
3. **使用完整路径配置**:
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"ssh-mcp-server": {
|
|
127
|
+
"command": "C:\\Users\\你的用户名\\anaconda3\\python.exe",
|
|
128
|
+
"args": ["-m", "mcp_ssh_server.index"],
|
|
129
|
+
"env": {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 使用示例
|
|
136
|
+
|
|
137
|
+
### 1. 密码认证连接
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"host": "192.168.1.100",
|
|
142
|
+
"port": 22,
|
|
143
|
+
"username": "admin",
|
|
144
|
+
"auth_type": "password",
|
|
145
|
+
"password": "your_password"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. 公钥认证连接
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"host": "192.168.1.100",
|
|
154
|
+
"port": 22,
|
|
155
|
+
"username": "admin",
|
|
156
|
+
"auth_type": "public_key",
|
|
157
|
+
"private_key_path": "C:\\Users\\you\\.ssh\\id_rsa"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. 执行命令
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"session_id": "<从 ssh_connect 返回的 session_id>",
|
|
166
|
+
"command": "ls -la /home"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 4. 断开连接
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"session_id": "<session_id>"
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 项目结构
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
SSH-MCP/
|
|
182
|
+
├── mcp_ssh_server/
|
|
183
|
+
│ ├── __init__.py
|
|
184
|
+
│ ├── index.py # MCP Server 主入口
|
|
185
|
+
│ └── ssh_manager.py # SSH 连接管理器
|
|
186
|
+
├── pyproject.toml # Python 项目配置
|
|
187
|
+
├── mcp.json # MCP 配置文件示例
|
|
188
|
+
└── README.md # 使用说明
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 依赖
|
|
192
|
+
|
|
193
|
+
- Python >= 3.10
|
|
194
|
+
- paramiko >= 3.4.0
|
|
195
|
+
- mcp >= 1.1.0
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
class SSHSession {
|
|
6
|
+
constructor(sessionId, client, host, username) {
|
|
7
|
+
this.sessionId = sessionId;
|
|
8
|
+
this.client = client;
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.username = username;
|
|
11
|
+
this.connected = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class SSHConnectionManager {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.sessions = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connect(options) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const client = new Client();
|
|
23
|
+
const sessionId = uuidv4();
|
|
24
|
+
const port = options.port || 22;
|
|
25
|
+
const timeout = (options.timeout || 10) * 1000;
|
|
26
|
+
|
|
27
|
+
const config = {
|
|
28
|
+
host: options.host,
|
|
29
|
+
port: port,
|
|
30
|
+
username: options.username,
|
|
31
|
+
timeout: timeout,
|
|
32
|
+
readyTimeout: timeout,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (options.authType === 'password') {
|
|
36
|
+
if (!options.password) {
|
|
37
|
+
resolve({
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Password is required for password authentication',
|
|
40
|
+
errorType: 'MissingPassword',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
config.password = options.password;
|
|
45
|
+
} else if (options.authType === 'public_key') {
|
|
46
|
+
if (!options.privateKeyPath) {
|
|
47
|
+
resolve({
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Private key path is required for public key authentication',
|
|
50
|
+
errorType: 'MissingPrivateKey',
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
config.privateKey = fs.readFileSync(options.privateKeyPath);
|
|
56
|
+
if (options.passphrase) {
|
|
57
|
+
config.passphrase = options.passphrase;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
resolve({
|
|
61
|
+
success: false,
|
|
62
|
+
error: `Failed to read private key: ${err.message}`,
|
|
63
|
+
errorType: 'PrivateKeyError',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
client.on('ready', () => {
|
|
70
|
+
const session = new SSHSession(sessionId, client, options.host, options.username);
|
|
71
|
+
this.sessions.set(sessionId, session);
|
|
72
|
+
|
|
73
|
+
resolve({
|
|
74
|
+
success: true,
|
|
75
|
+
sessionId,
|
|
76
|
+
host: options.host,
|
|
77
|
+
port,
|
|
78
|
+
username: options.username,
|
|
79
|
+
message: `Successfully connected to ${options.host}:${port}`,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client.on('error', (err) => {
|
|
84
|
+
resolve({
|
|
85
|
+
success: false,
|
|
86
|
+
error: err.message,
|
|
87
|
+
errorType: 'ConnectionError',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
client.connect(config);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
execCommand(sessionId, command) {
|
|
96
|
+
const session = this.sessions.get(sessionId);
|
|
97
|
+
|
|
98
|
+
if (!session) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: `Session ${sessionId} not found`,
|
|
102
|
+
errorType: 'SessionNotFound',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!session.connected) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Session ${sessionId} is not connected`,
|
|
110
|
+
errorType: 'SessionNotConnected',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
session.client.exec(command, (err, stream) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
resolve({
|
|
118
|
+
success: false,
|
|
119
|
+
error: err.message,
|
|
120
|
+
errorType: 'ExecError',
|
|
121
|
+
sessionId,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let stderr = '';
|
|
128
|
+
|
|
129
|
+
stream.on('close', (code) => {
|
|
130
|
+
resolve({
|
|
131
|
+
success: true,
|
|
132
|
+
sessionId,
|
|
133
|
+
command,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr,
|
|
136
|
+
exitCode: code,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
stream.on('data', (data) => {
|
|
141
|
+
stdout += data.toString();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
stream.stderr.on('data', (data) => {
|
|
145
|
+
stderr += data.toString();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
disconnect(sessionId) {
|
|
152
|
+
const session = this.sessions.get(sessionId);
|
|
153
|
+
|
|
154
|
+
if (!session) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: `Session ${sessionId} not found`,
|
|
158
|
+
errorType: 'SessionNotFound',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
session.client.end();
|
|
164
|
+
session.connected = false;
|
|
165
|
+
this.sessions.delete(sessionId);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
sessionId,
|
|
170
|
+
message: 'Disconnected successfully',
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: err.message,
|
|
176
|
+
errorType: 'DisconnectError',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
listSessions() {
|
|
182
|
+
const sessions = [];
|
|
183
|
+
|
|
184
|
+
this.sessions.forEach((session) => {
|
|
185
|
+
sessions.push({
|
|
186
|
+
sessionId: session.sessionId,
|
|
187
|
+
host: session.host,
|
|
188
|
+
username: session.username,
|
|
189
|
+
connected: session.connected,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
sessions,
|
|
196
|
+
count: sessions.length,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sshManager = new SSHConnectionManager();
|
|
202
|
+
|
|
203
|
+
const tools = [
|
|
204
|
+
{
|
|
205
|
+
name: 'ssh_connect',
|
|
206
|
+
description: 'Establish an SSH connection to a remote server',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
host: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Server hostname or IP address',
|
|
213
|
+
},
|
|
214
|
+
port: {
|
|
215
|
+
type: 'number',
|
|
216
|
+
description: 'SSH port number',
|
|
217
|
+
default: 22,
|
|
218
|
+
},
|
|
219
|
+
username: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'SSH username',
|
|
222
|
+
},
|
|
223
|
+
auth_type: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
enum: ['password', 'public_key'],
|
|
226
|
+
description: 'Authentication type',
|
|
227
|
+
},
|
|
228
|
+
password: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Password (required if auth_type is password)',
|
|
231
|
+
},
|
|
232
|
+
private_key_path: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'Path to private key file (required if auth_type is public_key)',
|
|
235
|
+
},
|
|
236
|
+
passphrase: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'Passphrase for private key (optional)',
|
|
239
|
+
},
|
|
240
|
+
timeout: {
|
|
241
|
+
type: 'number',
|
|
242
|
+
description: 'Connection timeout in seconds',
|
|
243
|
+
default: 10,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ['host', 'username', 'auth_type'],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'ssh_exec',
|
|
251
|
+
description: 'Execute a command on an active SSH session',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
session_id: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'The SSH session ID from ssh_connect',
|
|
258
|
+
},
|
|
259
|
+
command: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
description: 'The command to execute',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required: ['session_id', 'command'],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'ssh_disconnect',
|
|
269
|
+
description: 'Disconnect an SSH session',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
session_id: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'The SSH session ID to disconnect',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
required: ['session_id'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'ssh_list_sessions',
|
|
283
|
+
description: 'List all active SSH sessions',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
function send(message) {
|
|
292
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let buffer = '';
|
|
296
|
+
|
|
297
|
+
process.stdin.setEncoding('utf8');
|
|
298
|
+
|
|
299
|
+
process.stdin.on('data', (chunk) => {
|
|
300
|
+
buffer += chunk;
|
|
301
|
+
let newlineIndex;
|
|
302
|
+
|
|
303
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
304
|
+
const line = buffer.slice(0, newlineIndex);
|
|
305
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
306
|
+
|
|
307
|
+
if (line.trim()) {
|
|
308
|
+
try {
|
|
309
|
+
const request = JSON.parse(line);
|
|
310
|
+
handleRequest(request);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error('Parse error:', err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function handleRequest(request) {
|
|
319
|
+
const { id, method, params } = request;
|
|
320
|
+
|
|
321
|
+
if (method === 'initialize') {
|
|
322
|
+
send({
|
|
323
|
+
jsonrpc: '2.0',
|
|
324
|
+
id,
|
|
325
|
+
result: {
|
|
326
|
+
protocolVersion: '2024-11-05',
|
|
327
|
+
capabilities: { tools: {} },
|
|
328
|
+
serverInfo: {
|
|
329
|
+
name: 'ssh-mcp-server',
|
|
330
|
+
version: '1.0.0',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (method === 'notifications/initialized') {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (method === 'tools/list') {
|
|
342
|
+
send({
|
|
343
|
+
jsonrpc: '2.0',
|
|
344
|
+
id,
|
|
345
|
+
result: { tools },
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (method === 'tools/call') {
|
|
351
|
+
const { name, arguments: args } = params;
|
|
352
|
+
|
|
353
|
+
// Handle nested arguments (stringified JSON)
|
|
354
|
+
let toolArgs = args;
|
|
355
|
+
if (typeof args === 'string') {
|
|
356
|
+
try {
|
|
357
|
+
toolArgs = JSON.parse(args);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
toolArgs = {};
|
|
360
|
+
}
|
|
361
|
+
} else if (args && typeof args.arguments === 'string') {
|
|
362
|
+
try {
|
|
363
|
+
toolArgs = JSON.parse(args.arguments);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
toolArgs = args;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (name === 'ssh_connect') {
|
|
370
|
+
sshManager.connect({
|
|
371
|
+
host: toolArgs.host,
|
|
372
|
+
port: toolArgs.port,
|
|
373
|
+
username: toolArgs.username,
|
|
374
|
+
authType: toolArgs.auth_type,
|
|
375
|
+
password: toolArgs.password,
|
|
376
|
+
privateKeyPath: toolArgs.private_key_path,
|
|
377
|
+
passphrase: toolArgs.passphrase,
|
|
378
|
+
timeout: toolArgs.timeout,
|
|
379
|
+
}).then((result) => {
|
|
380
|
+
send({
|
|
381
|
+
jsonrpc: '2.0',
|
|
382
|
+
id,
|
|
383
|
+
result: {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: 'text',
|
|
387
|
+
text: JSON.stringify(result),
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (name === 'ssh_exec') {
|
|
397
|
+
const execResult = sshManager.execCommand(toolArgs.session_id, toolArgs.command);
|
|
398
|
+
if (execResult && typeof execResult.then === 'function') {
|
|
399
|
+
execResult.then((result) => {
|
|
400
|
+
send({
|
|
401
|
+
jsonrpc: '2.0',
|
|
402
|
+
id,
|
|
403
|
+
result: {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: 'text',
|
|
407
|
+
text: JSON.stringify(result),
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}).catch((err) => {
|
|
413
|
+
send({
|
|
414
|
+
jsonrpc: '2.0',
|
|
415
|
+
id,
|
|
416
|
+
result: {
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: 'text',
|
|
420
|
+
text: JSON.stringify({ success: false, error: err.message }),
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
send({
|
|
428
|
+
jsonrpc: '2.0',
|
|
429
|
+
id,
|
|
430
|
+
result: {
|
|
431
|
+
content: [
|
|
432
|
+
{
|
|
433
|
+
type: 'text',
|
|
434
|
+
text: JSON.stringify(execResult),
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (name === 'ssh_disconnect') {
|
|
444
|
+
const result = sshManager.disconnect(toolArgs.session_id);
|
|
445
|
+
send({
|
|
446
|
+
jsonrpc: '2.0',
|
|
447
|
+
id,
|
|
448
|
+
result: {
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: 'text',
|
|
452
|
+
text: JSON.stringify(result),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (name === 'ssh_list_sessions') {
|
|
461
|
+
const result = sshManager.listSessions();
|
|
462
|
+
send({
|
|
463
|
+
jsonrpc: '2.0',
|
|
464
|
+
id,
|
|
465
|
+
result: {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: 'text',
|
|
469
|
+
text: JSON.stringify(result),
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
send({
|
|
478
|
+
jsonrpc: '2.0',
|
|
479
|
+
id,
|
|
480
|
+
result: {
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }),
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
send({
|
|
493
|
+
jsonrpc: '2.0',
|
|
494
|
+
id,
|
|
495
|
+
error: {
|
|
496
|
+
code: -32601,
|
|
497
|
+
message: 'Method not found',
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
package/mcp.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-ssh-server-tool",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for SSH connections and remote command execution",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-ssh-server-tool": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["mcp", "ssh", "server", "mcp-server", "remote-execution"],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": ""
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ssh2": "^1.15.0",
|
|
23
|
+
"uuid": "^9.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"preferGlobal": true
|
|
29
|
+
}
|
package/ssh_manager.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
class SSHSession {
|
|
6
|
+
constructor(sessionId, client, host, username) {
|
|
7
|
+
this.sessionId = sessionId;
|
|
8
|
+
this.client = client;
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.username = username;
|
|
11
|
+
this.connected = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SSHConnectionManager {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.sessions = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connect(options) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const client = new Client();
|
|
23
|
+
const sessionId = uuidv4();
|
|
24
|
+
const port = options.port || 22;
|
|
25
|
+
const timeout = (options.timeout || 10) * 1000;
|
|
26
|
+
|
|
27
|
+
const config = {
|
|
28
|
+
host: options.host,
|
|
29
|
+
port: port,
|
|
30
|
+
username: options.username,
|
|
31
|
+
timeout: timeout,
|
|
32
|
+
readyTimeout: timeout,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (options.authType === 'password') {
|
|
36
|
+
if (!options.password) {
|
|
37
|
+
resolve({
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Password is required for password authentication',
|
|
40
|
+
errorType: 'MissingPassword',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
config.password = options.password;
|
|
45
|
+
} else if (options.authType === 'public_key') {
|
|
46
|
+
if (!options.privateKeyPath) {
|
|
47
|
+
resolve({
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Private key path is required for public key authentication',
|
|
50
|
+
errorType: 'MissingPrivateKey',
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
config.privateKey = fs.readFileSync(options.privateKeyPath);
|
|
56
|
+
if (options.passphrase) {
|
|
57
|
+
config.passphrase = options.passphrase;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
resolve({
|
|
61
|
+
success: false,
|
|
62
|
+
error: `Failed to read private key: ${err.message}`,
|
|
63
|
+
errorType: 'PrivateKeyError',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
client.on('ready', () => {
|
|
70
|
+
const session = new SSHSession(sessionId, client, options.host, options.username);
|
|
71
|
+
this.sessions.set(sessionId, session);
|
|
72
|
+
|
|
73
|
+
resolve({
|
|
74
|
+
success: true,
|
|
75
|
+
sessionId,
|
|
76
|
+
host: options.host,
|
|
77
|
+
port,
|
|
78
|
+
username: options.username,
|
|
79
|
+
message: `Successfully connected to ${options.host}:${port}`,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client.on('error', (err) => {
|
|
84
|
+
resolve({
|
|
85
|
+
success: false,
|
|
86
|
+
error: err.message,
|
|
87
|
+
errorType: 'ConnectionError',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
client.connect(config);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
execCommand(sessionId, command) {
|
|
96
|
+
const session = this.sessions.get(sessionId);
|
|
97
|
+
|
|
98
|
+
if (!session) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: `Session ${sessionId} not found`,
|
|
102
|
+
errorType: 'SessionNotFound',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!session.connected) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Session ${sessionId} is not connected`,
|
|
110
|
+
errorType: 'SessionNotConnected',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
session.client.exec(command, (err, stream) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
resolve({
|
|
118
|
+
success: false,
|
|
119
|
+
error: err.message,
|
|
120
|
+
errorType: 'ExecError',
|
|
121
|
+
sessionId,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let stderr = '';
|
|
128
|
+
|
|
129
|
+
stream.on('close', (code) => {
|
|
130
|
+
resolve({
|
|
131
|
+
success: true,
|
|
132
|
+
sessionId,
|
|
133
|
+
command,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr,
|
|
136
|
+
exitCode: code,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
stream.on('data', (data) => {
|
|
141
|
+
stdout += data.toString();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
stream.stderr.on('data', (data) => {
|
|
145
|
+
stderr += data.toString();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
disconnect(sessionId) {
|
|
152
|
+
const session = this.sessions.get(sessionId);
|
|
153
|
+
|
|
154
|
+
if (!session) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: `Session ${sessionId} not found`,
|
|
158
|
+
errorType: 'SessionNotFound',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
session.client.end();
|
|
164
|
+
session.connected = false;
|
|
165
|
+
this.sessions.delete(sessionId);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
sessionId,
|
|
170
|
+
message: 'Disconnected successfully',
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: err.message,
|
|
176
|
+
errorType: 'DisconnectError',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
listSessions() {
|
|
182
|
+
const sessions = [];
|
|
183
|
+
|
|
184
|
+
this.sessions.forEach((session) => {
|
|
185
|
+
sessions.push({
|
|
186
|
+
sessionId: session.sessionId,
|
|
187
|
+
host: session.host,
|
|
188
|
+
username: session.username,
|
|
189
|
+
connected: session.connected,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
sessions,
|
|
196
|
+
count: sessions.length,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|