mcp-agents 0.5.1 → 0.5.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +88 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "MCP server that wraps AI CLI tools (Claude Code, Gemini CLI, Codex CLI) for use by any MCP client",
5
5
  "type": "module",
6
6
  "bin": {
package/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable no-console */
3
3
 
4
- import { execFile, spawn } from "node:child_process";
4
+ import { spawn } from "node:child_process";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -95,6 +95,7 @@ Options:
95
95
  --model <model> Model to use (codex) [default: gpt-5.3-codex]
96
96
  --model_reasoning_effort <e> Reasoning effort (codex) [default: high]
97
97
  --sandbox <bool> Gemini sandbox mode (true/false) [default: false]
98
+ --timeout <seconds> Default timeout per call [default: 300]
98
99
  --help, -h Show this help message
99
100
  --version, -v Show version number`);
100
101
  }
@@ -102,7 +103,7 @@ Options:
102
103
  /**
103
104
  * Parse CLI flags from process.argv.
104
105
  * Handles --help, --version, --provider, --model, --model_reasoning_effort, --sandbox, and unknown flags.
105
- * @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandbox: boolean }}
106
+ * @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandbox: boolean, defaultTimeoutMs?: number }}
106
107
  */
107
108
  function parseArgs() {
108
109
  const args = process.argv.slice(2);
@@ -110,6 +111,7 @@ function parseArgs() {
110
111
  let model;
111
112
  let modelReasoningEffort;
112
113
  let sandbox = false;
114
+ let defaultTimeoutMs;
113
115
 
114
116
  for (let i = 0; i < args.length; i++) {
115
117
  switch (args[i]) {
@@ -153,17 +155,32 @@ function parseArgs() {
153
155
  }
154
156
  sandbox = args[++i] === "true";
155
157
  break;
158
+ case "--timeout": {
159
+ if (i + 1 >= args.length) {
160
+ process.stderr.write("error: --timeout requires a value\n");
161
+ process.exit(1);
162
+ }
163
+ const secs = Number(args[++i]);
164
+ if (!(secs > 0)) {
165
+ process.stderr.write("error: --timeout must be a positive number\n");
166
+ process.exit(1);
167
+ }
168
+ defaultTimeoutMs = Math.round(secs * 1000);
169
+ break;
170
+ }
156
171
  default:
157
172
  process.stderr.write(`error: unknown option: ${args[i]}\n`);
158
173
  process.exit(1);
159
174
  }
160
175
  }
161
176
 
162
- return { provider, model, modelReasoningEffort, sandbox };
177
+ return { provider, model, modelReasoningEffort, sandbox, defaultTimeoutMs };
163
178
  }
164
179
 
165
180
  /**
166
181
  * Run a CLI command and return stdout (or stderr if stdout is empty).
182
+ * Uses spawn with detached:true so the entire process group can be killed
183
+ * on timeout — prevents orphan child processes.
167
184
  * @param {string} command
168
185
  * @param {string[]} args
169
186
  * @param {{ timeoutMs?: number, stdinData?: string }} [opts]
@@ -174,31 +191,17 @@ function runCli(command, args, opts = {}) {
174
191
  const stdinData = opts.stdinData;
175
192
 
176
193
  return new Promise((resolve, reject) => {
177
- const child = execFile(
178
- command,
179
- args,
180
- {
181
- timeout: timeoutMs,
182
- maxBuffer: MAX_BUFFER_BYTES,
183
- env: { ...process.env, NO_COLOR: "1" },
184
- },
185
- (error, stdout, stderr) => {
186
- if (error) {
187
- const details = [
188
- `${command} failed: ${error.message}`,
189
- stderr ? `stderr:\n${stderr}` : null,
190
- ]
191
- .filter(Boolean)
192
- .join("\n");
193
-
194
- reject(new Error(details));
195
- return;
196
- }
197
-
198
- const out = (stdout || stderr || "").trimEnd();
199
- resolve(out);
200
- },
201
- );
194
+ let stdout = "";
195
+ let stderr = "";
196
+ let stdoutLen = 0;
197
+ let stderrLen = 0;
198
+ let settled = false;
199
+
200
+ const child = spawn(command, args, {
201
+ detached: true,
202
+ stdio: ["pipe", "pipe", "pipe"],
203
+ env: { ...process.env, NO_COLOR: "1" },
204
+ });
202
205
 
203
206
  // Pipe prompt via stdin to avoid arg-quoting issues, then close.
204
207
  child.stdin?.on("error", () => {}); // ignore EPIPE if child exits early
@@ -208,8 +211,59 @@ function runCli(command, args, opts = {}) {
208
211
  child.stdin?.end();
209
212
  }
210
213
 
214
+ const killGroup = () => {
215
+ try { process.kill(-child.pid, "SIGKILL"); } catch {}
216
+ };
217
+
218
+ const done = (err) => {
219
+ clearTimeout(timer);
220
+ if (settled) return;
221
+ settled = true;
222
+ err ? reject(err) : resolve((stdout || stderr || "").trimEnd());
223
+ };
224
+
225
+ child.stdout.on("data", (chunk) => {
226
+ stdoutLen += chunk.length;
227
+ if (stdoutLen > MAX_BUFFER_BYTES) {
228
+ killGroup();
229
+ done(new Error(`${command} stdout maxBuffer exceeded`));
230
+ } else {
231
+ stdout += chunk;
232
+ }
233
+ });
234
+
235
+ child.stderr.on("data", (chunk) => {
236
+ stderrLen += chunk.length;
237
+ if (stderrLen > MAX_BUFFER_BYTES) {
238
+ killGroup();
239
+ done(new Error(`${command} stderr maxBuffer exceeded`));
240
+ } else {
241
+ stderr += chunk;
242
+ }
243
+ });
244
+
245
+ // Kill entire process group on timeout (prevents orphan processes).
246
+ const timer = setTimeout(() => {
247
+ killGroup();
248
+ }, timeoutMs);
249
+
211
250
  child.on("error", (err) => {
212
- reject(new Error(`Failed to start ${command}: ${err.message}`));
251
+ done(new Error(`Failed to start ${command}: ${err.message}`));
252
+ });
253
+
254
+ child.on("close", (code, signal) => {
255
+ if (signal || code !== 0) {
256
+ const reason = signal ? `killed by ${signal}` : `exit code ${code}`;
257
+ const details = [
258
+ `${command} failed: ${reason}`,
259
+ stderr ? `stderr:\n${stderr}` : null,
260
+ ]
261
+ .filter(Boolean)
262
+ .join("\n");
263
+ done(new Error(details));
264
+ return;
265
+ }
266
+ done(null);
213
267
  });
214
268
  });
215
269
  }
@@ -250,7 +304,7 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
250
304
  // ---------------------------------------------------------------------------
251
305
 
252
306
  async function main() {
253
- const { provider: providerName, model, modelReasoningEffort, sandbox } = parseArgs();
307
+ const { provider: providerName, model, modelReasoningEffort, sandbox, defaultTimeoutMs } = parseArgs();
254
308
  const backend = CLI_BACKENDS[providerName];
255
309
 
256
310
  if (!backend) {
@@ -274,6 +328,8 @@ async function main() {
274
328
  { capabilities: { tools: {} } },
275
329
  );
276
330
 
331
+ const effectiveTimeout = defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
332
+
277
333
  const properties = {
278
334
  prompt: {
279
335
  type: "string",
@@ -282,7 +338,7 @@ async function main() {
282
338
  timeout_ms: {
283
339
  type: "integer",
284
340
  minimum: 1,
285
- description: `Optional timeout override (default ${DEFAULT_TIMEOUT_MS})`,
341
+ description: `Optional timeout override (default ${effectiveTimeout}ms)`,
286
342
  },
287
343
  ...backend.extraProperties,
288
344
  };
@@ -333,7 +389,7 @@ async function main() {
333
389
  const timeoutMsRaw = params.arguments?.timeout_ms;
334
390
  const timeoutMs = Number.isInteger(timeoutMsRaw)
335
391
  ? timeoutMsRaw
336
- : DEFAULT_TIMEOUT_MS;
392
+ : effectiveTimeout;
337
393
 
338
394
  if (!prompt.trim()) {
339
395
  return {