mcp-server-commands 0.7.4 → 0.8.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/README.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## `runProcess` tool
2
+
3
+ The `runProcess` tool runs processes on the host machine. There are two mutually exclusive ways to invoke it:
4
+
5
+ 1. **`command_line`** (string) — Executed via the system's default shell (just like typing into `bash`/`fish`/`pwsh`/etc). Shell features like pipes, redirects, and variable expansion all work.
6
+ 2. **`argv`** (string array) — Direct executable invocation. `argv[0]` is the executable, the rest are arguments. No shell interpretation.
7
+
8
+ You cannot pass both. The tool infers whether to use a shell from which parameter you provide.
9
+
10
+ If you want your model to use specific shell(s) on a system, I would list them in your system prompt. Or, maybe in your tool instructions, though models tend to pay better attention to examples in a system prompt.
11
+
12
+ Let me know if you encounter problems!
13
+
1
14
  ## Tools
2
15
 
3
16
  Tools are for LLMs to request. Claude Sonnet 3.5 intelligently uses `run_process`. And, initial testing shows promising results with [Groq Desktop with MCP](https://github.com/groq/groq-desktop-beta) and `llama4` models.
@@ -1,21 +1,128 @@
1
1
  // TODO cleanup exec usages once spawn is ready
2
2
  import { spawn } from "child_process";
3
- export async function spawn_wrapped(command, args, stdin, options) {
4
- return new Promise((resolve, reject) => {
5
- if (!stdin) {
6
- // FYI default is all 'pipe' (works when stdin is provided)
7
- // 'ignore' attaches /dev/null
8
- // order: [STDIN, STDOUT, STDERR]
3
+ import { performance } from "perf_hooks";
4
+ import { is_verbose, verbose_log } from "./logging.js";
5
+ import { resultFor } from "./messages.js";
6
+ import { errorResult } from "./messages.js";
7
+ /**
8
+ * Helper class that provides typed getters for the keys accepted by
9
+ * {@link runProcess}. It wraps a {@link RunProcessArgs} object and casts the
10
+ * values to the expected runtime types.
11
+ */
12
+ export class RunProcessArgsHelper {
13
+ raw;
14
+ constructor(raw) {
15
+ this.raw = raw ?? {};
16
+ }
17
+ /** Working directory – string if supplied, otherwise undefined */
18
+ get cwd() {
19
+ const v = this.raw.cwd;
20
+ return v == null ? undefined : String(v);
21
+ }
22
+ /** Text to write to STDIN – string if supplied, otherwise undefined */
23
+ get stdin_text() {
24
+ const v = this.raw.stdin_text;
25
+ return v == null ? undefined : String(v);
26
+ }
27
+ /** Shell command line – string if supplied, otherwise undefined */
28
+ get commandLine() {
29
+ const v = this.raw.command_line;
30
+ return v == null ? undefined : String(v);
31
+ }
32
+ /** Executable argv – array of strings if supplied, otherwise undefined */
33
+ get argv() {
34
+ const v = this.raw.argv;
35
+ if (!Array.isArray(v))
36
+ return undefined;
37
+ return v.map((item) => String(item));
38
+ }
39
+ /** Timeout in milliseconds – number if supplied, otherwise undefined */
40
+ /** Timeout in milliseconds – always a number (default 30_000) */
41
+ get timeoutMs() {
42
+ const v = this.raw.timeout_ms;
43
+ const n = Number(v);
44
+ return Number.isNaN(n) ? 30_000 : n;
45
+ }
46
+ /** True if a shell command line is provided */
47
+ get isShellMode() {
48
+ return Boolean(this.commandLine);
49
+ }
50
+ /** True if an argv array with at least one element is provided */
51
+ get isExecutableMode() {
52
+ return Array.isArray(this.raw.argv) && (this.argv?.length ?? 0) > 0;
53
+ }
54
+ }
55
+ export function runProcess(runProcessArgs) {
56
+ const startTime = performance.now();
57
+ const args = new RunProcessArgsHelper(runProcessArgs);
58
+ if (args.isShellMode && args.isExecutableMode) {
59
+ return Promise.resolve(errorResult("Cannot pass both 'command_line' and 'argv'. Use one or the other."));
60
+ }
61
+ if (!args.isShellMode && !args.isExecutableMode) {
62
+ return Promise.resolve(errorResult("Either 'command_line' (string) or 'argv' (array) is required."));
63
+ }
64
+ const options = {
65
+ // spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
66
+ encoding: "utf8"
67
+ };
68
+ if (args.cwd) {
69
+ options.cwd = args.cwd;
70
+ }
71
+ let spawnCommand = "";
72
+ let spawnArgs = [];
73
+ if (args.isShellMode) {
74
+ options.shell = true;
75
+ spawnCommand = String(args.commandLine);
76
+ spawnArgs = [];
77
+ }
78
+ else {
79
+ options.shell = false;
80
+ const argv = args.argv;
81
+ spawnCommand = argv[0];
82
+ spawnArgs = argv.slice(1);
83
+ }
84
+ const logWithElapsedTime = (msg, ...rest) => {
85
+ if (!is_verbose)
86
+ return;
87
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(3);
88
+ verbose_log(`[${elapsed}s] ${msg}`, ...rest, spawnCommand, spawnArgs);
89
+ };
90
+ let child_pid;
91
+ const promise = new Promise((resolve, reject) => {
92
+ if (!args.stdin_text) {
93
+ // PRN windowsHide on Windows, signal, killSignal
94
+ // FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe']
95
+ // order: [STDIN, STDOUT, STDERR]
96
+ // https://nodejs.org/api/child_process.html#optionsstdio
97
+ // 'ignore' attaches /dev/null
98
+ // do not set 'inherit' (causes ripgrep to see STDIN socket and search it, thus hanging)
9
99
  options.stdio = ['ignore', 'pipe', 'pipe'];
10
- // ? I wonder if this was related to fishWorkaround issue w/ STDIN (see commit history)... I was using base64 encoding to pass STDIN
11
100
  }
12
- const child = spawn(command, args, options);
13
- // PRN return pid to callers?
14
- // console.log(`child.pid: ${child.pid}`);
101
+ // remove timeout on spawn options (if set) so the built‑in spawn timeout does not interfere
102
+ delete options.timeout;
103
+ // Use a detached child so we can kill the entire process group.
104
+ options.detached = true;
105
+ let settled = false;
106
+ const settle = (result, isError) => {
107
+ if (settled)
108
+ return;
109
+ settled = true;
110
+ if (timer)
111
+ clearTimeout(timer);
112
+ if (isError) {
113
+ resolve(resultFor(result));
114
+ }
115
+ else {
116
+ resolve(resultFor(result));
117
+ }
118
+ };
119
+ const child = spawn(spawnCommand, spawnArgs, options);
120
+ logWithElapsedTime(`START SPAWN child.pid: ${child.pid}`);
121
+ child_pid = child.pid;
15
122
  let stdout = "";
16
123
  let stderr = "";
17
- if (child.stdin && stdin) {
18
- child.stdin.write(stdin);
124
+ if (child.stdin && args.stdin_text) {
125
+ child.stdin.write(args.stdin_text);
19
126
  child.stdin.end();
20
127
  }
21
128
  if (child.stdout) {
@@ -28,45 +135,90 @@ export async function spawn_wrapped(command, args, stdin, options) {
28
135
  stderr += chunk.toString();
29
136
  });
30
137
  }
31
- let errored = false;
138
+ child.on("exit", (code, signal) => {
139
+ // child process streams MAY still be open when EXIT is emitted (use close if need to ensure they're closed)
140
+ // "close" will come after "exit" once process is terminated + streams are closed
141
+ // so for now use "close" to determine if process was terminated too, that way you can access STDOUT/STDERR reliably for returning full output to agent
142
+ logWithElapsedTime("EXIT", { code, signal });
143
+ });
144
+ child.on("spawn", () => {
145
+ // emitted after child process starts successfully
146
+ // if child doesn't start, error emitted instead
147
+ // emitted BEFORE any data received via stdout/stderr
148
+ logWithElapsedTime("SPAWN");
149
+ });
150
+ // Timeout handling – kill the whole process group after the supplied timeout.
151
+ let timer = null;
152
+ timer = setTimeout(() => {
153
+ if (process.platform !== "win32") {
154
+ if (child.pid) {
155
+ try {
156
+ process.kill(-child.pid, "SIGTERM");
157
+ }
158
+ catch (_) { }
159
+ }
160
+ }
161
+ else {
162
+ child.kill("SIGTERM");
163
+ }
164
+ const killTimeout = setTimeout(() => {
165
+ if (process.platform !== "win32") {
166
+ if (child.pid) {
167
+ try {
168
+ process.kill(-child.pid, "SIGKILL");
169
+ }
170
+ catch (_) { }
171
+ }
172
+ }
173
+ else {
174
+ child.kill("SIGKILL");
175
+ }
176
+ }, 2000);
177
+ const clearKill = () => clearTimeout(killTimeout);
178
+ child.once("exit", clearKill);
179
+ child.once("close", clearKill);
180
+ }, args.timeoutMs);
32
181
  child.on("error", (err) => {
182
+ logWithElapsedTime("ERROR");
33
183
  // ChildProcess 'error' docs: https://nodejs.org/api/child_process.html#event-error
34
184
  // error running process
35
185
  // IIUC not just b/c of command failed w/ non-zero exit code
36
186
  const result = {
37
- stdout: stdout,
38
- stderr: stderr,
187
+ stdout,
188
+ stderr,
39
189
  //
40
- code: err.code,
41
- signal: err.signal,
190
+ // one of these will always be non-null
191
+ code: err.code, // set if process exited, else null
192
+ signal: err.signal, // set if process was terminated by signal, else null
42
193
  //
43
194
  message: err.message,
44
- // killed: (err as any).killed,
45
- cmd: command, // TODO does error have .cmd on it? is it the composite result of processing in spawn too? (or same as I passed)
195
+ // ? killed: (err as any).killed
46
196
  };
47
- // console.log("ON_ERROR", result);
48
- errored = true;
49
- reject(result);
197
+ logWithElapsedTime("ERROR_RESULT", result);
198
+ settle(result, true);
50
199
  });
51
200
  child.on("close", (code, signal) => {
201
+ logWithElapsedTime("CLOSE");
52
202
  // ChildProcess 'close' docs: https://nodejs.org/api/child_process.html#event-close
53
203
  // 'close' is after child process ends AND stdio streams are closed
54
204
  // - after 'exit' or 'error'
55
- //
56
- // * do not resolve if 'error' already called (promise is already resolved)
57
- if (errored)
58
- return;
59
205
  // either code is set, or signal, but NOT BOTH
60
206
  // signal if process killed
61
207
  // FYI close does not mean code==0
62
208
  const result = {
63
- stdout: stdout,
64
- stderr: stderr,
209
+ stdout,
210
+ stderr,
211
+ //
65
212
  code: code ?? undefined,
66
213
  signal: signal ?? undefined,
214
+ //
67
215
  };
68
- // console.log("ON_CLOSE", result);
69
- resolve(result);
216
+ logWithElapsedTime("CLOSE_RESULT", result);
217
+ settle(result, false);
70
218
  });
71
219
  });
220
+ // FYI later (when needed) I can map this onto the promise that comes back from runProcess too (and tie into that unit test I have that needs pid to terminate it)
221
+ // Resolve the underlying spawn result, then map to CallToolResult including PID.
222
+ promise.pid = child_pid;
223
+ return promise;
72
224
  }
package/build/index.js CHANGED
@@ -4,7 +4,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { createRequire } from "module";
6
6
  import { registerPrompts } from "./prompts.js";
7
- import { reisterTools } from "./tools.js";
7
+ import { registerTools } from "./tools.js";
8
8
  const require = createRequire(import.meta.url);
9
9
  const { name: package_name, version: package_version, } = require("../package.json");
10
10
  const server = new Server({
@@ -19,7 +19,7 @@ const server = new Server({
19
19
  //logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
20
20
  },
21
21
  });
22
- reisterTools(server);
22
+ registerTools(server);
23
23
  registerPrompts(server);
24
24
  async function main() {
25
25
  const transport = new StdioServerTransport();
@@ -0,0 +1,51 @@
1
+ import fs from 'fs';
2
+ export let is_verbose = false;
3
+ if (process.argv.includes("--verbose")) {
4
+ is_verbose = true;
5
+ }
6
+ const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
7
+ if (isJest) {
8
+ // is_verbose = true; // comment out to disable verbose logging in JEST test runner
9
+ }
10
+ if (is_verbose) {
11
+ always_log("INFO: verbose logging enabled");
12
+ }
13
+ export function verbose_log(message, data) {
14
+ if (is_verbose) {
15
+ always_log(message, data);
16
+ }
17
+ }
18
+ export function always_log(message, data) {
19
+ // LOGGING NOTES:
20
+ // https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
21
+ // * STDIO transport => STDERR captured by host app (i.e. Claude Desktop which writes to ~/Library/Logs/Claude/mcp.log)
22
+ // * Streamable HTTP transport => suggests server side capture (i.e. log file)
23
+ // * and/or log to MCP client regardless of transport
24
+ // server.sendLoggingMessage({
25
+ // level: "info",
26
+ // data: message,
27
+ // });
28
+ // // sends a message like:
29
+ // server.notification({
30
+ // method: "notifications/message",
31
+ // params: {
32
+ // level: "warning",
33
+ // logger: "mcp-server-commands",
34
+ // data: "ListToolsRequest2",
35
+ // },
36
+ // });
37
+ if (isJest) {
38
+ // * JEST => log to console so I can see in test runner output
39
+ console.log(`${message}${data ? ": " + JSON.stringify(data) : ""}`);
40
+ return;
41
+ }
42
+ // * all transports => server side log file
43
+ const timestamp = new Date().toISOString();
44
+ const logMessage = `[${timestamp}] ${message}${data ? ": " + JSON.stringify(data) : ""}`;
45
+ const shareDir = process.env.HOME + "/.local/share/mcp-server-commands/";
46
+ const logFile = shareDir + "/commands.log";
47
+ fs.mkdirSync(shareDir, { recursive: true });
48
+ fs.appendFileSync(logFile, logMessage + "\n");
49
+ // TODO any hail mary if this logging fails? just use console.error then?
50
+ // else still can get seemingly hung tool calls
51
+ }
package/build/prompts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GetPromptRequestSchema, ListPromptsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
- import { verbose_log } from "./always_log.js";
2
+ import { verbose_log } from "./logging.js";
3
3
  import { exec } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  const execAsync = promisify(exec);
@@ -20,10 +20,8 @@ export function registerPrompts(server) {
20
20
  description: "Include command output in the prompt. " +
21
21
  "This is effectively a user tool call.",
22
22
  arguments: [
23
- { name: "mode", },
24
23
  { name: "command_line", },
25
24
  { name: "argv", },
26
- { name: "dry_run", },
27
25
  // ? other args?
28
26
  ],
29
27
  },
@@ -41,8 +39,6 @@ export function registerPrompts(server) {
41
39
  throw new Error("run_process not yet ported to prompts");
42
40
  const command_line = String(request.params.arguments?.command_line);
43
41
  const argv = String(request.params.arguments?.argv);
44
- const mode = String(request.params.arguments?.mode);
45
- const dry_run = Boolean(request.params.arguments?.dry_run);
46
42
  // Is it possible/feasible to pass a path for the workdir when running the command?
47
43
  // - currently it uses / (yikez)
48
44
  // - IMO makes more sense to have it be based on the Zed workdir of each project
package/build/tools.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import os from "os";
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
- import { verbose_log } from "./always_log.js";
4
- import { runProcess } from "./run_process.js";
5
- export function reisterTools(server) {
3
+ import { verbose_log } from "./logging.js";
4
+ import { runProcess } from "./exec-utils.js";
5
+ export function registerTools(server) {
6
6
  server.setRequestHandler(ListToolsRequestSchema, async () => {
7
7
  verbose_log("INFO: ListTools");
8
8
  return {
@@ -10,6 +10,8 @@ export function reisterTools(server) {
10
10
  // https://modelcontextprotocol.io/docs/learn/architecture#understanding-the-tool-execution-request // tool request/response
11
11
  // typescript SDK docs:
12
12
  // servers: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md
13
+ // TODO upgrade to newer version AND check if STDIO delimiter style has changed to include content-length "header" before responses?
14
+ // OR is there an opt-in for this style vs what I get with my simple nvim uv.spawn nvim client... where \n terminates/delimits each message
13
15
  tools: [
14
16
  {
15
17
  // TODO RUN_PROCESS MIGRATION! provide examples in system message, that way it is very clear how to use these!
@@ -19,63 +21,32 @@ export function reisterTools(server) {
19
21
  type: "object",
20
22
  properties: {
21
23
  // ListToolsResult => Tool type (in protocol) => https://modelcontextprotocol.io/specification/2025-06-18/schema#tool
22
- // * appears I can do w/e I want to describe properties (as long as give a string key (name)) => hence enum below
23
- mode: {
24
- enum: ["shell", "executable"], // * I made this up
25
- description: "What are you running, two choices: 'shell' or 'executable' (required, no default)",
26
- // FYI only use default system shell, if the model wants a different shell, can explicitly run it with command_line/argv
27
- // DO NOT duplicate the mechanism of specifying what runs by adding yet another executable field!
28
- // spawn's options.shell is bool/string... string is command_name/path... that duplicates and complicates needlessly!
29
- },
30
24
  command_line: {
31
25
  type: "string",
32
- description: "Shell command line. Required when mode='shell'. Forbidden when mode='executable'."
26
+ description: "Shell mode: a shell command line executed via the system's default shell. Supports pipes, redirects, globbing. Cannot be combined with 'argv'."
33
27
  },
34
28
  argv: {
35
29
  minItems: 1, // * made up too
36
30
  type: "array",
37
31
  items: { type: "string" },
38
- description: "Executable and arguments. argv[0] is the executable. Required when mode='executable'. Forbidden when mode='shell'."
32
+ description: "Executable mode: directly spawn a process. argv[0] is the executable, followed by arguments passed verbatim (no shell interpretation). Cannot be combined with 'command_line'."
39
33
  },
40
34
  cwd: {
41
35
  // or "workdir" like before? => eval model behavior w/ each name?
42
36
  type: "string",
43
37
  description: "Optional to set working directory",
44
38
  },
45
- stdin: {
39
+ stdin_text: {
46
40
  type: "string",
47
41
  description: "Optional text written to STDIN (written fully, then closed). Useful for heredoc-style input or file contents."
48
42
  },
49
43
  timeout_ms: {
50
44
  type: "number",
51
45
  description: "Optional timeout in milliseconds, defaults to 30,000ms",
52
- },
53
- dry_run: {
54
- type: "boolean",
55
- description: "Optional explain what would run, defaults to False."
56
- // FYI this can help avoid the need for logging this information, I can call this as a user too!
57
- // TODO, dump info about shell (if applicable) and program versions!
58
- // - explain the node calls too, i.e. spawn('echo foo')
59
- // - show paths of resolved resources (i.e. shell)
60
- // - i.e. could run command --version to see its version (attempt various flags until one works, and/or have a lookup of common tools to know how to find their version)
61
46
  }
62
- // MAYBEs:
63
- // - env - obscure cases where command takes a param only via an env var?
64
47
  },
65
- required: ["mode"],
66
- // PRN add proprietary constraints explanation? only if model seems to struggle and there isn't a better fix...
67
- // oneOf: [
68
- // {
69
- // properties: { mode: { const: "shell" } },
70
- // required: ["command_line"],
71
- // not: { required: ["argv"] }
72
- // },
73
- // {
74
- // properties: { mode: { const: "executable" } },
75
- // required: ["argv"],
76
- // not: { required: ["command_line"] }
77
- // }
78
- // ]
48
+ // FYI no required arg top level and I am not gonna fret about specifiying one or the other, the tool definition is fine with that distinction in the descriptions, plus it is intuitive.
49
+ // and back when I had mode=shell/executable required, models would still forget to add it so why bother with a huge complexity in tool definition
79
50
  },
80
51
  },
81
52
  ],
@@ -85,7 +56,13 @@ export function reisterTools(server) {
85
56
  verbose_log("INFO: ToolRequest", request);
86
57
  switch (request.params.name) {
87
58
  case "run_process": {
88
- return await runProcess(request.params.arguments);
59
+ if (!request.params.arguments) {
60
+ throw new Error("Missing arguments for run_process");
61
+ }
62
+ const result = await runProcess(request.params.arguments);
63
+ // FYI logging this response is INVALUABLE! found a problem with my neovim MCP STDIO client!
64
+ verbose_log("INFO: ToolResponse", result);
65
+ return result;
89
66
  }
90
67
  default:
91
68
  throw new Error("Unknown tool");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-commands",
3
- "version": "0.7.4",
3
+ "version": "0.8.1",
4
4
  "description": "An MCP server to run arbitrary commands",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,52 +0,0 @@
1
- let verbose = false;
2
- // check CLI args:
3
- if (process.argv.includes("--verbose")) {
4
- verbose = true;
5
- }
6
- if (verbose) {
7
- always_log("INFO: verbose logging enabled");
8
- }
9
- export function verbose_log(message, data) {
10
- // https://modelcontextprotocol.io/docs/tools/debugging - mentions various ways to debug/troubleshoot (including dev tools)
11
- //
12
- // remember STDIO transport means can't log over STDOUT (client expects JSON messages per the spec)
13
- // https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
14
- // mentions STDERR is captured by the host app (i.e. Claude Desktop app)
15
- // server.sendLoggingMessage is captured by MCP client (not Claude Desktop app)
16
- // * SO, IIUC use STDERR for logging into Claude Desktop app logs in:
17
- // '~/Library/Logs/Claude/mcp.log'
18
- if (verbose) {
19
- always_log(message, data);
20
- }
21
- // inspector, catches these logs and shows them on left hand side of screen (sidebar)
22
- // TODO add verbose parameter (CLI arg?)
23
- // IF I wanted to log via MCP client logs (not sure what those are/do):
24
- // I do not see inspector catching these logs :(, there is a server notifications section and it remains empty
25
- //server.sendLoggingMessage({
26
- // level: "info",
27
- // data: message,
28
- //});
29
- // which results in something like:
30
- //server.notification({
31
- // method: "notifications/message",
32
- // params: {
33
- // level: "warning",
34
- // logger: "mcp-server-commands",
35
- // data: "ListToolsRequest2",
36
- // },
37
- //});
38
- //
39
- // FYI client should also requets a log level from the server, so that needs to be here at some point too
40
- }
41
- export function always_log(message, data) {
42
- const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
43
- if (isJest) {
44
- return;
45
- }
46
- if (data) {
47
- console.error(message + ": " + JSON.stringify(data));
48
- }
49
- else {
50
- console.error(message);
51
- }
52
- }
@@ -1,90 +0,0 @@
1
- import { spawn_wrapped } from "./exec-utils.js";
2
- import { always_log } from "./always_log.js";
3
- import { errorResult, messagesFor, resultFor } from "./messages.js";
4
- export async function runProcess(args) {
5
- const command_line = args?.command_line;
6
- const argv = args?.argv;
7
- const mode = String(args?.mode);
8
- if (!mode) {
9
- return errorResult("Mode is required");
10
- }
11
- const isShell = mode === "shell";
12
- const isExecutable = mode === "executable";
13
- if (!isShell && !isExecutable) {
14
- return errorResult(`Invalid mode '${mode}'. Allowed values are 'shell' or 'executable'.`);
15
- }
16
- // * shared args
17
- const spawn_options = {
18
- // spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
19
- encoding: "utf8"
20
- };
21
- if (args?.cwd) {
22
- spawn_options.cwd = String(args.cwd);
23
- }
24
- // PRN args.env
25
- if (args?.timeout_ms) {
26
- spawn_options.timeout = Number(args.timeout_ms);
27
- }
28
- else {
29
- // default timeout after 30s
30
- spawn_options.timeout = 30000;
31
- }
32
- // PRN windowsHide on Windows, signal, killSignal
33
- // FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe'] https://nodejs.org/api/child_process.html#optionsstdio
34
- // do not set inherit (this is what causes ripgrep to see STDIN socket and search it, thus hanging)
35
- const stdin = args?.stdin ? String(args.stdin) : undefined; // TODO
36
- const dryRun = Boolean(args?.dry_run);
37
- try {
38
- if (dryRun) {
39
- // Build a descriptive plan without executing anything
40
- let plan = '';
41
- if (isShell) {
42
- const cmd = String(args?.command_line);
43
- const shell = process.env.SHELL || (process.platform === 'win32' ? 'cmd.exe' : '/bin/sh');
44
- plan = `Shell mode: will execute command_line via ${shell}: ${cmd}`;
45
- }
46
- else if (isExecutable) {
47
- const argv = args?.argv;
48
- plan = `Executable mode: will spawn '${argv[0]}' with arguments ${JSON.stringify(argv.slice(1))}`;
49
- }
50
- return { content: [{ name: "PLAN", type: "text", text: plan }] };
51
- }
52
- if (isShell) {
53
- if (!args?.command_line) {
54
- return errorResult("Mode 'shell' requires a non‑empty 'command_line' parameter.");
55
- }
56
- if (args?.argv) {
57
- return errorResult("Mode 'shell' does not accept an 'argv' parameter.");
58
- }
59
- spawn_options.shell = true;
60
- const command_line = String(args?.command_line);
61
- const result = await spawn_wrapped(command_line, [], stdin, spawn_options);
62
- return resultFor(result);
63
- }
64
- if (isExecutable) {
65
- const argv = args?.argv;
66
- if (!Array.isArray(argv) || argv.length === 0) {
67
- return errorResult("Mode 'executable' requires a non‑empty 'argv' array.");
68
- }
69
- if (args?.command_line) {
70
- return errorResult("Mode 'executable' does not accept a 'command_line' parameter.");
71
- }
72
- spawn_options.shell = false;
73
- const command = argv[0];
74
- const commandArgs = argv.slice(1);
75
- const result = await spawn_wrapped(command, commandArgs, stdin, spawn_options);
76
- return resultFor(result);
77
- }
78
- // TODO... fish shell workaround (see exec-utils.ts) - ADD or FIX via a TEST FIRST
79
- return errorResult("Unexpected execution path in runProcess, should not be possible");
80
- }
81
- catch (error) {
82
- // TODO check if not SpawnFailure => i.e. test aborting/killing
83
- const response = {
84
- isError: true,
85
- content: messagesFor(error),
86
- };
87
- always_log("WARN: run_process failed", error);
88
- return response;
89
- }
90
- }