Skip to main content
Glama
g0t4

mcp-server-commands

by g0t4

run_process

Execute a shell command or spawn a process on Linux with configurable arguments, working directory, stdin, and timeout.

Instructions

Run a process on this linux machine

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
command_lineNoShell mode: a shell command line executed via the system's default shell. Supports pipes, redirects, globbing. Cannot be combined with 'argv'.
argvNoExecutable mode: directly spawn a process. argv[0] is the executable, followed by arguments passed verbatim (no shell interpretation). Cannot be combined with 'command_line'.
cwdNoOptional to set working directory
stdin_textNoOptional text written to STDIN (written fully, then closed). Useful for heredoc-style input or file contents.
timeout_msNoOptional timeout in milliseconds, defaults to 30,000ms

Implementation Reference

  • The main handler function `runProcess()` that executes the tool logic. It accepts RunProcessArgs, spawns a child process (either shell mode via command_line or executable mode via argv), handles stdin input, timeout with SIGTERM/SIGKILL, collects stdout/stderr, and returns a CallToolResult promise.
    export function runProcess(
        runProcessArgs: RunProcessArgs,
    ): SpawnPromise {
        const startTime = performance.now();
    
        const args = new RunProcessArgsHelper(runProcessArgs);
        if (args.isShellMode && args.isExecutableMode) {
            return Promise.resolve(errorResult("Cannot pass both 'command_line' and 'argv'. Use one or the other."));
        }
        if (!args.isShellMode && !args.isExecutableMode) {
            return Promise.resolve(errorResult("Either 'command_line' (string) or 'argv' (array) is required."));
        }
    
        const options: ObjectEncodingOptions & SpawnOptions = {
            // spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
            encoding: "utf8"
        };
        if (args.cwd) {
            options.cwd = args.cwd;
        }
    
        let spawnCommand = "";
        let spawnArgs: string[] = [];
        if (args.isShellMode) {
            (options as any).shell = true;
            spawnCommand = String(args.commandLine);
            spawnArgs = [];
        } else {
            (options as any).shell = false;
            const argv = args.argv as string[];
            spawnCommand = argv[0];
            spawnArgs = argv.slice(1);
        }
    
        const logWithElapsedTime = (msg: string, ...rest: any[]) => {
            if (!is_verbose) return;
    
            const elapsed = ((performance.now() - startTime) / 1000).toFixed(3);
            verbose_log(`[${elapsed}s] ${msg}`, ...rest, spawnCommand, spawnArgs);
        };
    
        let child_pid;
        const promise: SpawnPromise = new Promise<CallToolResult>((resolve, reject) => {
            if (!args.stdin_text) {
                // PRN windowsHide on Windows, signal, killSignal
                // FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe'] 
                //     order: [STDIN, STDOUT, STDERR]
                //     https://nodejs.org/api/child_process.html#optionsstdio 
                //     'ignore' attaches /dev/null
                //     do not set 'inherit' (causes ripgrep to see STDIN socket and search it, thus hanging)
                options.stdio = ['ignore', 'pipe', 'pipe'];
            }
    
            // remove timeout on spawn options (if set) so the built‑in spawn timeout does not interfere
            delete (options as any).timeout;
    
            // Use a detached child so we can kill the entire process group.
            options.detached = true;
    
            let settled = false;
            const settle = (result: SpawnResult, isError: boolean) => {
                if (settled) return;
                settled = true;
                if (timer) clearTimeout(timer);
                if (isError) {
                    resolve(resultFor(result));
                } else {
                    resolve(resultFor(result));
                }
            };
    
            const child = spawn(spawnCommand, spawnArgs, options);
            logWithElapsedTime(`START SPAWN child.pid: ${child.pid}`);
            child_pid = child.pid;
    
            let stdout = "";
            let stderr = "";
    
            if (child.stdin && args.stdin_text) {
                child.stdin.write(args.stdin_text);
                child.stdin.end();
            }
    
            if (child.stdout) {
                child.stdout.on("data", (chunk: Buffer | string) => {
                    stdout += chunk.toString();
                });
            }
    
            if (child.stderr) {
                child.stderr.on("data", (chunk: Buffer | string) => {
                    stderr += chunk.toString();
                });
            }
    
            child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
                // child process streams MAY still be open when EXIT is emitted (use close if need to ensure they're closed)
                // "close" will come after "exit" once process is terminated + streams are closed
                // 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
                logWithElapsedTime("EXIT", { code, signal });
            });
            child.on("spawn", () => {
                // emitted after child process starts successfully
                // if child doesn't start, error emitted instead
                // emitted BEFORE any data received via stdout/stderr
                logWithElapsedTime("SPAWN")
            });
    
            // Timeout handling – kill the whole process group after the supplied timeout.
            let timer: NodeJS.Timeout | null = null;
    
            timer = setTimeout(() => {
                if (process.platform !== "win32") {
                    if (child.pid) { try { process.kill(-child.pid, "SIGTERM"); } catch (_) {} }
                } else {
                    child.kill("SIGTERM");
                }
                const killTimeout = setTimeout(() => {
                    if (process.platform !== "win32") {
                        if (child.pid) { try { process.kill(-child.pid, "SIGKILL"); } catch (_) {} }
                    } else {
                        child.kill("SIGKILL");
                    }
                }, 2000);
                const clearKill = () => clearTimeout(killTimeout);
                child.once("exit", clearKill);
                child.once("close", clearKill);
            }, args.timeoutMs);
    
            child.on("error", (err: Error) => {
                logWithElapsedTime("ERROR");
                // ChildProcess 'error' docs: https://nodejs.org/api/child_process.html#event-error
                // error running process
                // IIUC not just b/c of command failed w/ non-zero exit code
                const result: SpawnFailure = {
                    stdout,
                    stderr,
                    //
                    // one of these will always be non-null
                    code: (err as any).code, // set if process exited, else null
                    signal: (err as any).signal, // set if process was terminated by signal, else null
                    //
                    message: err.message,
                    // ? killed: (err as any).killed
                };
                logWithElapsedTime("ERROR_RESULT", result);
                settle(result, true);
            });
    
            child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
                logWithElapsedTime("CLOSE");
                // ChildProcess 'close' docs: https://nodejs.org/api/child_process.html#event-close
                //   'close' is after child process ends AND stdio streams are closed
                //   - after 'exit' or 'error'
    
                //   either code is set, or signal, but NOT BOTH
                //   signal if process killed
                // FYI close does not mean code==0
                const result: SpawnResult = {
                    stdout,
                    stderr,
                    //
                    code: code ?? undefined,
                    signal: signal ?? undefined,
                    //
                };
                logWithElapsedTime("CLOSE_RESULT", result);
                settle(result, false);
            });
        });
        // 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)
        // Resolve the underlying spawn result, then map to CallToolResult including PID.
        promise.pid = child_pid;
        return promise
    }
  • Type definitions: RunProcessArgs (Record<string, unknown>), RunProcessArgsHelper class with typed getters (cwd, stdin_text, commandLine, argv, timeoutMs), and SpawnResult/SpawnFailure types.
    /**
     * Raw arguments passed to {@link runProcess}. Historically this was a loose
     * {@link Record} but we now expose a typed helper for safer access.
     */
    export type RunProcessArgs = Record<string, unknown>;
    
    /**
     * Helper class that provides typed getters for the keys accepted by
     * {@link runProcess}. It wraps a {@link RunProcessArgs} object and casts the
     * values to the expected runtime types.
     */
    export class RunProcessArgsHelper {
        private readonly raw: RunProcessArgs;
    
        constructor(raw: RunProcessArgs) {
            this.raw = raw ?? {};
        }
    
        /** Working directory – string if supplied, otherwise undefined */
        get cwd(): string | undefined {
            const v = this.raw.cwd;
            return v == null ? undefined : String(v);
        }
    
        /** Text to write to STDIN – string if supplied, otherwise undefined */
        get stdin_text(): string | undefined {
            const v = this.raw.stdin_text;
            return v == null ? undefined : String(v);
        }
    
        /** Shell command line – string if supplied, otherwise undefined */
        get commandLine(): string | undefined {
            const v = this.raw.command_line;
            return v == null ? undefined : String(v);
        }
    
        /** Executable argv – array of strings if supplied, otherwise undefined */
        get argv(): string[] | undefined {
            const v = this.raw.argv;
            if (!Array.isArray(v)) return undefined;
            return v.map((item) => String(item));
        }
    
        /** Timeout in milliseconds – number if supplied, otherwise undefined */
        /** Timeout in milliseconds – always a number (default 30_000) */
        get timeoutMs(): number {
            const v = this.raw.timeout_ms;
            const n = Number(v);
            return Number.isNaN(n) ? 30_000 : n;
        }
    
        /** True if a shell command line is provided */
        get isShellMode(): boolean {
            return Boolean(this.commandLine);
        }
    
        /** True if an argv array with at least one element is provided */
        get isExecutableMode(): boolean {
            return Array.isArray(this.raw.argv) && (this.argv?.length ?? 0) > 0;
        }
    }
  • src/tools.ts:12-82 (registration)
    The `registerTools()` function registers the tool on the MCP server. It sets ListToolsRequestSchema handler (tool definition with name 'run_process', description, inputSchema) and CallToolRequestSchema handler (routes to runProcess()).
    export function registerTools(server: Server) {
        server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
            verbose_log("INFO: ListTools");
            return {
                // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool // tool definition
                // https://modelcontextprotocol.io/docs/learn/architecture#understanding-the-tool-execution-request // tool request/response
                // typescript SDK docs:
                //   servers: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md
                //   TODO upgrade to newer version AND check if STDIO delimiter style has changed to include content-length "header" before responses? 
                //     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
                tools: [
                    {
                        // TODO RUN_PROCESS MIGRATION! provide examples in system message, that way it is very clear how to use these!
                        name: "run_process",
                        description: "Run a process on this " + os.platform() + " machine",
                        inputSchema: {
                            type: "object",
                            properties: {
                                // ListToolsResult => Tool type (in protocol) => https://modelcontextprotocol.io/specification/2025-06-18/schema#tool
                                command_line: {
                                    type: "string",
                                    description: "Shell mode: a shell command line executed via the system's default shell. Supports pipes, redirects, globbing. Cannot be combined with 'argv'."
                                },
                                argv: {
                                    minItems: 1, // * made up too
                                    type: "array",
                                    items: { type: "string" },
                                    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'."
                                },
                                cwd: {
                                    // or "workdir" like before? => eval model behavior w/ each name?
                                    type: "string",
                                    description: "Optional to set working directory",
                                },
                                stdin_text: {
                                    type: "string",
                                    description: "Optional text written to STDIN (written fully, then closed). Useful for heredoc-style input or file contents."
                                },
                                timeout_ms: {
                                    type: "number",
                                    description: "Optional timeout in milliseconds, defaults to 30,000ms",
                                }
                            },
                            // 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.
                            //  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
                        },
                    },
                ],
            };
        });
    
        server.setRequestHandler(
            CallToolRequestSchema,
            async (request): Promise<CallToolResult> => {
                verbose_log("INFO: ToolRequest", request);
                switch (request.params.name) {
                    case "run_process": {
                        if (!request.params.arguments) {
                            throw new Error("Missing arguments for run_process");
                        }
                        const result = await runProcess(request.params.arguments);
                        // FYI logging this response is INVALUABLE! found a problem with my neovim MCP STDIO client!
                        verbose_log("INFO: ToolResponse", result);
                        return result;
                    }
                    default:
                        throw new Error("Unknown tool");
                }
            }
        );
    }
  • src/index.ts:1-42 (registration)
    Server entrypoint: creates the MCP Server, calls registerTools(server) to register the run_process tool, and connects via StdioServerTransport.
    #!/usr/bin/env node
    
    import os from "os";
    import { Server } from "@modelcontextprotocol/sdk/server/index.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    
    import { createRequire } from "module";
    import { registerPrompts } from "./prompts.js";
    import { registerTools } from "./tools.js";
    const require = createRequire(import.meta.url);
    const {
        name: package_name,
        version: package_version,
    } = require("../package.json");
    
    const server = new Server(
        {
            name: package_name,
            version: package_version,
            description: "Run commands on this " + os.platform() + " machine",
        },
        {
            capabilities: {
                //resources: {},
                tools: {},
                prompts: {},
                //logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
            },
        }
    );
    registerTools(server);
    registerPrompts(server);
    
    async function main() {
        const transport = new StdioServerTransport();
        await server.connect(transport);
    }
    
    main().catch((error) => {
        console.error("Server error:", error);
        process.exit(1);
    });
  • Helper functions resultFor() and messagesFor() that convert SpawnResult into CallToolResult format with properly structured TextContent messages (EXIT_CODE, STDOUT, STDERR, SIGNAL, etc.), and errorResult() for validation errors.
    import { SpawnFailure, SpawnResult } from "./run_process.js";
    import { CallToolResult, TextContent } from "@modelcontextprotocol/sdk/types.js";
    
    export function resultFor(spawn_result: SpawnResult): CallToolResult {
        const result_obj: CallToolResult = {
            content: messagesFor(spawn_result),
        }
        if (spawn_result.code !== 0) {
            result_obj.isError = true;
        }
        return result_obj
    }
    
    export function messagesFor(result: SpawnFailure | SpawnResult): TextContent[] {
        const messages: TextContent[] = [];
    
        if (result.code !== undefined) {
            // FYI include EXIT_CODE always, to make EXPLICIT when it is NOT a FAILURE!
            //   will double underscore when a comman fails vs not stating if it was a failure
            //   some commands give no other indication of success!
            messages.push({
                name: "EXIT_CODE",
                type: "text",
                text: String(result.code),
            });
        }
    
        // // PRN map COMMAND for failures so model can adjust what it passes vs what actually ran?
        // if ("cmd" in result && result.cmd) {
        //     messages.push({
        //         name: "COMMAND",
        //         type: "text",
        //         text: result.cmd,
        //     });
        // }
    
        if ("message" in result && result.message) {
            // at least need error.message from spawn errors
            messages.push({
                name: "MESSAGE",
                type: "text",
                text: result.message,
            });
        }
    
        if (result.signal) {
            messages.push({
                name: "SIGNAL",
                type: "text",
                text: result.signal,
            });
        }
    
        // // when is this set? what conditions? I tried `kill -9` and didn't trigger "error" event
        // if ("killed" in result && result.killed) {
        //     // killed == true is the only time to include this
        //     messages.push({
        //         name: "KILLED",
        //         type: "text",
        //         text: "Process was killed",
        //     });
        // }
    
        if (result.stdout) {
            messages.push({
                name: "STDOUT",
                type: "text",
                text: result.stdout,
            });
        }
        if (result.stderr) {
            messages.push({
                name: "STDERR",
                type: "text",
                text: result.stderr,
            });
        }
        return messages;
    }
    
    export function errorResult(message: string): CallToolResult {
        return {
            isError: true,
            content: [{
                name: "ERROR",
                type: "text",
                text: message,
            }],
        };
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are present, so the description must carry the burden. It only states 'Run a process' without disclosing that it executes shell commands (which could be dangerous), blocks until completion, or handles errors. The lack of behavioral context is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, short sentence with no fluff. It is front-loaded and efficient. Could be slightly more informative without harm, but current length is acceptable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema or annotations exist. The description fails to mention return values, error behavior, or success/failure indicators. For a command execution tool, this is notably incomplete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the input schema already explains each parameter. The description adds no extra meaning beyond what the schema provides. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's action and resource ('Run a process on this linux machine'). The verb 'run' and noun 'process' are specific. No siblings exist, so differentiation isn't needed. Slight lack of detail about the two execution modes (shell vs. argv) prevents a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit when or when-not-to-use guidance is provided. However, the tool is standalone with no siblings, and the description implies it's for executing arbitrary processes. This is minimally adequate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/g0t4/mcp-server-commands'

If you have feedback or need assistance with the MCP directory API, please join our Discord server