@baitong-dev/execute-command-mcp 0.0.1 → 0.0.2
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/ExecuteCommandMcpServer.js +102 -68
- package/package.json +2 -2
|
@@ -53,7 +53,9 @@ const fs = __importStar(require("fs"));
|
|
|
53
53
|
const path_1 = __importDefault(require("path"));
|
|
54
54
|
const os_1 = __importDefault(require("os"));
|
|
55
55
|
const zod_1 = __importDefault(require("zod"));
|
|
56
|
-
const
|
|
56
|
+
const MCP_NAME = 'Execute Command MCP';
|
|
57
|
+
const MCP_VERSION = '0.0.2';
|
|
58
|
+
const HOME_DIR = process.env.MCP_HOME_DIR || os_1.default.homedir();
|
|
57
59
|
function getBinaryName(name) {
|
|
58
60
|
if (process.platform == 'win32') {
|
|
59
61
|
return `${name}.exe`;
|
|
@@ -62,10 +64,10 @@ function getBinaryName(name) {
|
|
|
62
64
|
}
|
|
63
65
|
function getBinaryPath(name) {
|
|
64
66
|
if (!name) {
|
|
65
|
-
return path_1.default.join(os_1.default.homedir(),
|
|
67
|
+
return path_1.default.join(os_1.default.homedir(), HOME_DIR, 'bin');
|
|
66
68
|
}
|
|
67
69
|
const binaryName = getBinaryName(name);
|
|
68
|
-
const binariesDir = path_1.default.join(os_1.default.homedir(),
|
|
70
|
+
const binariesDir = path_1.default.join(os_1.default.homedir(), HOME_DIR, 'bin');
|
|
69
71
|
const binariesDirExists = fs.existsSync(binariesDir);
|
|
70
72
|
return binariesDirExists ? path_1.default.join(binariesDir, binaryName) : binaryName;
|
|
71
73
|
}
|
|
@@ -125,9 +127,27 @@ function handleCommand(fullCommand) {
|
|
|
125
127
|
args
|
|
126
128
|
};
|
|
127
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* 检查一个目录是否是另一个目录的子目录
|
|
132
|
+
* @param {string} parent - 父目录路径
|
|
133
|
+
* @param {string} child - 要检查的子目录路径
|
|
134
|
+
* @param {Object} options - 配置选项
|
|
135
|
+
* @param {boolean} options.resolveSymlinks - 是否解析符号链接
|
|
136
|
+
* @param {boolean} options.includeSelf - 是否将自身视为子目录
|
|
137
|
+
* @returns {boolean|Promise<boolean>} - 同步返回 boolean,异步返回 Promise
|
|
138
|
+
*/
|
|
139
|
+
function isSubdirectory(parent, child, { includeSelf = false }) {
|
|
140
|
+
const parentPath = path_1.default.resolve(parent);
|
|
141
|
+
const childPath = path_1.default.resolve(child);
|
|
142
|
+
const relative = path_1.default.relative(parentPath, childPath);
|
|
143
|
+
if (includeSelf && childPath === parentPath) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return relative && !relative.startsWith('..') && !path_1.default.isAbsolute(relative);
|
|
147
|
+
}
|
|
128
148
|
const server = new mcp_js_1.McpServer({
|
|
129
|
-
name:
|
|
130
|
-
version:
|
|
149
|
+
name: MCP_NAME,
|
|
150
|
+
version: MCP_VERSION
|
|
131
151
|
}, {
|
|
132
152
|
capabilities: {
|
|
133
153
|
logging: {}
|
|
@@ -169,7 +189,7 @@ function formatCommandLine(command) {
|
|
|
169
189
|
* Format timing
|
|
170
190
|
*/
|
|
171
191
|
function formatTiming(durationMs) {
|
|
172
|
-
return `[${durationMs}ms]`;
|
|
192
|
+
return `[duration: ${durationMs}ms]`;
|
|
173
193
|
}
|
|
174
194
|
/**
|
|
175
195
|
* Middle-truncate output to preserve beginning and end
|
|
@@ -205,31 +225,27 @@ function classifyErrorType(exitCode, killed, forceKilled, spawnError, cwdError)
|
|
|
205
225
|
/**
|
|
206
226
|
* Format command result for output (Batch 1.2: includes errorType)
|
|
207
227
|
*/
|
|
208
|
-
function formatResult(result, options
|
|
228
|
+
function formatResult(result, options) {
|
|
209
229
|
const { description, timeout, command, showTiming = false } = options;
|
|
210
|
-
let output = ''
|
|
211
|
-
if (
|
|
212
|
-
output +=
|
|
213
|
-
if (
|
|
214
|
-
output +=
|
|
215
|
-
if (result.stdout)
|
|
216
|
-
output += result.stdout;
|
|
217
|
-
if (result.stderr)
|
|
218
|
-
output += `\n[stderr]: ${result.stderr}`;
|
|
230
|
+
let output = `=== ${formatCommandLine(command)} ${description ? `(${description})` : ''} ===`;
|
|
231
|
+
if (result.truncatedStdout)
|
|
232
|
+
output += `\n\n[stdout]: ${result.truncatedStdout}`;
|
|
233
|
+
if (result.truncatedStderr)
|
|
234
|
+
output += `\n\n[stderr]: ${result.truncatedStderr}`;
|
|
219
235
|
if (result.killed) {
|
|
220
236
|
if (result.forceKilled) {
|
|
221
|
-
output += `\n[timeout after ${(timeout || 30000) + GRACEFUL_TIMEOUT_MS}ms, SIGKILL]`;
|
|
237
|
+
output += `\n\n[timeout after ${(timeout || 30000) + GRACEFUL_TIMEOUT_MS}ms, SIGKILL]`;
|
|
222
238
|
}
|
|
223
239
|
else {
|
|
224
|
-
output += `\n[timeout after ${timeout || 30000}ms, SIGTERM]`;
|
|
240
|
+
output += `\n\n[timeout after ${timeout || 30000}ms, SIGTERM]`;
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
if (result.errorType && result.errorType !== 'timeout') {
|
|
228
|
-
output += `\n[errorType: ${result.errorType}]`;
|
|
244
|
+
output += `\n\n[errorType: ${result.errorType}]`;
|
|
229
245
|
}
|
|
230
|
-
output += `\n[exit code: ${result.exitCode}]`;
|
|
246
|
+
output += `\n\n[exit code: ${result.exitCode}]`;
|
|
231
247
|
if (showTiming && result.durationMs !== undefined) {
|
|
232
|
-
output += `\n${formatTiming(result.durationMs)}`;
|
|
248
|
+
output += `\n\n${formatTiming(result.durationMs)}`;
|
|
233
249
|
}
|
|
234
250
|
return output.trim() || '(no output)';
|
|
235
251
|
}
|
|
@@ -240,13 +256,20 @@ function formatResult(result, options = {}) {
|
|
|
240
256
|
* Write output to file (before truncation)
|
|
241
257
|
* Returns error message on failure, undefined on success
|
|
242
258
|
*/
|
|
243
|
-
function writeOutputToFile(filePath, content) {
|
|
259
|
+
function writeOutputToFile(filePath, content, label = 'stdout') {
|
|
244
260
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
if (isSubdirectory(HOME_DIR, filePath, {
|
|
262
|
+
includeSelf: true
|
|
263
|
+
})) {
|
|
264
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
265
|
+
return `\n\n[${label} saved to: ${filePath}]`;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
throw Error(`"${filePath}" is not in home directory`);
|
|
269
|
+
}
|
|
247
270
|
}
|
|
248
271
|
catch (err) {
|
|
249
|
-
return err.message
|
|
272
|
+
return `\n\n[${label} save failed: ${err.message}]`;
|
|
250
273
|
}
|
|
251
274
|
}
|
|
252
275
|
/**
|
|
@@ -276,6 +299,8 @@ async function executeSequence(options) {
|
|
|
276
299
|
command: ((_a = commands[0]) === null || _a === void 0 ? void 0 : _a.command) || '',
|
|
277
300
|
stdout: '',
|
|
278
301
|
stderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
302
|
+
truncatedStdout: '',
|
|
303
|
+
truncatedStderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
279
304
|
exitCode: 1,
|
|
280
305
|
durationMs: 0,
|
|
281
306
|
stopped: true,
|
|
@@ -307,21 +332,25 @@ async function executeSequence(options) {
|
|
|
307
332
|
results.push({
|
|
308
333
|
index,
|
|
309
334
|
command,
|
|
310
|
-
stdout:
|
|
335
|
+
stdout: command,
|
|
311
336
|
stderr: '',
|
|
337
|
+
truncatedStdout: command,
|
|
338
|
+
truncatedStderr: '',
|
|
312
339
|
exitCode: 0,
|
|
313
|
-
durationMs:
|
|
340
|
+
durationMs: 1
|
|
314
341
|
});
|
|
315
342
|
continue;
|
|
316
343
|
}
|
|
317
344
|
// 每次执行非cd命令
|
|
318
345
|
const result = await executeCommand(Object.assign(Object.assign({}, options), { cwd: newCwd, timeout: newTimeout, command }));
|
|
319
|
-
const { stdout, stderr, exitCode, killed, durationMs } = result;
|
|
346
|
+
const { stdout, stderr, exitCode, killed, durationMs, truncatedStderr, truncatedStdout } = result;
|
|
320
347
|
const sequenceResult = {
|
|
321
348
|
index,
|
|
322
349
|
command,
|
|
323
350
|
stdout,
|
|
324
351
|
stderr,
|
|
352
|
+
truncatedStdout,
|
|
353
|
+
truncatedStderr,
|
|
325
354
|
exitCode,
|
|
326
355
|
durationMs,
|
|
327
356
|
stopped: false
|
|
@@ -387,8 +416,14 @@ const bashInputSchema = zod_1.default.object({
|
|
|
387
416
|
.max(1000000)
|
|
388
417
|
.optional()
|
|
389
418
|
.describe('Maximum output length before middle-truncation (default 30000)'),
|
|
390
|
-
outputFile: zod_1.default
|
|
391
|
-
|
|
419
|
+
outputFile: zod_1.default
|
|
420
|
+
.string()
|
|
421
|
+
.optional()
|
|
422
|
+
.describe('File path (.txt) to save full stdout (before truncation)'),
|
|
423
|
+
stderrFile: zod_1.default
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe('File path (.txt) to save full stderr (before truncation)'),
|
|
392
427
|
loginShell: zod_1.default
|
|
393
428
|
.boolean()
|
|
394
429
|
.optional()
|
|
@@ -398,7 +433,7 @@ const bashInputSchema = zod_1.default.object({
|
|
|
398
433
|
* Execute a command and return result
|
|
399
434
|
*/
|
|
400
435
|
function executeCommand(options) {
|
|
401
|
-
const { command, cwd = DEFAULT_CWD, timeout = 30000, env, stdin, maxOutput = 30000
|
|
436
|
+
const { command, cwd = DEFAULT_CWD, timeout = 30000, env, stdin, maxOutput = 30000
|
|
402
437
|
// loginShell = false
|
|
403
438
|
} = options;
|
|
404
439
|
return new Promise(resolve => {
|
|
@@ -407,6 +442,8 @@ function executeCommand(options) {
|
|
|
407
442
|
// Batch 1.3: CWD Validation
|
|
408
443
|
if (!fs.existsSync(cwd)) {
|
|
409
444
|
resolve({
|
|
445
|
+
truncatedStdout: '',
|
|
446
|
+
truncatedStderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
410
447
|
stdout: '',
|
|
411
448
|
stderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
412
449
|
exitCode: 1,
|
|
@@ -423,7 +460,6 @@ function executeCommand(options) {
|
|
|
423
460
|
// Use -lc for login shell (sources .profile), -c for regular
|
|
424
461
|
// const shellArgs = loginShell ? ['-lc', command] : ['-c', command]
|
|
425
462
|
const handledCommand = handleCommand(command);
|
|
426
|
-
console.log(handledCommand);
|
|
427
463
|
proc = (0, child_process_1.spawn)(handledCommand.command, handledCommand.args, {
|
|
428
464
|
cwd,
|
|
429
465
|
env: mergedEnv,
|
|
@@ -432,6 +468,8 @@ function executeCommand(options) {
|
|
|
432
468
|
}
|
|
433
469
|
catch (err) {
|
|
434
470
|
resolve({
|
|
471
|
+
truncatedStdout: '',
|
|
472
|
+
truncatedStderr: `[error]: Failed to spawn process: ${err.message}`,
|
|
435
473
|
stdout: '',
|
|
436
474
|
stderr: `[error]: Failed to spawn process: ${err.message}`,
|
|
437
475
|
exitCode: 1,
|
|
@@ -476,14 +514,7 @@ function executeCommand(options) {
|
|
|
476
514
|
if (graceTimer)
|
|
477
515
|
clearTimeout(graceTimer);
|
|
478
516
|
const durationMs = Date.now() - startTime;
|
|
479
|
-
|
|
480
|
-
if (outputFile) {
|
|
481
|
-
writeOutputToFile(outputFile, stdout);
|
|
482
|
-
}
|
|
483
|
-
if (stderrFile) {
|
|
484
|
-
writeOutputToFile(stderrFile, stderr);
|
|
485
|
-
}
|
|
486
|
-
// Apply truncation for response
|
|
517
|
+
const errorType = classifyErrorType(code, killed, forceKilled);
|
|
487
518
|
let truncatedStdout = stdout;
|
|
488
519
|
let truncatedStderr = stderr;
|
|
489
520
|
if (truncatedStdout.length > maxOutput) {
|
|
@@ -492,10 +523,11 @@ function executeCommand(options) {
|
|
|
492
523
|
if (truncatedStderr.length > maxOutput) {
|
|
493
524
|
truncatedStderr = middleTruncate(truncatedStderr, maxOutput);
|
|
494
525
|
}
|
|
495
|
-
const errorType = classifyErrorType(code, killed, forceKilled);
|
|
496
526
|
resolve({
|
|
497
|
-
|
|
498
|
-
|
|
527
|
+
truncatedStdout,
|
|
528
|
+
truncatedStderr,
|
|
529
|
+
stdout,
|
|
530
|
+
stderr,
|
|
499
531
|
exitCode: killed ? EXIT_CODE_TIMEOUT : code,
|
|
500
532
|
killed,
|
|
501
533
|
forceKilled,
|
|
@@ -510,6 +542,8 @@ function executeCommand(options) {
|
|
|
510
542
|
clearTimeout(graceTimer);
|
|
511
543
|
const durationMs = Date.now() - startTime;
|
|
512
544
|
resolve({
|
|
545
|
+
truncatedStdout: '',
|
|
546
|
+
truncatedStderr: err.message,
|
|
513
547
|
stdout: '',
|
|
514
548
|
stderr: err.message,
|
|
515
549
|
exitCode: 1,
|
|
@@ -525,7 +559,7 @@ server.registerTool('execute_command', {
|
|
|
525
559
|
description: 'Execute a command directly. Returns stdout, stderr, and exit code.',
|
|
526
560
|
inputSchema: bashInputSchema
|
|
527
561
|
}, async (args) => {
|
|
528
|
-
const command = args
|
|
562
|
+
const { command, outputFile, stderrFile, timeout = 30000 } = args;
|
|
529
563
|
if (command.includes('&&')) {
|
|
530
564
|
return executeSequenceCallback({
|
|
531
565
|
commands: command.split('&&').map(cmd => ({ command: cmd.trim() })),
|
|
@@ -547,16 +581,16 @@ server.registerTool('execute_command', {
|
|
|
547
581
|
// Use formatFullOutput for DRY consistency
|
|
548
582
|
let output = formatResult(result, {
|
|
549
583
|
description: args.description,
|
|
550
|
-
timeout
|
|
584
|
+
timeout,
|
|
551
585
|
command,
|
|
552
586
|
showTiming: true
|
|
553
587
|
});
|
|
554
588
|
// Add file save notices
|
|
555
|
-
if (
|
|
556
|
-
output +=
|
|
589
|
+
if (outputFile && result.stdout) {
|
|
590
|
+
output += writeOutputToFile(outputFile, result.stdout);
|
|
557
591
|
}
|
|
558
|
-
if (
|
|
559
|
-
output +=
|
|
592
|
+
if (stderrFile && result.stderr) {
|
|
593
|
+
output += writeOutputToFile(stderrFile, result.stderr);
|
|
560
594
|
}
|
|
561
595
|
return {
|
|
562
596
|
isError: result.exitCode !== 0,
|
|
@@ -593,7 +627,10 @@ const bashSequenceInputSchema = zod_1.default.object({
|
|
|
593
627
|
.record(zod_1.default.string(), zod_1.default.string())
|
|
594
628
|
.optional()
|
|
595
629
|
.describe('Additional environment variables to set'),
|
|
596
|
-
outputFile: zod_1.default
|
|
630
|
+
outputFile: zod_1.default
|
|
631
|
+
.string()
|
|
632
|
+
.optional()
|
|
633
|
+
.describe('File path (.txt) to save full output (before truncation)')
|
|
597
634
|
});
|
|
598
635
|
const executeSequenceCallback = async (args) => {
|
|
599
636
|
var _a;
|
|
@@ -613,35 +650,32 @@ const executeSequenceCallback = async (args) => {
|
|
|
613
650
|
}
|
|
614
651
|
}
|
|
615
652
|
const seqResult = await executeSequence(args);
|
|
616
|
-
// Format output per Batch 4.4 spec
|
|
617
653
|
const outputParts = seqResult.results.map(r => {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
body += r.stdout;
|
|
625
|
-
if (r.stderr)
|
|
626
|
-
body += `\n[stderr]: ${r.stderr}`;
|
|
627
|
-
body += `\n[exit code: ${r.exitCode}]`;
|
|
654
|
+
let output = `[${r.index}] === Command ${formatCommandLine(r.command)} ${r.description ? `(${r.description})` : ''} ===`;
|
|
655
|
+
if (r.truncatedStdout)
|
|
656
|
+
output += `\n\n[stdout]: ${r.truncatedStdout}`;
|
|
657
|
+
if (r.truncatedStderr)
|
|
658
|
+
output += `\n\n[stderr]: ${r.truncatedStderr}`;
|
|
659
|
+
output += `\n\n[exit code: ${r.exitCode}]`;
|
|
628
660
|
if (r.stopped && r.stopReason) {
|
|
629
|
-
|
|
661
|
+
output += `\n\n[stopped - ${r.stopReason}]`;
|
|
630
662
|
}
|
|
631
|
-
|
|
632
|
-
return
|
|
663
|
+
output += `\n\n${formatTiming(r.durationMs)}`;
|
|
664
|
+
return output;
|
|
633
665
|
});
|
|
634
666
|
// Summary
|
|
635
|
-
let summary = `\n=== Summary ===`;
|
|
636
|
-
summary += `\nTotal: ${seqResult.totalDurationMs}ms | Executed: ${seqResult.executed}/${commands.length} | Succeeded: ${seqResult.succeeded} | Failed: ${seqResult.failed}`;
|
|
667
|
+
let summary = `\n\n=== Summary ===`;
|
|
668
|
+
summary += `\n\nTotal: ${seqResult.totalDurationMs}ms | Executed: ${seqResult.executed}/${commands.length} | Succeeded: ${seqResult.succeeded} | Failed: ${seqResult.failed}`;
|
|
637
669
|
if (seqResult.stoppedAt !== undefined) {
|
|
638
670
|
const stoppedCmd = seqResult.results[seqResult.stoppedAt];
|
|
639
671
|
const stoppedDesc = (stoppedCmd === null || stoppedCmd === void 0 ? void 0 : stoppedCmd.description) || ((_a = stoppedCmd === null || stoppedCmd === void 0 ? void 0 : stoppedCmd.command) === null || _a === void 0 ? void 0 : _a.slice(0, 30)) || 'unknown';
|
|
640
|
-
summary += `\nStopped at: [${seqResult.stoppedAt + 1}] ${stoppedDesc}`;
|
|
672
|
+
summary += `\n\nStopped at: [${seqResult.stoppedAt + 1}] ${stoppedDesc}`;
|
|
641
673
|
}
|
|
642
674
|
let response = outputParts.join('\n\n') + summary;
|
|
643
675
|
if (args.outputFile) {
|
|
644
|
-
response +=
|
|
676
|
+
response += writeOutputToFile(args.outputFile, seqResult.results
|
|
677
|
+
.map(r => `[${r.index}] === Command ${r.index} ===\n[stdout]: ${r.stdout}\n[stderr]: ${r.stderr}`)
|
|
678
|
+
.join('\n\n'), 'output');
|
|
645
679
|
}
|
|
646
680
|
return {
|
|
647
681
|
isError: seqResult.failed > 0,
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baitong-dev/execute-command-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"main": "ExecuteCommandMcpServer.js",
|
|
5
5
|
"bin": {
|
|
6
|
-
"@baitong-dev/
|
|
6
|
+
"@baitong-dev/execute-command-mcp": "./ExecuteCommandMcpServer.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"ExecuteCommandMcpServer.js",
|