mcp-ssh-server-tool 1.0.6 → 2.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/index.js +139 -478
- package/package.json +2 -3
- package/ssh_manager.js +62 -99
package/index.js
CHANGED
|
@@ -1,352 +1,99 @@
|
|
|
1
|
-
import {
|
|
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
|
-
}
|
|
1
|
+
import { SSHConnectionManager } from './ssh_manager.js';
|
|
200
2
|
|
|
201
3
|
const sshManager = new SSHConnectionManager();
|
|
202
4
|
|
|
203
5
|
const tools = [
|
|
204
6
|
{
|
|
205
7
|
name: 'ssh_connect',
|
|
206
|
-
description: 'Establish an SSH connection
|
|
8
|
+
description: 'Establish an SSH connection. Returns session_id for subsequent commands.',
|
|
207
9
|
inputSchema: {
|
|
208
10
|
type: 'object',
|
|
209
11
|
properties: {
|
|
210
|
-
host: {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
},
|
|
12
|
+
host: { type: 'string', description: 'Server hostname or IP' },
|
|
13
|
+
port: { type: 'number', description: 'SSH port (default: 22)', default: 22 },
|
|
14
|
+
username: { type: 'string', description: 'SSH username' },
|
|
15
|
+
password: { type: 'string', description: 'Password (for password auth)' },
|
|
16
|
+
auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
|
|
17
|
+
private_key_path: { type: 'string', description: 'Path to private key' },
|
|
18
|
+
passphrase: { type: 'string', description: 'Passphrase for private key' },
|
|
19
|
+
timeout: { type: 'number', description: 'Timeout in seconds (default: 10)', default: 10 },
|
|
245
20
|
},
|
|
246
|
-
required: ['host', 'username'
|
|
21
|
+
required: ['host', 'username'],
|
|
247
22
|
},
|
|
248
23
|
},
|
|
249
24
|
{
|
|
250
25
|
name: 'ssh_exec',
|
|
251
|
-
description: 'Execute
|
|
26
|
+
description: 'Execute command(s) on SSH server. Supports direct mode (host/username) or session mode (session_id).',
|
|
252
27
|
inputSchema: {
|
|
253
28
|
type: 'object',
|
|
254
29
|
properties: {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
},
|
|
267
|
-
port: {
|
|
268
|
-
type: 'number',
|
|
269
|
-
description: 'SSH port (default: 22)',
|
|
270
|
-
default: 22,
|
|
271
|
-
},
|
|
272
|
-
username: {
|
|
273
|
-
type: 'string',
|
|
274
|
-
description: 'SSH username (optional)',
|
|
275
|
-
},
|
|
276
|
-
password: {
|
|
277
|
-
type: 'string',
|
|
278
|
-
description: 'SSH password (optional)',
|
|
279
|
-
},
|
|
280
|
-
auth_type: {
|
|
281
|
-
type: 'string',
|
|
282
|
-
enum: ['password', 'public_key'],
|
|
283
|
-
description: 'Authentication type (default: password)',
|
|
284
|
-
default: 'password',
|
|
285
|
-
},
|
|
286
|
-
private_key_path: {
|
|
287
|
-
type: 'string',
|
|
288
|
-
description: 'Private key path (for public_key auth)',
|
|
289
|
-
},
|
|
290
|
-
passphrase: {
|
|
291
|
-
type: 'string',
|
|
292
|
-
description: 'Passphrase for private key',
|
|
293
|
-
},
|
|
30
|
+
host: { type: 'string', description: 'Server hostname (use for direct execution)' },
|
|
31
|
+
port: { type: 'number', description: 'SSH port', default: 22 },
|
|
32
|
+
username: { type: 'string', description: 'SSH username' },
|
|
33
|
+
password: { type: 'string', description: 'Password' },
|
|
34
|
+
auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
|
|
35
|
+
private_key_path: { type: 'string', description: 'Path to private key' },
|
|
36
|
+
passphrase: { type: 'string', description: 'Passphrase for private key' },
|
|
37
|
+
timeout: { type: 'number', description: 'Timeout in seconds', default: 10 },
|
|
38
|
+
session_id: { type: 'string', description: 'Session ID from ssh_connect (use for session mode)' },
|
|
39
|
+
command: { type: 'string', description: 'Command to execute' },
|
|
40
|
+
commands: { type: 'array', items: { type: 'string' }, description: 'Multiple commands to execute sequentially' },
|
|
294
41
|
},
|
|
295
|
-
required: ['command'],
|
|
296
42
|
},
|
|
297
43
|
},
|
|
298
44
|
{
|
|
299
45
|
name: 'ssh_disconnect',
|
|
300
|
-
description: '
|
|
46
|
+
description: 'Close an SSH session',
|
|
301
47
|
inputSchema: {
|
|
302
48
|
type: 'object',
|
|
303
49
|
properties: {
|
|
304
|
-
session_id: {
|
|
305
|
-
type: 'string',
|
|
306
|
-
description: 'The SSH session ID to disconnect',
|
|
307
|
-
},
|
|
50
|
+
session_id: { type: 'string', description: 'Session ID to disconnect' },
|
|
308
51
|
},
|
|
309
52
|
required: ['session_id'],
|
|
310
53
|
},
|
|
311
54
|
},
|
|
312
55
|
{
|
|
313
|
-
name: '
|
|
314
|
-
description: '
|
|
56
|
+
name: 'ssh_test',
|
|
57
|
+
description: 'Quick connection test. Connects, executes a test command, and disconnects.',
|
|
315
58
|
inputSchema: {
|
|
316
59
|
type: 'object',
|
|
317
|
-
properties: {
|
|
60
|
+
properties: {
|
|
61
|
+
host: { type: 'string', description: 'Server hostname' },
|
|
62
|
+
port: { type: 'number', description: 'SSH port', default: 22 },
|
|
63
|
+
username: { type: 'string', description: 'SSH username' },
|
|
64
|
+
password: { type: 'string', description: 'Password' },
|
|
65
|
+
auth_type: { type: 'string', enum: ['password', 'public_key'], description: 'Auth type' },
|
|
66
|
+
private_key_path: { type: 'string', description: 'Path to private key' },
|
|
67
|
+
passphrase: { type: 'string', description: 'Passphrase' },
|
|
68
|
+
timeout: { type: 'number', description: 'Timeout in seconds', default: 10 },
|
|
69
|
+
test_command: { type: 'string', description: 'Test command (default: echo test)', default: 'echo test' },
|
|
70
|
+
},
|
|
71
|
+
required: ['host', 'username'],
|
|
318
72
|
},
|
|
319
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: 'ssh_list_sessions',
|
|
76
|
+
description: 'List active SSH sessions',
|
|
77
|
+
inputSchema: { type: 'object', properties: {} },
|
|
78
|
+
},
|
|
320
79
|
];
|
|
321
80
|
|
|
322
81
|
function send(message) {
|
|
323
82
|
process.stdout.write(JSON.stringify(message) + '\n');
|
|
324
83
|
}
|
|
325
84
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
process.stdin.on('data', (chunk) => {
|
|
331
|
-
buffer += chunk;
|
|
332
|
-
let newlineIndex;
|
|
333
|
-
|
|
334
|
-
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
335
|
-
const line = buffer.slice(0, newlineIndex);
|
|
336
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
337
|
-
|
|
338
|
-
if (line.trim()) {
|
|
339
|
-
try {
|
|
340
|
-
const request = JSON.parse(line);
|
|
341
|
-
handleRequest(request);
|
|
342
|
-
} catch (err) {
|
|
343
|
-
console.error('Parse error:', err);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
85
|
+
function parseArgs(args) {
|
|
86
|
+
if (!args) return {};
|
|
87
|
+
if (typeof args === 'string') {
|
|
88
|
+
try { return JSON.parse(args); } catch { return {}; }
|
|
346
89
|
}
|
|
347
|
-
|
|
90
|
+
if (typeof args.arguments === 'string') {
|
|
91
|
+
try { return JSON.parse(args.arguments); } catch { return args; }
|
|
92
|
+
}
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
348
95
|
|
|
349
|
-
function handleRequest(request) {
|
|
96
|
+
async function handleRequest(request) {
|
|
350
97
|
const { id, method, params } = request;
|
|
351
98
|
|
|
352
99
|
if (method === 'initialize') {
|
|
@@ -356,198 +103,122 @@ function handleRequest(request) {
|
|
|
356
103
|
result: {
|
|
357
104
|
protocolVersion: '2024-11-05',
|
|
358
105
|
capabilities: { tools: {} },
|
|
359
|
-
serverInfo: {
|
|
360
|
-
name: 'ssh-mcp-server',
|
|
361
|
-
version: '1.0.0',
|
|
362
|
-
},
|
|
106
|
+
serverInfo: { name: 'ssh-mcp-server', version: '1.0.0' },
|
|
363
107
|
},
|
|
364
108
|
});
|
|
365
109
|
return;
|
|
366
110
|
}
|
|
367
111
|
|
|
368
|
-
if (method === 'notifications/initialized')
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
112
|
+
if (method === 'notifications/initialized') return;
|
|
371
113
|
|
|
372
114
|
if (method === 'tools/list') {
|
|
373
|
-
send({
|
|
374
|
-
jsonrpc: '2.0',
|
|
375
|
-
id,
|
|
376
|
-
result: { tools },
|
|
377
|
-
});
|
|
115
|
+
send({ jsonrpc: '2.0', id, result: { tools } });
|
|
378
116
|
return;
|
|
379
117
|
}
|
|
380
118
|
|
|
381
119
|
if (method === 'tools/call') {
|
|
382
120
|
const { name, arguments: args } = params;
|
|
383
|
-
|
|
384
|
-
// Handle nested arguments (stringified JSON)
|
|
385
|
-
let toolArgs = args;
|
|
386
|
-
if (typeof args === 'string') {
|
|
387
|
-
try {
|
|
388
|
-
toolArgs = JSON.parse(args);
|
|
389
|
-
} catch (e) {
|
|
390
|
-
toolArgs = {};
|
|
391
|
-
}
|
|
392
|
-
} else if (args && typeof args.arguments === 'string') {
|
|
393
|
-
try {
|
|
394
|
-
toolArgs = JSON.parse(args.arguments);
|
|
395
|
-
} catch (e) {
|
|
396
|
-
toolArgs = args;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
121
|
+
const toolArgs = parseArgs(args);
|
|
399
122
|
|
|
400
123
|
if (name === 'ssh_connect') {
|
|
401
|
-
sshManager.connect({
|
|
124
|
+
const result = await sshManager.connect({
|
|
402
125
|
host: toolArgs.host,
|
|
403
126
|
port: toolArgs.port,
|
|
404
127
|
username: toolArgs.username,
|
|
405
|
-
authType: toolArgs.auth_type,
|
|
406
128
|
password: toolArgs.password,
|
|
407
129
|
privateKeyPath: toolArgs.private_key_path,
|
|
408
130
|
passphrase: toolArgs.passphrase,
|
|
409
131
|
timeout: toolArgs.timeout,
|
|
410
|
-
}).then((result) => {
|
|
411
|
-
send({
|
|
412
|
-
jsonrpc: '2.0',
|
|
413
|
-
id,
|
|
414
|
-
result: {
|
|
415
|
-
content: [
|
|
416
|
-
{
|
|
417
|
-
type: 'text',
|
|
418
|
-
text: JSON.stringify(result),
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
132
|
});
|
|
133
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
|
|
424
134
|
return;
|
|
425
135
|
}
|
|
426
136
|
|
|
427
137
|
if (name === 'ssh_exec') {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
send({
|
|
434
|
-
jsonrpc: '2.0',
|
|
435
|
-
id,
|
|
436
|
-
result: {
|
|
437
|
-
content: [
|
|
438
|
-
{
|
|
439
|
-
type: 'text',
|
|
440
|
-
text: JSON.stringify(result),
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
}).catch((err) => {
|
|
446
|
-
send({
|
|
447
|
-
jsonrpc: '2.0',
|
|
448
|
-
id,
|
|
449
|
-
result: {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: 'text',
|
|
453
|
-
text: JSON.stringify({ success: false, error: err.message }),
|
|
454
|
-
},
|
|
455
|
-
],
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
} else {
|
|
460
|
-
send({
|
|
461
|
-
jsonrpc: '2.0',
|
|
462
|
-
id,
|
|
463
|
-
result: {
|
|
464
|
-
content: [
|
|
465
|
-
{
|
|
466
|
-
type: 'text',
|
|
467
|
-
text: JSON.stringify(execResult),
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
}
|
|
138
|
+
const hasSession = toolArgs.session_id;
|
|
139
|
+
const hasAuth = toolArgs.host && toolArgs.username;
|
|
140
|
+
|
|
141
|
+
if (!hasSession && !hasAuth) {
|
|
142
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Either session_id or host/username required' }) }] } });
|
|
473
143
|
return;
|
|
474
144
|
}
|
|
475
145
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const connResult = await sshManager.connect({
|
|
146
|
+
if (hasAuth && !hasSession) {
|
|
147
|
+
const conn = await sshManager.connect({
|
|
479
148
|
host: toolArgs.host,
|
|
480
149
|
port: toolArgs.port,
|
|
481
150
|
username: toolArgs.username,
|
|
482
|
-
authType: toolArgs.auth_type || 'password',
|
|
483
151
|
password: toolArgs.password,
|
|
484
152
|
privateKeyPath: toolArgs.private_key_path,
|
|
485
153
|
passphrase: toolArgs.passphrase,
|
|
486
154
|
timeout: toolArgs.timeout,
|
|
487
155
|
});
|
|
488
156
|
|
|
489
|
-
if (!
|
|
490
|
-
send({
|
|
491
|
-
jsonrpc: '2.0',
|
|
492
|
-
id,
|
|
493
|
-
result: {
|
|
494
|
-
content: [
|
|
495
|
-
{
|
|
496
|
-
type: 'text',
|
|
497
|
-
text: JSON.stringify(connResult),
|
|
498
|
-
},
|
|
499
|
-
],
|
|
500
|
-
},
|
|
501
|
-
});
|
|
157
|
+
if (!conn.success) {
|
|
158
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
|
|
502
159
|
return;
|
|
503
160
|
}
|
|
504
161
|
|
|
505
|
-
|
|
506
|
-
|
|
162
|
+
let result;
|
|
163
|
+
if (toolArgs.commands) {
|
|
164
|
+
result = await sshManager.execCommands(conn.sessionId, toolArgs.commands);
|
|
165
|
+
} else if (toolArgs.command) {
|
|
166
|
+
result = await sshManager.execCommand(conn.sessionId, toolArgs.command);
|
|
167
|
+
}
|
|
507
168
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
id,
|
|
511
|
-
result: {
|
|
512
|
-
content: [
|
|
513
|
-
{
|
|
514
|
-
type: 'text',
|
|
515
|
-
text: JSON.stringify(execResult),
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
},
|
|
519
|
-
});
|
|
169
|
+
sshManager.disconnect(conn.sessionId);
|
|
170
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
|
|
520
171
|
return;
|
|
521
172
|
}
|
|
522
173
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
],
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
return;
|
|
174
|
+
if (hasSession) {
|
|
175
|
+
let result;
|
|
176
|
+
if (toolArgs.commands) {
|
|
177
|
+
result = await sshManager.execCommands(toolArgs.session_id, toolArgs.commands);
|
|
178
|
+
} else if (toolArgs.command) {
|
|
179
|
+
result = await sshManager.execCommand(toolArgs.session_id, toolArgs.command);
|
|
180
|
+
}
|
|
181
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
537
184
|
}
|
|
538
185
|
|
|
539
186
|
if (name === 'ssh_disconnect') {
|
|
540
187
|
const result = sshManager.disconnect(toolArgs.session_id);
|
|
188
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (name === 'ssh_test') {
|
|
193
|
+
const conn = await sshManager.connect({
|
|
194
|
+
host: toolArgs.host,
|
|
195
|
+
port: toolArgs.port,
|
|
196
|
+
username: toolArgs.username,
|
|
197
|
+
password: toolArgs.password,
|
|
198
|
+
privateKeyPath: toolArgs.private_key_path,
|
|
199
|
+
passphrase: toolArgs.passphrase,
|
|
200
|
+
timeout: toolArgs.timeout,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!conn.success) {
|
|
204
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(conn) }] } });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const execResult = await sshManager.execCommand(conn.sessionId, toolArgs.test_command || 'echo test');
|
|
209
|
+
sshManager.disconnect(conn.sessionId);
|
|
210
|
+
|
|
541
211
|
send({
|
|
542
212
|
jsonrpc: '2.0',
|
|
543
213
|
id,
|
|
544
214
|
result: {
|
|
545
|
-
content: [
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
215
|
+
content: [{
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: JSON.stringify({
|
|
218
|
+
connection: conn,
|
|
219
|
+
test: execResult,
|
|
220
|
+
}),
|
|
221
|
+
}],
|
|
551
222
|
},
|
|
552
223
|
});
|
|
553
224
|
return;
|
|
@@ -555,42 +226,32 @@ function handleRequest(request) {
|
|
|
555
226
|
|
|
556
227
|
if (name === 'ssh_list_sessions') {
|
|
557
228
|
const result = sshManager.listSessions();
|
|
558
|
-
send({
|
|
559
|
-
jsonrpc: '2.0',
|
|
560
|
-
id,
|
|
561
|
-
result: {
|
|
562
|
-
content: [
|
|
563
|
-
{
|
|
564
|
-
type: 'text',
|
|
565
|
-
text: JSON.stringify(result),
|
|
566
|
-
},
|
|
567
|
-
],
|
|
568
|
-
},
|
|
569
|
-
});
|
|
229
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
|
|
570
230
|
return;
|
|
571
231
|
}
|
|
572
232
|
|
|
573
|
-
send({
|
|
574
|
-
jsonrpc: '2.0',
|
|
575
|
-
id,
|
|
576
|
-
result: {
|
|
577
|
-
content: [
|
|
578
|
-
{
|
|
579
|
-
type: 'text',
|
|
580
|
-
text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }),
|
|
581
|
-
},
|
|
582
|
-
],
|
|
583
|
-
},
|
|
584
|
-
});
|
|
233
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Unknown tool: ${name}` }) }] } });
|
|
585
234
|
return;
|
|
586
235
|
}
|
|
587
236
|
|
|
588
|
-
send({
|
|
589
|
-
jsonrpc: '2.0',
|
|
590
|
-
id,
|
|
591
|
-
error: {
|
|
592
|
-
code: -32601,
|
|
593
|
-
message: 'Method not found',
|
|
594
|
-
},
|
|
595
|
-
});
|
|
237
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
596
238
|
}
|
|
239
|
+
|
|
240
|
+
let buffer = '';
|
|
241
|
+
process.stdin.setEncoding('utf8');
|
|
242
|
+
|
|
243
|
+
process.stdin.on('data', async (chunk) => {
|
|
244
|
+
buffer += chunk;
|
|
245
|
+
let newlineIndex;
|
|
246
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
247
|
+
const line = buffer.slice(0, newlineIndex);
|
|
248
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
249
|
+
if (line.trim()) {
|
|
250
|
+
try {
|
|
251
|
+
await handleRequest(JSON.parse(line));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error('Parse error:', err.message);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-ssh-server-tool",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MCP Server for SSH connections and remote command execution",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"url": ""
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"ssh2": "^1.15.0"
|
|
20
|
-
"uuid": "^9.0.0"
|
|
19
|
+
"ssh2": "^1.15.0"
|
|
21
20
|
},
|
|
22
21
|
"engines": {
|
|
23
22
|
"node": ">=18.0.0"
|
package/ssh_manager.js
CHANGED
|
@@ -1,86 +1,62 @@
|
|
|
1
1
|
import { Client } from 'ssh2';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
4
5
|
|
|
5
6
|
class SSHSession {
|
|
6
|
-
constructor(sessionId, client,
|
|
7
|
+
constructor(sessionId, client, config) {
|
|
7
8
|
this.sessionId = sessionId;
|
|
8
9
|
this.client = client;
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
-
this.connected = true;
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.createdAt = Date.now();
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
class SSHConnectionManager {
|
|
16
16
|
constructor() {
|
|
17
17
|
this.sessions = new Map();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
connect(options) {
|
|
20
|
+
async connect(options) {
|
|
21
21
|
return new Promise((resolve) => {
|
|
22
22
|
const client = new Client();
|
|
23
|
-
const sessionId =
|
|
24
|
-
const port = options.port || 22;
|
|
25
|
-
const timeout = (options.timeout || 10) * 1000;
|
|
26
|
-
|
|
23
|
+
const sessionId = randomUUID();
|
|
27
24
|
const config = {
|
|
28
25
|
host: options.host,
|
|
29
|
-
port: port,
|
|
26
|
+
port: options.port || 22,
|
|
30
27
|
username: options.username,
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
password: options.password,
|
|
29
|
+
privateKey: options.privateKeyPath ? this._readPrivateKey(options.privateKeyPath) : undefined,
|
|
30
|
+
passphrase: options.passphrase,
|
|
31
|
+
readyTimeout: (options.timeout || 10) * 1000,
|
|
32
|
+
keepaliveInterval: 0,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
client.end();
|
|
37
|
+
resolve({
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Connection timeout',
|
|
40
|
+
errorType: 'Timeout',
|
|
41
|
+
});
|
|
42
|
+
}, (options.timeout || 10) * 1000);
|
|
68
43
|
|
|
69
44
|
client.on('ready', () => {
|
|
70
|
-
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
const session = new SSHSession(sessionId, client, { host: config.host, port: config.port, username: config.username });
|
|
71
47
|
this.sessions.set(sessionId, session);
|
|
72
|
-
|
|
73
48
|
resolve({
|
|
74
49
|
success: true,
|
|
75
50
|
sessionId,
|
|
76
|
-
host:
|
|
77
|
-
port,
|
|
78
|
-
username:
|
|
79
|
-
message: `
|
|
51
|
+
host: config.host,
|
|
52
|
+
port: config.port,
|
|
53
|
+
username: config.username,
|
|
54
|
+
message: `Connected to ${config.host}:${config.port}`,
|
|
80
55
|
});
|
|
81
56
|
});
|
|
82
57
|
|
|
83
58
|
client.on('error', (err) => {
|
|
59
|
+
clearTimeout(timeout);
|
|
84
60
|
resolve({
|
|
85
61
|
success: false,
|
|
86
62
|
error: err.message,
|
|
@@ -92,9 +68,17 @@ export class SSHConnectionManager {
|
|
|
92
68
|
});
|
|
93
69
|
}
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
71
|
+
_readPrivateKey(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const resolvedPath = path.resolve(filePath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ''));
|
|
74
|
+
return fs.readFileSync(resolvedPath);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new Error(`Failed to read private key: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
97
79
|
|
|
80
|
+
async execCommand(sessionId, command) {
|
|
81
|
+
const session = this.sessions.get(sessionId);
|
|
98
82
|
if (!session) {
|
|
99
83
|
return {
|
|
100
84
|
success: false,
|
|
@@ -103,14 +87,6 @@ export class SSHConnectionManager {
|
|
|
103
87
|
};
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
if (!session.connected) {
|
|
107
|
-
return {
|
|
108
|
-
success: false,
|
|
109
|
-
error: `Session ${sessionId} is not connected`,
|
|
110
|
-
errorType: 'SessionNotConnected',
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
90
|
return new Promise((resolve) => {
|
|
115
91
|
session.client.exec(command, (err, stream) => {
|
|
116
92
|
if (err) {
|
|
@@ -137,20 +113,24 @@ export class SSHConnectionManager {
|
|
|
137
113
|
});
|
|
138
114
|
});
|
|
139
115
|
|
|
140
|
-
stream.on('data', (data) => {
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
stream.stderr.on('data', (data) => {
|
|
145
|
-
stderr += data.toString();
|
|
146
|
-
});
|
|
116
|
+
stream.on('data', (data) => { stdout += data.toString(); });
|
|
117
|
+
stream.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
147
118
|
});
|
|
148
119
|
});
|
|
149
120
|
}
|
|
150
121
|
|
|
122
|
+
async execCommands(sessionId, commands) {
|
|
123
|
+
const results = [];
|
|
124
|
+
for (const cmd of commands) {
|
|
125
|
+
const result = await this.execCommand(sessionId, cmd);
|
|
126
|
+
results.push(result);
|
|
127
|
+
if (!result.success) break;
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
151
132
|
disconnect(sessionId) {
|
|
152
133
|
const session = this.sessions.get(sessionId);
|
|
153
|
-
|
|
154
134
|
if (!session) {
|
|
155
135
|
return {
|
|
156
136
|
success: false,
|
|
@@ -161,39 +141,22 @@ export class SSHConnectionManager {
|
|
|
161
141
|
|
|
162
142
|
try {
|
|
163
143
|
session.client.end();
|
|
164
|
-
session.connected = false;
|
|
165
144
|
this.sessions.delete(sessionId);
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
success: true,
|
|
169
|
-
sessionId,
|
|
170
|
-
message: 'Disconnected successfully',
|
|
171
|
-
};
|
|
145
|
+
return { success: true, sessionId, message: 'Disconnected' };
|
|
172
146
|
} catch (err) {
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
error: err.message,
|
|
176
|
-
errorType: 'DisconnectError',
|
|
177
|
-
};
|
|
147
|
+
return { success: false, error: err.message, errorType: 'DisconnectError' };
|
|
178
148
|
}
|
|
179
149
|
}
|
|
180
150
|
|
|
181
151
|
listSessions() {
|
|
182
|
-
const sessions =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
connected: session.connected,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
success: true,
|
|
195
|
-
sessions,
|
|
196
|
-
count: sessions.length,
|
|
197
|
-
};
|
|
152
|
+
const sessions = Array.from(this.sessions.values()).map(s => ({
|
|
153
|
+
sessionId: s.sessionId,
|
|
154
|
+
host: s.config.host,
|
|
155
|
+
username: s.config.username,
|
|
156
|
+
createdAt: s.createdAt,
|
|
157
|
+
}));
|
|
158
|
+
return { success: true, sessions, count: sessions.length };
|
|
198
159
|
}
|
|
199
160
|
}
|
|
161
|
+
|
|
162
|
+
export { SSHConnectionManager };
|