@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.
@@ -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 HOME_APP_DIR = '.baitong-desktop';
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(), HOME_APP_DIR, 'bin');
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(), HOME_APP_DIR, 'bin');
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: 'execute-command-mcp',
130
- version: '1.0.0'
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 (description)
212
- output += `=== ${description} ===\n`;
213
- if (command)
214
- output += `${formatCommandLine(command)}\n`;
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
- fs.writeFileSync(filePath, content, 'utf8');
246
- return undefined;
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: 0
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.string().optional().describe('File path to save full stdout (before truncation)'),
391
- stderrFile: zod_1.default.string().optional().describe('File path to save full stderr (before truncation)'),
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, outputFile, stderrFile
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
- // Batch 5.1: Write to files BEFORE truncation
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
- stdout: truncatedStdout,
498
- stderr: truncatedStderr,
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.command;
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: args.timeout,
584
+ timeout,
551
585
  command,
552
586
  showTiming: true
553
587
  });
554
588
  // Add file save notices
555
- if (args.outputFile) {
556
- output += `\n[stdout saved to: ${args.outputFile}]`;
589
+ if (outputFile && result.stdout) {
590
+ output += writeOutputToFile(outputFile, result.stdout);
557
591
  }
558
- if (args.stderrFile) {
559
- output += `\n[stderr saved to: ${args.stderrFile}]`;
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.string().optional().describe('File path to save full output (before truncation)')
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
- const header = r.description
619
- ? `[${r.index}] === ${r.description} ===`
620
- : `[${r.index}] === Command ${r.index} ===`;
621
- const cmdLine = formatCommandLine(r.command);
622
- let body = '';
623
- if (r.stdout)
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
- body += `\n[stopped - ${r.stopReason}]`;
661
+ output += `\n\n[stopped - ${r.stopReason}]`;
630
662
  }
631
- const timing = formatTiming(r.durationMs);
632
- return `${header}\n${cmdLine}\n${body.trim()}\n${timing}`;
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 += `\n[output saved to: ${args.outputFile}]`;
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.1",
3
+ "version": "0.0.2",
4
4
  "main": "ExecuteCommandMcpServer.js",
5
5
  "bin": {
6
- "@baitong-dev/skills-mcp": "./ExecuteCommandMcpServer.js"
6
+ "@baitong-dev/execute-command-mcp": "./ExecuteCommandMcpServer.js"
7
7
  },
8
8
  "files": [
9
9
  "ExecuteCommandMcpServer.js",