Skip to main content
Glama

run_shell_across_list

Execute shell commands across multiple items in batches, substituting $item in each command and streaming output to separate files for parallel batch processing.

Instructions

Executes a shell command for each item in a previously created list. Commands run in batches of 10 parallel processes, with stdout and stderr streamed to separate files.

WHEN TO USE:

  • Running the same shell command across multiple files (e.g., linting, formatting, compiling)

  • Batch processing with command-line tools

  • Any operation where you need to execute shell commands on a collection of items

HOW IT WORKS:

  1. Each item in the list is substituted into the command where $item appears

  2. Commands run in batches of 10 at a time to avoid overwhelming the system

  3. Output streams directly to files as the commands execute

  4. This tool waits for all commands to complete before returning

AFTER COMPLETION:

  • Read the stdout files to check results

  • Check stderr files if you encounter errors or unexpected output

  • Files are named based on the item (e.g., "myfile.ts.stdout.txt")

VARIABLE SUBSTITUTION:

  • Use $item in your command - it will be replaced with each list item (properly shell-escaped)

  • Example: "cat $item" becomes "cat 'src/file.ts'" for item "src/file.ts"

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
list_idYesThe list ID returned by create_list. This identifies which list of items to iterate over.
commandYesShell command to execute for each item. Use $item as a placeholder - it will be replaced with the current item value (properly escaped). Example: 'wc -l $item' or 'cat $item | grep TODO'

Implementation Reference

  • The handler function for 'run_shell_across_list' that retrieves the list, substitutes $item in the command for each item, sets up output files, runs the commands in batches using runInBatches, and returns a summary with file locations.
    async ({ list_id, command }) => { const items = lists.get(list_id); if (!items) { return { content: [ { type: "text", text: `Error: No list found with ID "${list_id}". Please call create_list first to create a list of items, then use the returned ID with this tool.`, }, ], isError: true, }; } // Create output directory const runId = randomUUID(); const runDir = join(outputDir, runId); await mkdir(runDir, { recursive: true }); const results: Array<{ item: string; files: OutputFiles }> = []; const tasks: Array<{ command: string; stdoutFile: string; stderrFile: string; }> = []; for (let i = 0; i < items.length; i++) { const item = items[i]; // Replace $item with the actual item value (properly escaped) const escapedItem = item.replace(/'/g, "'\\''"); const expandedCommand = command.replace(/\$item/g, `'${escapedItem}'`); const safeFilename = toSafeFilename(item); const stdoutFile = join(runDir, `${safeFilename}.stdout.txt`); const stderrFile = join(runDir, `${safeFilename}.stderr.txt`); tasks.push({ command: expandedCommand, stdoutFile, stderrFile, }); results.push({ item, files: { stdout: stdoutFile, stderr: stderrFile }, }); } // Run commands in batches of 10 await runInBatches(tasks); // Build prose response const fileList = results .map( (r) => `- ${r.item}: stdout at "${r.files.stdout}", stderr at "${r.files.stderr}"`, ) .join("\n"); const numBatches = Math.ceil(items.length / BATCH_SIZE); return { content: [ { type: "text", text: `Completed ${results.length} shell commands in ${numBatches} batch(es) of up to ${BATCH_SIZE} parallel commands each. Output has been streamed to files. OUTPUT FILES: ${fileList} NEXT STEPS: 1. Read the stdout files to check the results of each command 2. If there are errors, check the corresponding stderr files for details All commands have completed and output files are ready to read.`, }, ], }; },
  • Zod input schema defining the required parameters: list_id (string) and command (string).
    inputSchema: { list_id: z .string() .describe( "The list ID returned by create_list. This identifies which list of items to iterate over.", ), command: z .string() .describe( "Shell command to execute for each item. Use $item as a placeholder - it will be replaced with the current item value (properly escaped). Example: 'wc -l $item' or 'cat $item | grep TODO'", ), },
  • src/index.ts:463-580 (registration)
    Registration of the 'run_shell_across_list' tool using server.registerTool, including description, inputSchema, and handler function.
    // Tool: run_shell_across_list server.registerTool( "run_shell_across_list", { description: `Executes a shell command for each item in a previously created list. Commands run in batches of ${BATCH_SIZE} parallel processes, with stdout and stderr streamed to separate files. WHEN TO USE: - Running the same shell command across multiple files (e.g., linting, formatting, compiling) - Batch processing with command-line tools - Any operation where you need to execute shell commands on a collection of items HOW IT WORKS: 1. Each item in the list is substituted into the command where $item appears 2. Commands run in batches of ${BATCH_SIZE} at a time to avoid overwhelming the system 3. Output streams directly to files as the commands execute 4. This tool waits for all commands to complete before returning AFTER COMPLETION: - Read the stdout files to check results - Check stderr files if you encounter errors or unexpected output - Files are named based on the item (e.g., "myfile.ts.stdout.txt") VARIABLE SUBSTITUTION: - Use $item in your command - it will be replaced with each list item (properly shell-escaped) - Example: "cat $item" becomes "cat 'src/file.ts'" for item "src/file.ts"`, inputSchema: { list_id: z .string() .describe( "The list ID returned by create_list. This identifies which list of items to iterate over.", ), command: z .string() .describe( "Shell command to execute for each item. Use $item as a placeholder - it will be replaced with the current item value (properly escaped). Example: 'wc -l $item' or 'cat $item | grep TODO'", ), }, }, async ({ list_id, command }) => { const items = lists.get(list_id); if (!items) { return { content: [ { type: "text", text: `Error: No list found with ID "${list_id}". Please call create_list first to create a list of items, then use the returned ID with this tool.`, }, ], isError: true, }; } // Create output directory const runId = randomUUID(); const runDir = join(outputDir, runId); await mkdir(runDir, { recursive: true }); const results: Array<{ item: string; files: OutputFiles }> = []; const tasks: Array<{ command: string; stdoutFile: string; stderrFile: string; }> = []; for (let i = 0; i < items.length; i++) { const item = items[i]; // Replace $item with the actual item value (properly escaped) const escapedItem = item.replace(/'/g, "'\\''"); const expandedCommand = command.replace(/\$item/g, `'${escapedItem}'`); const safeFilename = toSafeFilename(item); const stdoutFile = join(runDir, `${safeFilename}.stdout.txt`); const stderrFile = join(runDir, `${safeFilename}.stderr.txt`); tasks.push({ command: expandedCommand, stdoutFile, stderrFile, }); results.push({ item, files: { stdout: stdoutFile, stderr: stderrFile }, }); } // Run commands in batches of 10 await runInBatches(tasks); // Build prose response const fileList = results .map( (r) => `- ${r.item}: stdout at "${r.files.stdout}", stderr at "${r.files.stderr}"`, ) .join("\n"); const numBatches = Math.ceil(items.length / BATCH_SIZE); return { content: [ { type: "text", text: `Completed ${results.length} shell commands in ${numBatches} batch(es) of up to ${BATCH_SIZE} parallel commands each. Output has been streamed to files. OUTPUT FILES: ${fileList} NEXT STEPS: 1. Read the stdout files to check the results of each command 2. If there are errors, check the corresponding stderr files for details All commands have completed and output files are ready to read.`, }, ], }; }, );
  • Helper function runInBatches that executes the command tasks in batches of BATCH_SIZE using Promise.all for parallelism, calling runCommandToFiles for each.
    async function runInBatches( tasks: Array<{ command: string; stdoutFile: string; stderrFile: string; }>, ): Promise<void> { for (let i = 0; i < tasks.length; i += BATCH_SIZE) { const batch = tasks.slice(i, i + BATCH_SIZE); await Promise.all( batch.map((task) => runCommandToFiles(task.command, task.stdoutFile, task.stderrFile), ), ); } }
  • Helper function that spawns a shell command and streams stdout and stderr to separate files, tracking the process for cleanup.
    function runCommandToFiles( command: string, stdoutFile: string, stderrFile: string, ): Promise<void> { return new Promise((resolve) => { (async () => { const stdoutHandle = await open(stdoutFile, "w"); const stderrHandle = await open(stderrFile, "w"); const stdoutStream = stdoutHandle.createWriteStream(); const stderrStream = stderrHandle.createWriteStream(); const child = spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"], }); // Track the child process for cleanup on shutdown activeProcesses.add(child); child.stdout.pipe(stdoutStream); child.stderr.pipe(stderrStream); child.on("close", async () => { activeProcesses.delete(child); stdoutStream.end(); stderrStream.end(); await stdoutHandle.close(); await stderrHandle.close(); resolve(); }); child.on("error", async (err) => { activeProcesses.delete(child); stderrStream.write(`\nERROR: ${err.message}\n`); stdoutStream.end(); stderrStream.end(); await stdoutHandle.close(); await stderrHandle.close(); resolve(); }); })(); }); }

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/jrandolf/par5-mcp'

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