@baitong-dev/execute-command-mcp 0.0.1
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 +660 -0
- package/package.json +27 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Execute Command MCP Server v1.0
|
|
5
|
+
* Provides direct command execution tools that bypass Claude Code's
|
|
6
|
+
* slow haiku-based pre-flight checks.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - execute_command: Single command execution
|
|
10
|
+
* - execute_command_sequence: Sequential commands in a single shell session (stateful)
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
50
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const os_1 = __importDefault(require("os"));
|
|
55
|
+
const zod_1 = __importDefault(require("zod"));
|
|
56
|
+
const HOME_APP_DIR = '.baitong-desktop';
|
|
57
|
+
function getBinaryName(name) {
|
|
58
|
+
if (process.platform == 'win32') {
|
|
59
|
+
return `${name}.exe`;
|
|
60
|
+
}
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
function getBinaryPath(name) {
|
|
64
|
+
if (!name) {
|
|
65
|
+
return path_1.default.join(os_1.default.homedir(), HOME_APP_DIR, 'bin');
|
|
66
|
+
}
|
|
67
|
+
const binaryName = getBinaryName(name);
|
|
68
|
+
const binariesDir = path_1.default.join(os_1.default.homedir(), HOME_APP_DIR, 'bin');
|
|
69
|
+
const binariesDirExists = fs.existsSync(binariesDir);
|
|
70
|
+
return binariesDirExists ? path_1.default.join(binariesDir, binaryName) : binaryName;
|
|
71
|
+
}
|
|
72
|
+
function isBinaryExists(name) {
|
|
73
|
+
const cmd = getBinaryPath(name);
|
|
74
|
+
return fs.existsSync(cmd);
|
|
75
|
+
}
|
|
76
|
+
function handleCommand(fullCommand) {
|
|
77
|
+
const args = fullCommand.split(' ').map(arg => arg.trim());
|
|
78
|
+
const command = args.shift();
|
|
79
|
+
if (!command) {
|
|
80
|
+
throw new Error('No command specified');
|
|
81
|
+
}
|
|
82
|
+
let cmd = command;
|
|
83
|
+
if (command === 'bun' || command === 'npx' || command === 'node') {
|
|
84
|
+
if (isBinaryExists('bun')) {
|
|
85
|
+
// Fall back to bundled bun
|
|
86
|
+
cmd = getBinaryPath('bun');
|
|
87
|
+
// Transform args for bun x format
|
|
88
|
+
if (args && args.length > 0) {
|
|
89
|
+
if (command === 'npx') {
|
|
90
|
+
// Transform args for npx format npx xxx -> bun x -y xxx
|
|
91
|
+
if (!args.includes('-y')) {
|
|
92
|
+
args.unshift('-y');
|
|
93
|
+
}
|
|
94
|
+
if (!args.includes('x')) {
|
|
95
|
+
args.unshift('x');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Neither npx nor bun available
|
|
102
|
+
throw new Error('npx not found in PATH and bundled bun is not available. This may indicate an installation issue.\n' +
|
|
103
|
+
'Please either:\n' +
|
|
104
|
+
'1. Install Node.js (which includes npx) from https://nodejs.org\n' +
|
|
105
|
+
'2. Run the MCP dependencies installer from Settings\n' +
|
|
106
|
+
'3. Restart the application if you recently installed Node.js');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (command === 'uvx' || command === 'uv') {
|
|
110
|
+
if (isBinaryExists(command)) {
|
|
111
|
+
// Fall back to bundled version
|
|
112
|
+
cmd = getBinaryPath(command);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Neither system nor bundled available
|
|
116
|
+
throw new Error(`${command} not found in PATH and bundled version is not available. This may indicate an installation issue.\n` +
|
|
117
|
+
'Please either:\n' +
|
|
118
|
+
'1. Install uv from https://github.com/astral-sh/uv\n' +
|
|
119
|
+
'2. Run the MCP dependencies installer from Settings\n' +
|
|
120
|
+
`3. Restart the application if you recently installed ${command}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
command: cmd,
|
|
125
|
+
args
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const server = new mcp_js_1.McpServer({
|
|
129
|
+
name: 'execute-command-mcp',
|
|
130
|
+
version: '1.0.0'
|
|
131
|
+
}, {
|
|
132
|
+
capabilities: {
|
|
133
|
+
logging: {}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// Constants
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Default working directory: env var > process.cwd() > HOME > /tmp
|
|
140
|
+
const DEFAULT_CWD = process.env.FAST_BASH_DEFAULT_CWD || process.cwd() || process.env.HOME || '/tmp';
|
|
141
|
+
// Grace period before SIGKILL (ms)
|
|
142
|
+
const GRACEFUL_TIMEOUT_MS = 5000;
|
|
143
|
+
// Reserved characters for truncation ellipsis
|
|
144
|
+
const TRUNCATION_ELLIPSIS_RESERVE = 50;
|
|
145
|
+
// Exit code for timeout (shell convention)
|
|
146
|
+
const EXIT_CODE_TIMEOUT = 124;
|
|
147
|
+
// Sudo rejection message
|
|
148
|
+
const SUDO_REJECTION_MESSAGE = '[REJECTED] sudo commands cannot be executed. Please STOP and ask human user to run it for you!\n';
|
|
149
|
+
/**
|
|
150
|
+
* Check if a command contains sudo
|
|
151
|
+
* Matches: sudo at start, after semicolon, after &&, after ||, after |, after $(, after backtick
|
|
152
|
+
*/
|
|
153
|
+
function containsSudo(command) {
|
|
154
|
+
// Match sudo as a standalone command (not part of another word like "pseudocode")
|
|
155
|
+
return /(?:^|[;&|`$()]\s*)sudo(?:\s|$)/m.test(command);
|
|
156
|
+
}
|
|
157
|
+
// Default timeout for sequence commands (5 minutes)
|
|
158
|
+
const DEFAULT_SEQUENCE_TIMEOUT = 300000;
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// Batch 1.1: DRY Output Formatting
|
|
161
|
+
// =============================================================================
|
|
162
|
+
/**
|
|
163
|
+
* Format command line with $ prefix
|
|
164
|
+
*/
|
|
165
|
+
function formatCommandLine(command) {
|
|
166
|
+
return `$ ${command}`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Format timing
|
|
170
|
+
*/
|
|
171
|
+
function formatTiming(durationMs) {
|
|
172
|
+
return `[${durationMs}ms]`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Middle-truncate output to preserve beginning and end
|
|
176
|
+
*/
|
|
177
|
+
function middleTruncate(text, maxLength) {
|
|
178
|
+
if (text.length <= maxLength)
|
|
179
|
+
return text;
|
|
180
|
+
const halfLength = Math.floor((maxLength - TRUNCATION_ELLIPSIS_RESERVE) / 2);
|
|
181
|
+
const start = text.slice(0, halfLength);
|
|
182
|
+
const end = text.slice(-halfLength);
|
|
183
|
+
const truncatedBytes = text.length - maxLength;
|
|
184
|
+
return `${start}\n\n... [truncated ${truncatedBytes} characters] ...\n\n${end}`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Classify error type from exit code and error conditions
|
|
188
|
+
*/
|
|
189
|
+
function classifyErrorType(exitCode, killed, forceKilled, spawnError, cwdError) {
|
|
190
|
+
if (cwdError)
|
|
191
|
+
return 'cwd_not_found';
|
|
192
|
+
if (spawnError) {
|
|
193
|
+
if (spawnError.code === 'ENOENT')
|
|
194
|
+
return 'spawn_error';
|
|
195
|
+
return 'spawn_error';
|
|
196
|
+
}
|
|
197
|
+
if (killed)
|
|
198
|
+
return forceKilled ? 'killed' : 'timeout';
|
|
199
|
+
if (exitCode === 127)
|
|
200
|
+
return 'command_not_found';
|
|
201
|
+
if (exitCode === 126)
|
|
202
|
+
return 'permission_denied';
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Format command result for output (Batch 1.2: includes errorType)
|
|
207
|
+
*/
|
|
208
|
+
function formatResult(result, options = {}) {
|
|
209
|
+
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}`;
|
|
219
|
+
if (result.killed) {
|
|
220
|
+
if (result.forceKilled) {
|
|
221
|
+
output += `\n[timeout after ${(timeout || 30000) + GRACEFUL_TIMEOUT_MS}ms, SIGKILL]`;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
output += `\n[timeout after ${timeout || 30000}ms, SIGTERM]`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (result.errorType && result.errorType !== 'timeout') {
|
|
228
|
+
output += `\n[errorType: ${result.errorType}]`;
|
|
229
|
+
}
|
|
230
|
+
output += `\n[exit code: ${result.exitCode}]`;
|
|
231
|
+
if (showTiming && result.durationMs !== undefined) {
|
|
232
|
+
output += `\n${formatTiming(result.durationMs)}`;
|
|
233
|
+
}
|
|
234
|
+
return output.trim() || '(no output)';
|
|
235
|
+
}
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Batch 5.1: File Output Helper
|
|
238
|
+
// =============================================================================
|
|
239
|
+
/**
|
|
240
|
+
* Write output to file (before truncation)
|
|
241
|
+
* Returns error message on failure, undefined on success
|
|
242
|
+
*/
|
|
243
|
+
function writeOutputToFile(filePath, content) {
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return err.message;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Execute commands sequentially in a single shell session (stateful)
|
|
254
|
+
*/
|
|
255
|
+
async function executeSequence(options) {
|
|
256
|
+
var _a;
|
|
257
|
+
const { commands, stopOnFailure = true, continueOnCodes = [0], cwd = DEFAULT_CWD, timeout = DEFAULT_SEQUENCE_TIMEOUT
|
|
258
|
+
// outputFile
|
|
259
|
+
} = options;
|
|
260
|
+
// Empty commands validation
|
|
261
|
+
if (commands.length === 0) {
|
|
262
|
+
return {
|
|
263
|
+
results: [],
|
|
264
|
+
totalDurationMs: 0,
|
|
265
|
+
succeeded: 0,
|
|
266
|
+
failed: 0,
|
|
267
|
+
executed: 0
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// CWD Validation
|
|
271
|
+
if (!fs.existsSync(cwd)) {
|
|
272
|
+
return {
|
|
273
|
+
results: [
|
|
274
|
+
{
|
|
275
|
+
index: 0,
|
|
276
|
+
command: ((_a = commands[0]) === null || _a === void 0 ? void 0 : _a.command) || '',
|
|
277
|
+
stdout: '',
|
|
278
|
+
stderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
279
|
+
exitCode: 1,
|
|
280
|
+
durationMs: 0,
|
|
281
|
+
stopped: true,
|
|
282
|
+
stopReason: 'cwd_not_found'
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
totalDurationMs: 0,
|
|
286
|
+
succeeded: 0,
|
|
287
|
+
failed: 1,
|
|
288
|
+
executed: 0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
let newCwd = cwd;
|
|
292
|
+
let newTimeout = timeout;
|
|
293
|
+
const results = [];
|
|
294
|
+
let succeeded = 0;
|
|
295
|
+
let failed = 0;
|
|
296
|
+
let stoppedAt = undefined;
|
|
297
|
+
let totalDurationMs = 0;
|
|
298
|
+
let index = 0;
|
|
299
|
+
try {
|
|
300
|
+
for (let i = 0; i < commands.length; i++) {
|
|
301
|
+
index = i;
|
|
302
|
+
const command = commands[i].command;
|
|
303
|
+
if (command.startsWith('cd ')) {
|
|
304
|
+
// 每次执行cd命令,不执行exec了
|
|
305
|
+
newCwd = path_1.default.resolve(newCwd, command.replace('cd ', ''));
|
|
306
|
+
succeeded++;
|
|
307
|
+
results.push({
|
|
308
|
+
index,
|
|
309
|
+
command,
|
|
310
|
+
stdout: '',
|
|
311
|
+
stderr: '',
|
|
312
|
+
exitCode: 0,
|
|
313
|
+
durationMs: 0
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// 每次执行非cd命令
|
|
318
|
+
const result = await executeCommand(Object.assign(Object.assign({}, options), { cwd: newCwd, timeout: newTimeout, command }));
|
|
319
|
+
const { stdout, stderr, exitCode, killed, durationMs } = result;
|
|
320
|
+
const sequenceResult = {
|
|
321
|
+
index,
|
|
322
|
+
command,
|
|
323
|
+
stdout,
|
|
324
|
+
stderr,
|
|
325
|
+
exitCode,
|
|
326
|
+
durationMs,
|
|
327
|
+
stopped: false
|
|
328
|
+
};
|
|
329
|
+
totalDurationMs += durationMs;
|
|
330
|
+
newTimeout = timeout - durationMs;
|
|
331
|
+
if (sequenceResult.exitCode != null && continueOnCodes.includes(sequenceResult.exitCode)) {
|
|
332
|
+
succeeded++;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
failed++;
|
|
336
|
+
if (stopOnFailure && stoppedAt === undefined) {
|
|
337
|
+
stoppedAt = i;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (stoppedAt === i) {
|
|
341
|
+
sequenceResult.stopped = true;
|
|
342
|
+
sequenceResult.stopReason = killed
|
|
343
|
+
? 'timeout'
|
|
344
|
+
: `exit code ${exitCode} not in continue_on_codes`;
|
|
345
|
+
}
|
|
346
|
+
results.push(sequenceResult);
|
|
347
|
+
if (stoppedAt !== undefined)
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (_) {
|
|
352
|
+
//
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
results,
|
|
356
|
+
totalDurationMs,
|
|
357
|
+
succeeded,
|
|
358
|
+
failed,
|
|
359
|
+
executed: results.length,
|
|
360
|
+
stoppedAt: 0
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// =============================================================================
|
|
364
|
+
// Tool Definitions
|
|
365
|
+
// =============================================================================
|
|
366
|
+
const bashInputSchema = zod_1.default.object({
|
|
367
|
+
command: zod_1.default.string().min(1).describe('The command to execute'),
|
|
368
|
+
cwd: zod_1.default.string().describe('Working directory'),
|
|
369
|
+
timeout: zod_1.default
|
|
370
|
+
.number()
|
|
371
|
+
.min(1)
|
|
372
|
+
.max(600000)
|
|
373
|
+
.optional()
|
|
374
|
+
.describe('Timeout in milliseconds (optional, default 30000, max 600000)'),
|
|
375
|
+
description: zod_1.default
|
|
376
|
+
.string()
|
|
377
|
+
.optional()
|
|
378
|
+
.describe('Short description of what this command does (for logging)'),
|
|
379
|
+
env: zod_1.default
|
|
380
|
+
.record(zod_1.default.string(), zod_1.default.string())
|
|
381
|
+
.optional()
|
|
382
|
+
.describe('Additional environment variables to set'),
|
|
383
|
+
stdin: zod_1.default.string().optional().describe("Input to pipe to the command's stdin"),
|
|
384
|
+
maxOutput: zod_1.default
|
|
385
|
+
.number()
|
|
386
|
+
.min(1)
|
|
387
|
+
.max(1000000)
|
|
388
|
+
.optional()
|
|
389
|
+
.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)'),
|
|
392
|
+
loginShell: zod_1.default
|
|
393
|
+
.boolean()
|
|
394
|
+
.optional()
|
|
395
|
+
.describe('Run as login shell (-l flag) to source .profile/.bash_profile (default: false)')
|
|
396
|
+
});
|
|
397
|
+
/**
|
|
398
|
+
* Execute a command and return result
|
|
399
|
+
*/
|
|
400
|
+
function executeCommand(options) {
|
|
401
|
+
const { command, cwd = DEFAULT_CWD, timeout = 30000, env, stdin, maxOutput = 30000, outputFile, stderrFile
|
|
402
|
+
// loginShell = false
|
|
403
|
+
} = options;
|
|
404
|
+
return new Promise(resolve => {
|
|
405
|
+
var _a, _b, _c, _d;
|
|
406
|
+
const startTime = Date.now();
|
|
407
|
+
// Batch 1.3: CWD Validation
|
|
408
|
+
if (!fs.existsSync(cwd)) {
|
|
409
|
+
resolve({
|
|
410
|
+
stdout: '',
|
|
411
|
+
stderr: `[error]: Working directory does not exist: ${cwd}`,
|
|
412
|
+
exitCode: 1,
|
|
413
|
+
killed: false,
|
|
414
|
+
forceKilled: false,
|
|
415
|
+
errorType: 'cwd_not_found',
|
|
416
|
+
durationMs: Date.now() - startTime
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const mergedEnv = Object.assign(Object.assign({}, process.env), env);
|
|
421
|
+
let proc;
|
|
422
|
+
try {
|
|
423
|
+
// Use -lc for login shell (sources .profile), -c for regular
|
|
424
|
+
// const shellArgs = loginShell ? ['-lc', command] : ['-c', command]
|
|
425
|
+
const handledCommand = handleCommand(command);
|
|
426
|
+
console.log(handledCommand);
|
|
427
|
+
proc = (0, child_process_1.spawn)(handledCommand.command, handledCommand.args, {
|
|
428
|
+
cwd,
|
|
429
|
+
env: mergedEnv,
|
|
430
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
resolve({
|
|
435
|
+
stdout: '',
|
|
436
|
+
stderr: `[error]: Failed to spawn process: ${err.message}`,
|
|
437
|
+
exitCode: 1,
|
|
438
|
+
killed: false,
|
|
439
|
+
forceKilled: false,
|
|
440
|
+
errorType: 'spawn_error',
|
|
441
|
+
durationMs: Date.now() - startTime
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
let stdout = '';
|
|
446
|
+
let stderr = '';
|
|
447
|
+
let killed = false;
|
|
448
|
+
let forceKilled = false;
|
|
449
|
+
let completed = false;
|
|
450
|
+
let graceTimer;
|
|
451
|
+
// Batch 5.2: Graceful Timeout - SIGTERM first, then SIGKILL after grace period
|
|
452
|
+
const timer = setTimeout(() => {
|
|
453
|
+
killed = true;
|
|
454
|
+
proc.kill('SIGTERM');
|
|
455
|
+
// Grace period before SIGKILL
|
|
456
|
+
graceTimer = setTimeout(() => {
|
|
457
|
+
if (!completed) {
|
|
458
|
+
proc.kill('SIGKILL');
|
|
459
|
+
forceKilled = true;
|
|
460
|
+
}
|
|
461
|
+
}, GRACEFUL_TIMEOUT_MS);
|
|
462
|
+
}, timeout);
|
|
463
|
+
if (stdin) {
|
|
464
|
+
(_a = proc.stdin) === null || _a === void 0 ? void 0 : _a.write(stdin);
|
|
465
|
+
(_b = proc.stdin) === null || _b === void 0 ? void 0 : _b.end();
|
|
466
|
+
}
|
|
467
|
+
(_c = proc.stdout) === null || _c === void 0 ? void 0 : _c.on('data', data => {
|
|
468
|
+
stdout += data.toString();
|
|
469
|
+
});
|
|
470
|
+
(_d = proc.stderr) === null || _d === void 0 ? void 0 : _d.on('data', data => {
|
|
471
|
+
stderr += data.toString();
|
|
472
|
+
});
|
|
473
|
+
proc.on('close', code => {
|
|
474
|
+
completed = true;
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
if (graceTimer)
|
|
477
|
+
clearTimeout(graceTimer);
|
|
478
|
+
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
|
|
487
|
+
let truncatedStdout = stdout;
|
|
488
|
+
let truncatedStderr = stderr;
|
|
489
|
+
if (truncatedStdout.length > maxOutput) {
|
|
490
|
+
truncatedStdout = middleTruncate(truncatedStdout, maxOutput);
|
|
491
|
+
}
|
|
492
|
+
if (truncatedStderr.length > maxOutput) {
|
|
493
|
+
truncatedStderr = middleTruncate(truncatedStderr, maxOutput);
|
|
494
|
+
}
|
|
495
|
+
const errorType = classifyErrorType(code, killed, forceKilled);
|
|
496
|
+
resolve({
|
|
497
|
+
stdout: truncatedStdout,
|
|
498
|
+
stderr: truncatedStderr,
|
|
499
|
+
exitCode: killed ? EXIT_CODE_TIMEOUT : code,
|
|
500
|
+
killed,
|
|
501
|
+
forceKilled,
|
|
502
|
+
errorType,
|
|
503
|
+
durationMs
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
proc.on('error', err => {
|
|
507
|
+
completed = true;
|
|
508
|
+
clearTimeout(timer);
|
|
509
|
+
if (graceTimer)
|
|
510
|
+
clearTimeout(graceTimer);
|
|
511
|
+
const durationMs = Date.now() - startTime;
|
|
512
|
+
resolve({
|
|
513
|
+
stdout: '',
|
|
514
|
+
stderr: err.message,
|
|
515
|
+
exitCode: 1,
|
|
516
|
+
killed: false,
|
|
517
|
+
forceKilled: false,
|
|
518
|
+
errorType: classifyErrorType(1, false, false, err),
|
|
519
|
+
durationMs
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
server.registerTool('execute_command', {
|
|
525
|
+
description: 'Execute a command directly. Returns stdout, stderr, and exit code.',
|
|
526
|
+
inputSchema: bashInputSchema
|
|
527
|
+
}, async (args) => {
|
|
528
|
+
const command = args.command;
|
|
529
|
+
if (command.includes('&&')) {
|
|
530
|
+
return executeSequenceCallback({
|
|
531
|
+
commands: command.split('&&').map(cmd => ({ command: cmd.trim() })),
|
|
532
|
+
cwd: args.cwd,
|
|
533
|
+
timeout: args.timeout,
|
|
534
|
+
env: args.env,
|
|
535
|
+
maxOutput: args.maxOutput,
|
|
536
|
+
outputFile: args.outputFile
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// Reject sudo commands
|
|
540
|
+
if (containsSudo(command)) {
|
|
541
|
+
return {
|
|
542
|
+
isError: true,
|
|
543
|
+
content: [{ type: 'text', text: `${SUDO_REJECTION_MESSAGE}$ ${command}` }]
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const result = await executeCommand(args);
|
|
547
|
+
// Use formatFullOutput for DRY consistency
|
|
548
|
+
let output = formatResult(result, {
|
|
549
|
+
description: args.description,
|
|
550
|
+
timeout: args.timeout,
|
|
551
|
+
command,
|
|
552
|
+
showTiming: true
|
|
553
|
+
});
|
|
554
|
+
// Add file save notices
|
|
555
|
+
if (args.outputFile) {
|
|
556
|
+
output += `\n[stdout saved to: ${args.outputFile}]`;
|
|
557
|
+
}
|
|
558
|
+
if (args.stderrFile) {
|
|
559
|
+
output += `\n[stderr saved to: ${args.stderrFile}]`;
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
isError: result.exitCode !== 0,
|
|
563
|
+
content: [{ type: 'text', text: output }]
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
const bashSequenceInputSchema = zod_1.default.object({
|
|
567
|
+
commands: zod_1.default
|
|
568
|
+
.array(zod_1.default.object({
|
|
569
|
+
command: zod_1.default.string().describe('The command to execute'),
|
|
570
|
+
description: zod_1.default.string().optional().describe('Short description of what this command does')
|
|
571
|
+
}))
|
|
572
|
+
.describe('Array of commands to execute sequentially in one shell'),
|
|
573
|
+
cwd: zod_1.default.string().describe('Initial working directory'),
|
|
574
|
+
stopOnFailure: zod_1.default
|
|
575
|
+
.boolean()
|
|
576
|
+
.optional()
|
|
577
|
+
.describe('Stop execution if a command fails (default: true)'),
|
|
578
|
+
continueOnCodes: zod_1.default
|
|
579
|
+
.array(zod_1.default.number())
|
|
580
|
+
.optional()
|
|
581
|
+
.describe('Exit codes that are considered success (default: [0])'),
|
|
582
|
+
timeout: zod_1.default
|
|
583
|
+
.number()
|
|
584
|
+
.optional()
|
|
585
|
+
.describe('Overall timeout for all commands in ms (default: 300000)'),
|
|
586
|
+
maxOutput: zod_1.default
|
|
587
|
+
.number()
|
|
588
|
+
.min(1)
|
|
589
|
+
.max(1000000)
|
|
590
|
+
.optional()
|
|
591
|
+
.describe('Maximum output length before middle-truncation (default 30000)'),
|
|
592
|
+
env: zod_1.default
|
|
593
|
+
.record(zod_1.default.string(), zod_1.default.string())
|
|
594
|
+
.optional()
|
|
595
|
+
.describe('Additional environment variables to set'),
|
|
596
|
+
outputFile: zod_1.default.string().optional().describe('File path to save full output (before truncation)')
|
|
597
|
+
});
|
|
598
|
+
const executeSequenceCallback = async (args) => {
|
|
599
|
+
var _a;
|
|
600
|
+
const commands = args.commands;
|
|
601
|
+
// Reject any command containing sudo
|
|
602
|
+
for (let i = 0; i < commands.length; i++) {
|
|
603
|
+
if (containsSudo(commands[i].command)) {
|
|
604
|
+
return {
|
|
605
|
+
isError: true,
|
|
606
|
+
content: [
|
|
607
|
+
{
|
|
608
|
+
type: 'text',
|
|
609
|
+
text: `${SUDO_REJECTION_MESSAGE}[${i + 1}] $ ${commands[i].command}`
|
|
610
|
+
}
|
|
611
|
+
]
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const seqResult = await executeSequence(args);
|
|
616
|
+
// Format output per Batch 4.4 spec
|
|
617
|
+
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}]`;
|
|
628
|
+
if (r.stopped && r.stopReason) {
|
|
629
|
+
body += `\n[stopped - ${r.stopReason}]`;
|
|
630
|
+
}
|
|
631
|
+
const timing = formatTiming(r.durationMs);
|
|
632
|
+
return `${header}\n${cmdLine}\n${body.trim()}\n${timing}`;
|
|
633
|
+
});
|
|
634
|
+
// Summary
|
|
635
|
+
let summary = `\n=== Summary ===`;
|
|
636
|
+
summary += `\nTotal: ${seqResult.totalDurationMs}ms | Executed: ${seqResult.executed}/${commands.length} | Succeeded: ${seqResult.succeeded} | Failed: ${seqResult.failed}`;
|
|
637
|
+
if (seqResult.stoppedAt !== undefined) {
|
|
638
|
+
const stoppedCmd = seqResult.results[seqResult.stoppedAt];
|
|
639
|
+
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}`;
|
|
641
|
+
}
|
|
642
|
+
let response = outputParts.join('\n\n') + summary;
|
|
643
|
+
if (args.outputFile) {
|
|
644
|
+
response += `\n[output saved to: ${args.outputFile}]`;
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
isError: seqResult.failed > 0,
|
|
648
|
+
content: [{ type: 'text', text: response }]
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
server.registerTool('execute_command_sequence', {
|
|
652
|
+
description: 'Execute commands sequentially in a SINGLE shell session (stateful). Unlike parallel, cd/export commands persist between steps. Use for workflows where commands depend on each other. Note: stderr is combined for the whole script and assigned to the last executed command.',
|
|
653
|
+
inputSchema: bashSequenceInputSchema
|
|
654
|
+
}, executeSequenceCallback);
|
|
655
|
+
// =============================================================================
|
|
656
|
+
// Start Server
|
|
657
|
+
// =============================================================================
|
|
658
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
659
|
+
server.connect(transport);
|
|
660
|
+
console.error('Execute Command MCP server v1.0 running');
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baitong-dev/execute-command-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "ExecuteCommandMcpServer.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"@baitong-dev/skills-mcp": "./ExecuteCommandMcpServer.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"ExecuteCommandMcpServer.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"skills"
|
|
16
|
+
],
|
|
17
|
+
"description": "execute-command-mcp",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
20
|
+
"commander": "^14.0.3",
|
|
21
|
+
"zod": "^4.3.4"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
}
|
|
27
|
+
}
|