ssh-mcp 1.2.2 → 1.3.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 +18 -0
- package/build/index.js +247 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
- **Parameters:**
|
|
46
46
|
- `command` (required): Shell command to execute on the remote SSH server
|
|
47
47
|
- **Timeout Configuration:**
|
|
48
|
+
|
|
49
|
+
- `sudo-exec`: Execute a shell command with sudo elevation
|
|
50
|
+
- **Parameters:**
|
|
51
|
+
- `command` (required): Shell command to execute as root using sudo
|
|
52
|
+
- **Notes:**
|
|
53
|
+
- Requires `--sudoPassword` to be set for password-protected sudo
|
|
54
|
+
- Can be disabled by passing the `--disableSudo` flag at startup if sudo access is not needed or not available
|
|
55
|
+
- For persistent root access, consider using `--suPassword` instead which establishes a root shell
|
|
56
|
+
- Tool will not be available at all if server is started with `--disableSudo`
|
|
57
|
+
- **Timeout Configuration:**
|
|
48
58
|
- Timeout is configured via command line argument `--timeout` (in milliseconds)
|
|
49
59
|
- Default timeout: 60000ms (1 minute)
|
|
50
60
|
- When a command times out, the server automatically attempts to abort the running process before closing the connection
|
|
@@ -77,8 +87,11 @@ You can configure your IDE or LLM like Cursor, Windsurf, Claude Desktop to use t
|
|
|
77
87
|
- `port`: SSH port (default: 22)
|
|
78
88
|
- `password`: SSH password (or use `key` for key-based auth)
|
|
79
89
|
- `key`: Path to private SSH key
|
|
90
|
+
- `sudoPassword`: Password for sudo elevation (when executing commands with sudo)
|
|
91
|
+
- `suPassword`: Password for su elevation (when you need a persistent root shell)
|
|
80
92
|
- `timeout`: Command execution timeout in milliseconds (default: 60000ms = 1 minute)
|
|
81
93
|
- `maxChars`: Maximum allowed characters for the `command` input (default: 1000). Use `none` or `0` to disable the limit.
|
|
94
|
+
- `disableSudo`: Flag to disable the `sudo-exec` tool completely. Useful when sudo access is not needed or not available.
|
|
82
95
|
|
|
83
96
|
|
|
84
97
|
```commandline
|
|
@@ -130,6 +143,11 @@ claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=example.com
|
|
|
130
143
|
claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=192.168.1.100 --user=admin --password=your_password --timeout=120000 --maxChars=none
|
|
131
144
|
```
|
|
132
145
|
|
|
146
|
+
**With Sudo and Su Support:**
|
|
147
|
+
```bash
|
|
148
|
+
claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=192.168.1.100 --user=admin --password=your_password --sudoPassword=sudo_pass --suPassword=root_pass
|
|
149
|
+
```
|
|
150
|
+
|
|
133
151
|
**Installation Scopes:**
|
|
134
152
|
|
|
135
153
|
You can specify the scope when adding the server:
|
package/build/index.js
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { Client
|
|
4
|
+
import { Client } from 'ssh2';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
// Example usage: node build/index.js --host=1.2.3.4 --port=22 --user=root --password=pass --key=path/to/key --timeout=5000
|
|
7
|
+
// Example usage: node build/index.js --host=1.2.3.4 --port=22 --user=root --password=pass --key=path/to/key --timeout=5000 --disableSudo
|
|
8
8
|
function parseArgv() {
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const config = {};
|
|
11
11
|
for (const arg of args) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (arg.startsWith('--')) {
|
|
13
|
+
const equalIndex = arg.indexOf('=');
|
|
14
|
+
if (equalIndex === -1) {
|
|
15
|
+
// Flag without value
|
|
16
|
+
config[arg.slice(2)] = null;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Key=value pair
|
|
20
|
+
config[arg.slice(2, equalIndex)] = arg.slice(equalIndex + 1);
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
return config;
|
|
18
25
|
}
|
|
26
|
+
const isTestMode = process.env.SSH_MCP_TEST === '1';
|
|
19
27
|
const isCliEnabled = process.env.SSH_MCP_DISABLE_MAIN !== '1';
|
|
20
|
-
const argvConfig = isCliEnabled ? parseArgv() : {};
|
|
28
|
+
const argvConfig = (isCliEnabled || isTestMode) ? parseArgv() : {};
|
|
21
29
|
const HOST = argvConfig.host;
|
|
22
30
|
const PORT = argvConfig.port ? parseInt(argvConfig.port) : 22;
|
|
23
31
|
const USER = argvConfig.user;
|
|
24
32
|
const PASSWORD = argvConfig.password;
|
|
33
|
+
const SUPASSWORD = argvConfig.suPassword;
|
|
34
|
+
const SUDOPASSWORD = argvConfig.sudoPassword;
|
|
35
|
+
const DISABLE_SUDO = argvConfig.disableSudo !== undefined;
|
|
25
36
|
const KEY = argvConfig.key;
|
|
26
37
|
const DEFAULT_TIMEOUT = argvConfig.timeout ? parseInt(argvConfig.timeout) : 60000; // 60 seconds default timeout
|
|
27
38
|
// Max characters configuration:
|
|
@@ -75,17 +86,27 @@ export function sanitizeCommand(command) {
|
|
|
75
86
|
}
|
|
76
87
|
return trimmedCommand;
|
|
77
88
|
}
|
|
89
|
+
function sanitizePassword(password) {
|
|
90
|
+
if (typeof password !== 'string')
|
|
91
|
+
return undefined;
|
|
92
|
+
// minimal check, do not log or modify content
|
|
93
|
+
if (password.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
return password;
|
|
96
|
+
}
|
|
78
97
|
// Escape command for use in shell contexts (like pkill)
|
|
79
98
|
export function escapeCommandForShell(command) {
|
|
80
99
|
// Replace single quotes with escaped single quotes
|
|
81
100
|
return command.replace(/'/g, "'\"'\"'");
|
|
82
101
|
}
|
|
83
|
-
// SSH Connection Manager to maintain persistent connection
|
|
84
102
|
export class SSHConnectionManager {
|
|
85
103
|
conn = null;
|
|
86
|
-
sshConfig
|
|
104
|
+
sshConfig;
|
|
87
105
|
isConnecting = false;
|
|
88
106
|
connectionPromise = null;
|
|
107
|
+
suShell = null; // Store the elevated shell session
|
|
108
|
+
suPromise = null;
|
|
109
|
+
isElevated = false; // Track if we're in su mode
|
|
89
110
|
constructor(config) {
|
|
90
111
|
this.sshConfig = config;
|
|
91
112
|
}
|
|
@@ -98,7 +119,7 @@ export class SSHConnectionManager {
|
|
|
98
119
|
}
|
|
99
120
|
this.isConnecting = true;
|
|
100
121
|
this.connectionPromise = new Promise((resolve, reject) => {
|
|
101
|
-
this.conn = new
|
|
122
|
+
this.conn = new Client();
|
|
102
123
|
const timeoutId = setTimeout(() => {
|
|
103
124
|
this.conn?.end();
|
|
104
125
|
this.conn = null;
|
|
@@ -106,10 +127,22 @@ export class SSHConnectionManager {
|
|
|
106
127
|
this.connectionPromise = null;
|
|
107
128
|
reject(new McpError(ErrorCode.InternalError, 'SSH connection timeout'));
|
|
108
129
|
}, 30000); // 30 seconds connection timeout
|
|
109
|
-
this.conn.on('ready', () => {
|
|
130
|
+
this.conn.on('ready', async () => {
|
|
110
131
|
clearTimeout(timeoutId);
|
|
111
132
|
this.isConnecting = false;
|
|
112
133
|
console.error('SSH connection established');
|
|
134
|
+
// If suPassword is provided, attempt to elevate to su shell in background
|
|
135
|
+
// Do not block connection establishment on elevation (it may take time);
|
|
136
|
+
// elevation is attempted asynchronously and errors are logged.
|
|
137
|
+
if (this.sshConfig.suPassword) {
|
|
138
|
+
this.ensureElevated().then(() => {
|
|
139
|
+
console.error('Successfully elevated to su shell');
|
|
140
|
+
}).catch((err) => {
|
|
141
|
+
console.error('Failed to elevate to su shell:', err);
|
|
142
|
+
// Do not reject connection; just log the error. Subsequent sudo calls
|
|
143
|
+
// will either use the su shell if available or provide sudo password.
|
|
144
|
+
});
|
|
145
|
+
}
|
|
113
146
|
resolve();
|
|
114
147
|
});
|
|
115
148
|
this.conn.on('error', (err) => {
|
|
@@ -138,6 +171,73 @@ export class SSHConnectionManager {
|
|
|
138
171
|
isConnected() {
|
|
139
172
|
return this.conn !== null && this.conn._sock && !this.conn._sock.destroyed;
|
|
140
173
|
}
|
|
174
|
+
getSudoPassword() {
|
|
175
|
+
return this.sshConfig.sudoPassword;
|
|
176
|
+
}
|
|
177
|
+
async ensureElevated() {
|
|
178
|
+
if (this.isElevated && this.suShell)
|
|
179
|
+
return;
|
|
180
|
+
if (!this.sshConfig.suPassword)
|
|
181
|
+
return;
|
|
182
|
+
if (this.suPromise)
|
|
183
|
+
return this.suPromise;
|
|
184
|
+
this.suPromise = new Promise((resolve, reject) => {
|
|
185
|
+
const conn = this.getConnection();
|
|
186
|
+
// Open an interactive shell and run `su -` inside it, responding to the password prompt.
|
|
187
|
+
// Request a PTY so that su/sudo can prompt for a password interactively.
|
|
188
|
+
conn.shell({ term: 'xterm', cols: 80, rows: 24 }, (err, stream) => {
|
|
189
|
+
if (err) {
|
|
190
|
+
this.suPromise = null;
|
|
191
|
+
reject(new McpError(ErrorCode.InternalError, `Failed to start interactive shell for su: ${err.message}`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
let buffer = '';
|
|
195
|
+
const cleanup = () => {
|
|
196
|
+
try {
|
|
197
|
+
stream.removeAllListeners('data');
|
|
198
|
+
}
|
|
199
|
+
catch (e) { /* ignore */ }
|
|
200
|
+
};
|
|
201
|
+
const onData = (data) => {
|
|
202
|
+
buffer += data.toString();
|
|
203
|
+
// Detect password prompt from su
|
|
204
|
+
if (/password[: ]*$/i.test(buffer)) {
|
|
205
|
+
// Send the su password
|
|
206
|
+
stream.write(this.sshConfig.suPassword + '\n');
|
|
207
|
+
buffer = '';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Detect common root prompt (ends with #) or explicit root indicator
|
|
211
|
+
if (/\n[^\n]*# $/.test(buffer) || /root[@:]/i.test(buffer)) {
|
|
212
|
+
// We appear to be elevated; keep the interactive shell as suShell
|
|
213
|
+
cleanup();
|
|
214
|
+
this.suShell = stream;
|
|
215
|
+
this.isElevated = true;
|
|
216
|
+
this.suPromise = null;
|
|
217
|
+
resolve();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Detect authentication failure
|
|
221
|
+
if (/authentication failure|incorrect password|su: .*failed/i.test(buffer)) {
|
|
222
|
+
cleanup();
|
|
223
|
+
this.suPromise = null;
|
|
224
|
+
reject(new McpError(ErrorCode.InternalError, `su authentication failed: ${buffer}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
stream.on('data', onData);
|
|
229
|
+
stream.on('close', () => {
|
|
230
|
+
if (!this.isElevated) {
|
|
231
|
+
this.suPromise = null;
|
|
232
|
+
reject(new McpError(ErrorCode.InternalError, 'su shell closed before elevation completed'));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// Kick off the su command
|
|
236
|
+
stream.write('su -\n');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return this.suPromise;
|
|
240
|
+
}
|
|
141
241
|
async ensureConnected() {
|
|
142
242
|
if (!this.isConnected()) {
|
|
143
243
|
await this.connect();
|
|
@@ -151,6 +251,14 @@ export class SSHConnectionManager {
|
|
|
151
251
|
}
|
|
152
252
|
close() {
|
|
153
253
|
if (this.conn) {
|
|
254
|
+
if (this.suShell) {
|
|
255
|
+
try {
|
|
256
|
+
this.suShell.end();
|
|
257
|
+
}
|
|
258
|
+
catch (e) { /* ignore */ }
|
|
259
|
+
this.suShell = null;
|
|
260
|
+
this.isElevated = false;
|
|
261
|
+
}
|
|
154
262
|
this.conn.end();
|
|
155
263
|
this.conn = null;
|
|
156
264
|
}
|
|
@@ -159,7 +267,7 @@ export class SSHConnectionManager {
|
|
|
159
267
|
let connectionManager = null;
|
|
160
268
|
const server = new McpServer({
|
|
161
269
|
name: 'SSH MCP Server',
|
|
162
|
-
version: '1.
|
|
270
|
+
version: '1.3.0',
|
|
163
271
|
capabilities: {
|
|
164
272
|
resources: {},
|
|
165
273
|
tools: {},
|
|
@@ -173,6 +281,9 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
|
|
|
173
281
|
try {
|
|
174
282
|
// Initialize connection manager if not already done
|
|
175
283
|
if (!connectionManager) {
|
|
284
|
+
if (!HOST || !USER) {
|
|
285
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required host or username');
|
|
286
|
+
}
|
|
176
287
|
const sshConfig = {
|
|
177
288
|
host: HOST,
|
|
178
289
|
port: PORT,
|
|
@@ -185,6 +296,9 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
|
|
|
185
296
|
const fs = await import('fs/promises');
|
|
186
297
|
sshConfig.privateKey = await fs.readFile(KEY, 'utf8');
|
|
187
298
|
}
|
|
299
|
+
if (SUPASSWORD !== null && SUPASSWORD !== undefined) {
|
|
300
|
+
sshConfig.suPassword = sanitizePassword(SUPASSWORD);
|
|
301
|
+
}
|
|
188
302
|
connectionManager = new SSHConnectionManager(sshConfig);
|
|
189
303
|
}
|
|
190
304
|
// Ensure connection is active (reconnect if needed)
|
|
@@ -199,12 +313,66 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
|
|
|
199
313
|
throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`);
|
|
200
314
|
}
|
|
201
315
|
});
|
|
316
|
+
// Expose sudo-exec tool unless explicitly disabled
|
|
317
|
+
if (!DISABLE_SUDO) {
|
|
318
|
+
server.tool("sudo-exec", "Execute a shell command on the remote SSH server using sudo. Will use sudo password if provided, otherwise assumes passwordless sudo.", {
|
|
319
|
+
command: z.string().describe("Shell command to execute with sudo on the remote SSH server"),
|
|
320
|
+
}, async ({ command }) => {
|
|
321
|
+
const sanitizedCommand = sanitizeCommand(command);
|
|
322
|
+
try {
|
|
323
|
+
if (!connectionManager) {
|
|
324
|
+
if (!HOST || !USER) {
|
|
325
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required host or username');
|
|
326
|
+
}
|
|
327
|
+
const sshConfig = {
|
|
328
|
+
host: HOST,
|
|
329
|
+
port: PORT || 22,
|
|
330
|
+
username: USER,
|
|
331
|
+
};
|
|
332
|
+
if (PASSWORD) {
|
|
333
|
+
sshConfig.password = PASSWORD;
|
|
334
|
+
}
|
|
335
|
+
else if (KEY) {
|
|
336
|
+
const fs = await import('fs/promises');
|
|
337
|
+
sshConfig.privateKey = await fs.readFile(KEY, 'utf8');
|
|
338
|
+
}
|
|
339
|
+
if (SUPASSWORD !== null && SUPASSWORD !== undefined) {
|
|
340
|
+
sshConfig.suPassword = sanitizePassword(SUPASSWORD);
|
|
341
|
+
}
|
|
342
|
+
if (SUDOPASSWORD !== null && SUDOPASSWORD !== undefined) {
|
|
343
|
+
sshConfig.sudoPassword = sanitizePassword(SUDOPASSWORD);
|
|
344
|
+
}
|
|
345
|
+
connectionManager = new SSHConnectionManager(sshConfig);
|
|
346
|
+
}
|
|
347
|
+
await connectionManager.ensureConnected();
|
|
348
|
+
let wrapped;
|
|
349
|
+
const sudoPassword = connectionManager.getSudoPassword();
|
|
350
|
+
if (!sudoPassword) {
|
|
351
|
+
// No password provided, use -n to fail if sudo requires a password
|
|
352
|
+
wrapped = `sudo -n sh -c '${sanitizedCommand.replace(/'/g, "'\\''")}'`;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Password provided — pipe it into sudo using printf. This avoids complex
|
|
356
|
+
// PTY/stdin handling on the SSH channel and is simpler and more reliable.
|
|
357
|
+
const pwdEscaped = sudoPassword.replace(/'/g, "'\\''");
|
|
358
|
+
wrapped = `printf '%s\\n' '${pwdEscaped}' | sudo -p "" -S sh -c '${sanitizedCommand.replace(/'/g, "'\\''")}'`;
|
|
359
|
+
}
|
|
360
|
+
return await execSshCommandWithConnection(connectionManager, wrapped);
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
if (err instanceof McpError)
|
|
364
|
+
throw err;
|
|
365
|
+
throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
202
369
|
// New function that uses persistent connection
|
|
203
|
-
export async function execSshCommandWithConnection(manager, command) {
|
|
370
|
+
export async function execSshCommandWithConnection(manager, command, stdin) {
|
|
204
371
|
return new Promise((resolve, reject) => {
|
|
205
372
|
let timeoutId;
|
|
206
373
|
let isResolved = false;
|
|
207
374
|
const conn = manager.getConnection();
|
|
375
|
+
const shell = manager.suShell; // Use su shell if available
|
|
208
376
|
// Set up timeout
|
|
209
377
|
timeoutId = setTimeout(() => {
|
|
210
378
|
if (!isResolved) {
|
|
@@ -226,7 +394,23 @@ export async function execSshCommandWithConnection(manager, command) {
|
|
|
226
394
|
reject(new McpError(ErrorCode.InternalError, `Command execution timed out after ${DEFAULT_TIMEOUT}ms`));
|
|
227
395
|
}
|
|
228
396
|
}, DEFAULT_TIMEOUT);
|
|
229
|
-
|
|
397
|
+
let execCommand = command;
|
|
398
|
+
const execFn = shell ?
|
|
399
|
+
(cb) => {
|
|
400
|
+
shell.write(execCommand + '\n');
|
|
401
|
+
cb(null, shell);
|
|
402
|
+
} :
|
|
403
|
+
(cb) => {
|
|
404
|
+
// If we have stdin to send (e.g., sudo -S), request a PTY so sudo can read the
|
|
405
|
+
// password reliably in environments that require a TTY.
|
|
406
|
+
if (stdin && stdin.length > 0) {
|
|
407
|
+
conn.exec(execCommand, { pty: true }, cb);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
conn.exec(execCommand, cb);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
execFn((err, stream) => {
|
|
230
414
|
if (err) {
|
|
231
415
|
if (!isResolved) {
|
|
232
416
|
isResolved = true;
|
|
@@ -237,6 +421,31 @@ export async function execSshCommandWithConnection(manager, command) {
|
|
|
237
421
|
}
|
|
238
422
|
let stdout = '';
|
|
239
423
|
let stderr = '';
|
|
424
|
+
// Setup data handlers before sending any input
|
|
425
|
+
stream.on('data', (data) => {
|
|
426
|
+
stdout += data.toString();
|
|
427
|
+
});
|
|
428
|
+
stream.stderr.on('data', (data) => {
|
|
429
|
+
stderr += data.toString();
|
|
430
|
+
});
|
|
431
|
+
// If stdin provided, write it to the stream
|
|
432
|
+
if (stdin && stdin.length > 0 && !shell) {
|
|
433
|
+
try {
|
|
434
|
+
// Write to the exec channel (send to remote stdin)
|
|
435
|
+
stream.write(stdin);
|
|
436
|
+
stream.end(); // End stdin after writing password
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
console.error('Error writing to stdin:', e);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// No stdin, just end the stream (guard in case stream.end is not available)
|
|
444
|
+
try {
|
|
445
|
+
stream.end();
|
|
446
|
+
}
|
|
447
|
+
catch (e) { /* ignore */ }
|
|
448
|
+
}
|
|
240
449
|
stream.on('close', (code, signal) => {
|
|
241
450
|
if (!isResolved) {
|
|
242
451
|
isResolved = true;
|
|
@@ -254,19 +463,13 @@ export async function execSshCommandWithConnection(manager, command) {
|
|
|
254
463
|
}
|
|
255
464
|
}
|
|
256
465
|
});
|
|
257
|
-
stream.on('data', (data) => {
|
|
258
|
-
stdout += data.toString();
|
|
259
|
-
});
|
|
260
|
-
stream.stderr.on('data', (data) => {
|
|
261
|
-
stderr += data.toString();
|
|
262
|
-
});
|
|
263
466
|
});
|
|
264
467
|
});
|
|
265
468
|
}
|
|
266
469
|
// Keep the old function for backward compatibility (used in tests)
|
|
267
|
-
export async function execSshCommand(sshConfig, command) {
|
|
470
|
+
export async function execSshCommand(sshConfig, command, stdin) {
|
|
268
471
|
return new Promise((resolve, reject) => {
|
|
269
|
-
const conn = new
|
|
472
|
+
const conn = new Client();
|
|
270
473
|
let timeoutId;
|
|
271
474
|
let isResolved = false;
|
|
272
475
|
// Set up timeout
|
|
@@ -304,6 +507,19 @@ export async function execSshCommand(sshConfig, command) {
|
|
|
304
507
|
conn.end();
|
|
305
508
|
return;
|
|
306
509
|
}
|
|
510
|
+
// If stdin provided, write it to the stream and end stdin
|
|
511
|
+
if (stdin && stdin.length > 0) {
|
|
512
|
+
try {
|
|
513
|
+
stream.write(stdin);
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
516
|
+
// ignore
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
stream.end();
|
|
521
|
+
}
|
|
522
|
+
catch (e) { /* ignore */ }
|
|
307
523
|
let stdout = '';
|
|
308
524
|
let stderr = '';
|
|
309
525
|
stream.on('close', (code, signal) => {
|
|
@@ -363,7 +579,16 @@ async function main() {
|
|
|
363
579
|
}
|
|
364
580
|
});
|
|
365
581
|
}
|
|
366
|
-
|
|
582
|
+
// Initialize server in test mode for automated tests
|
|
583
|
+
if (isTestMode) {
|
|
584
|
+
const transport = new StdioServerTransport();
|
|
585
|
+
server.connect(transport).catch(error => {
|
|
586
|
+
console.error("Fatal error connecting server:", error);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
// Start server in CLI mode
|
|
591
|
+
else if (isCliEnabled) {
|
|
367
592
|
main().catch((error) => {
|
|
368
593
|
console.error("Fatal error in main():", error);
|
|
369
594
|
if (connectionManager) {
|