par5-mcp 0.0.0

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.
@@ -0,0 +1,41 @@
1
+ name: Release and Publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ runs-on: ubuntu-latest
15
+ outputs:
16
+ release_created: ${{ steps.release.outputs.release_created }}
17
+ steps:
18
+ - uses: googleapis/release-please-action@v4
19
+ id: release
20
+ with:
21
+ token: ${{ secrets.RELEASE_PAT }}
22
+
23
+ publish:
24
+ needs: release-please
25
+ if: ${{ needs.release-please.outputs.release_created }}
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - uses: actions/setup-node@v4
31
+ with:
32
+ node-version: '20'
33
+ registry-url: 'https://registry.npmjs.org'
34
+
35
+ - run: npm ci
36
+
37
+ - run: npm run build
38
+
39
+ - run: npm publish
40
+ env:
41
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.0.0"
3
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 jrandolf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # par5-mcp
2
+
3
+ An MCP (Model Context Protocol) server that enables parallel execution of shell commands and AI coding agents across lists of items. Perfect for batch processing files, running linters across multiple targets, or delegating complex tasks to multiple AI agents simultaneously.
4
+
5
+ ## Features
6
+
7
+ - **List Management**: Create, update, delete, and inspect lists of items (file paths, URLs, identifiers, etc.)
8
+ - **Parallel Shell Execution**: Run shell commands across all items in a list with batched parallelism
9
+ - **Multi-Agent Orchestration**: Spawn Claude, Gemini, or Codex agents in parallel to process items
10
+ - **Streaming Output**: Results stream to files in real-time for monitoring progress
11
+ - **Batched Processing**: Commands and agents run in batches of 10 to avoid overwhelming the system
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install par5-mcp
17
+ ```
18
+
19
+ Or install globally:
20
+
21
+ ```bash
22
+ npm install -g par5-mcp
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### As an MCP Server
28
+
29
+ Add to your MCP client configuration:
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "par5": {
35
+ "command": "npx",
36
+ "args": ["par5-mcp"]
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ Or if installed globally:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "par5": {
48
+ "command": "par5-mcp"
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Available Tools
55
+
56
+ ### List Management
57
+
58
+ #### `create_list`
59
+
60
+ Creates a named list of items for parallel processing.
61
+
62
+ **Parameters:**
63
+ - `items` (string[]): Array of items to store in the list
64
+
65
+ **Returns:** A unique list ID to use with other tools
66
+
67
+ **Example:**
68
+ ```
69
+ create_list(items: ["src/a.ts", "src/b.ts", "src/c.ts"])
70
+ // Returns: list_id = "abc-123-..."
71
+ ```
72
+
73
+ #### `get_list`
74
+
75
+ Retrieves the items in an existing list by its ID.
76
+
77
+ **Parameters:**
78
+ - `list_id` (string): The list ID returned by `create_list`
79
+
80
+ #### `update_list`
81
+
82
+ Updates an existing list by replacing its items with a new array.
83
+
84
+ **Parameters:**
85
+ - `list_id` (string): The list ID to update
86
+ - `items` (string[]): The new array of items
87
+
88
+ #### `delete_list`
89
+
90
+ Deletes an existing list by its ID.
91
+
92
+ **Parameters:**
93
+ - `list_id` (string): The list ID to delete
94
+
95
+ #### `list_all_lists`
96
+
97
+ Lists all existing lists and their item counts.
98
+
99
+ **Parameters:** None
100
+
101
+ ---
102
+
103
+ ### Parallel Execution
104
+
105
+ #### `run_shell_across_list`
106
+
107
+ Executes a shell command for each item in a list. Commands run in batches of 10 parallel processes.
108
+
109
+ **Parameters:**
110
+ - `list_id` (string): The list ID to iterate over
111
+ - `command` (string): Shell command with `$item` placeholder
112
+
113
+ **Variable Substitution:**
114
+ - Use `$item` in your command - it will be replaced with each list item (properly shell-escaped)
115
+
116
+ **Example:**
117
+ ```
118
+ run_shell_across_list(
119
+ list_id: "abc-123",
120
+ command: "wc -l $item"
121
+ )
122
+ ```
123
+
124
+ This runs `wc -l 'src/a.ts'`, `wc -l 'src/b.ts'`, etc. in parallel.
125
+
126
+ **Output:**
127
+ - stdout and stderr are streamed to separate files per item
128
+ - File paths are returned for you to read the results
129
+
130
+ #### `run_agent_across_list`
131
+
132
+ Spawns an AI coding agent for each item in a list. Agents run in batches of 10 with a 5-minute timeout per agent.
133
+
134
+ **Parameters:**
135
+ - `list_id` (string): The list ID to iterate over
136
+ - `agent` (enum): `"claude"`, `"gemini"`, or `"codex"`
137
+ - `prompt` (string): Prompt with `{{item}}` placeholder
138
+
139
+ **Available Agents:**
140
+ | Agent | CLI | Auto-Accept Flag |
141
+ |-------|-----|------------------|
142
+ | `claude` | Claude Code CLI | `--dangerously-skip-permissions` |
143
+ | `gemini` | Google Gemini CLI | `--yolo` |
144
+ | `codex` | OpenAI Codex CLI | `--dangerously-bypass-approvals-and-sandbox` |
145
+
146
+ **Variable Substitution:**
147
+ - Use `{{item}}` in your prompt - it will be replaced with each list item
148
+
149
+ **Example:**
150
+ ```
151
+ run_agent_across_list(
152
+ list_id: "abc-123",
153
+ agent: "claude",
154
+ prompt: "Review {{item}} for security vulnerabilities and suggest fixes"
155
+ )
156
+ ```
157
+
158
+ **Output:**
159
+ - stdout and stderr are streamed to separate files per item
160
+ - File paths are returned for you to read the agent outputs
161
+
162
+ ## Workflow Example
163
+
164
+ Here's a typical workflow for processing multiple files:
165
+
166
+ 1. **Create a list of files to process:**
167
+ ```
168
+ create_list(items: ["src/auth.ts", "src/api.ts", "src/utils.ts"])
169
+ ```
170
+
171
+ 2. **Run a shell command across all files:**
172
+ ```
173
+ run_shell_across_list(
174
+ list_id: "<returned-id>",
175
+ command: "cat $item | grep -n 'TODO'"
176
+ )
177
+ ```
178
+
179
+ 3. **Or delegate to AI agents:**
180
+ ```
181
+ run_agent_across_list(
182
+ list_id: "<returned-id>",
183
+ agent: "claude",
184
+ prompt: "Add comprehensive JSDoc comments to all exported functions in {{item}}"
185
+ )
186
+ ```
187
+
188
+ 4. **Read the output files** to check results
189
+
190
+ 5. **Clean up:**
191
+ ```
192
+ delete_list(list_id: "<returned-id>")
193
+ ```
194
+
195
+ ## Configuration
196
+
197
+ The following environment variables can be used to configure par5-mcp:
198
+
199
+ | Variable | Description | Default |
200
+ |----------|-------------|---------|
201
+ | `PAR5_BATCH_SIZE` | Number of parallel processes per batch | `10` |
202
+ | `PAR5_AGENT_ARGS` | Additional arguments passed to all agents | (none) |
203
+ | `PAR5_CLAUDE_ARGS` | Additional arguments passed to Claude CLI | (none) |
204
+ | `PAR5_GEMINI_ARGS` | Additional arguments passed to Gemini CLI | (none) |
205
+ | `PAR5_CODEX_ARGS` | Additional arguments passed to Codex CLI | (none) |
206
+ | `PAR5_DISABLE_CLAUDE` | Set to any value to disable the Claude agent | (none) |
207
+ | `PAR5_DISABLE_GEMINI` | Set to any value to disable the Gemini agent | (none) |
208
+ | `PAR5_DISABLE_CODEX` | Set to any value to disable the Codex agent | (none) |
209
+
210
+ **Example:**
211
+
212
+ ```json
213
+ {
214
+ "mcpServers": {
215
+ "par5": {
216
+ "command": "npx",
217
+ "args": ["par5-mcp"],
218
+ "env": {
219
+ "PAR5_BATCH_SIZE": "20",
220
+ "PAR5_CLAUDE_ARGS": "--model claude-sonnet-4-20250514"
221
+ }
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Output Files
228
+
229
+ Results are written to temporary files in the system temp directory under `par5-mcp-results/`:
230
+
231
+ ```
232
+ /tmp/par5-mcp-results/<run-id>/
233
+ ├── auth.ts.stdout.txt
234
+ ├── auth.ts.stderr.txt
235
+ ├── api.ts.stdout.txt
236
+ ├── api.ts.stderr.txt
237
+ └── ...
238
+ ```
239
+
240
+ File names are derived from the item value (sanitized for filesystem safety).
241
+
242
+ ## Development
243
+
244
+ ### Building from Source
245
+
246
+ ```bash
247
+ git clone <repository-url>
248
+ cd par5-mcp
249
+ npm install
250
+ npm run build
251
+ ```
252
+
253
+ ### Running Locally
254
+
255
+ ```bash
256
+ npm start
257
+ ```
258
+
259
+ ## Requirements
260
+
261
+ - Node.js 18+
262
+ - For `run_agent_across_list`:
263
+ - `claude` agent requires [Claude Code CLI](https://claude.ai/code) installed
264
+ - `gemini` agent requires [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed
265
+ - `codex` agent requires [Codex CLI](https://github.com/openai/codex) installed
266
+
267
+ ## License
268
+
269
+ ISC
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { mkdir, open } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, join } from "node:path";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+ // Helper to create a safe filename from an item string
11
+ function toSafeFilename(item) {
12
+ // Get basename if it's a path
13
+ const name = basename(item);
14
+ // Replace unsafe characters with underscores
15
+ return name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
16
+ }
17
+ // Batch size for parallel execution (configurable via PAR5_BATCH_SIZE env var)
18
+ const BATCH_SIZE = parseInt(process.env.PAR5_BATCH_SIZE || "10", 10);
19
+ // Helper to run a command and stream stdout/stderr to separate files
20
+ // Returns a promise that resolves when the command completes
21
+ function runCommandToFiles(command, stdoutFile, stderrFile, options = {}) {
22
+ return new Promise((resolve) => {
23
+ (async () => {
24
+ const stdoutHandle = await open(stdoutFile, "w");
25
+ const stderrHandle = await open(stderrFile, "w");
26
+ const stdoutStream = stdoutHandle.createWriteStream();
27
+ const stderrStream = stderrHandle.createWriteStream();
28
+ const child = spawn("sh", ["-c", command], {
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ });
31
+ let timeoutId;
32
+ if (options.timeout) {
33
+ timeoutId = setTimeout(() => {
34
+ child.kill("SIGTERM");
35
+ }, options.timeout);
36
+ }
37
+ child.stdout.pipe(stdoutStream);
38
+ child.stderr.pipe(stderrStream);
39
+ child.on("close", async () => {
40
+ if (timeoutId)
41
+ clearTimeout(timeoutId);
42
+ stdoutStream.end();
43
+ stderrStream.end();
44
+ await stdoutHandle.close();
45
+ await stderrHandle.close();
46
+ resolve();
47
+ });
48
+ child.on("error", async (err) => {
49
+ if (timeoutId)
50
+ clearTimeout(timeoutId);
51
+ stderrStream.write(`\nERROR: ${err.message}\n`);
52
+ stdoutStream.end();
53
+ stderrStream.end();
54
+ await stdoutHandle.close();
55
+ await stderrHandle.close();
56
+ resolve();
57
+ });
58
+ })();
59
+ });
60
+ }
61
+ // Helper to run commands in batches
62
+ async function runInBatches(tasks) {
63
+ for (let i = 0; i < tasks.length; i += BATCH_SIZE) {
64
+ const batch = tasks.slice(i, i + BATCH_SIZE);
65
+ await Promise.all(batch.map((task) => runCommandToFiles(task.command, task.stdoutFile, task.stderrFile, {
66
+ timeout: task.timeout,
67
+ })));
68
+ }
69
+ }
70
+ // Create output directory for results
71
+ const outputDir = join(tmpdir(), "par5-mcp-results");
72
+ // Store for lists
73
+ const lists = new Map();
74
+ // Create the MCP server
75
+ const server = new McpServer({
76
+ name: "par5-mcp",
77
+ version: "1.0.0",
78
+ });
79
+ // Tool: create_list
80
+ server.registerTool("create_list", {
81
+ description: `Creates a named list of items for parallel processing. Use this tool when you need to perform the same operation across multiple files, URLs, or any collection of items.
82
+
83
+ WHEN TO USE:
84
+ - Before running shell commands or AI agents across multiple items
85
+ - When you have a collection of file paths, URLs, identifiers, or any strings to process in parallel
86
+
87
+ WORKFLOW:
88
+ 1. Call create_list with your array of items
89
+ 2. Use the returned list_id with run_shell_across_list or run_agent_across_list
90
+ 3. The list persists for the duration of the session
91
+
92
+ EXAMPLE: To process files ["src/a.ts", "src/b.ts", "src/c.ts"], first create a list, then use run_shell_across_list or run_agent_across_list with the returned id.`,
93
+ inputSchema: {
94
+ items: z
95
+ .array(z.string())
96
+ .describe("Array of items to store in the list. Each item can be a file path, URL, identifier, or any string that will be substituted into commands or prompts."),
97
+ },
98
+ }, async ({ items }) => {
99
+ const id = randomUUID();
100
+ lists.set(id, items);
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: `Successfully created a list with ${items.length} items. The list ID is "${id}". You can now use this ID with run_shell_across_list or run_agent_across_list to process each item in parallel. The commands will run in the background and stream output to files. After starting the commands, you should sleep briefly and then read the output files to check results.`,
106
+ },
107
+ ],
108
+ };
109
+ });
110
+ // Tool: get_list
111
+ server.registerTool("get_list", {
112
+ description: `Retrieves the items in an existing list by its ID.
113
+
114
+ WHEN TO USE:
115
+ - To inspect the contents of a list before processing
116
+ - To verify which items are in a list
117
+ - To check if a list exists`,
118
+ inputSchema: {
119
+ list_id: z.string().describe("The list ID returned by create_list."),
120
+ },
121
+ }, async ({ list_id }) => {
122
+ const items = lists.get(list_id);
123
+ if (!items) {
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: `Error: No list found with ID "${list_id}". The list may have been deleted or the ID is incorrect.`,
129
+ },
130
+ ],
131
+ isError: true,
132
+ };
133
+ }
134
+ const itemList = items.map((item, i) => `${i + 1}. ${item}`).join("\n");
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `List "${list_id}" contains ${items.length} items:\n\n${itemList}`,
140
+ },
141
+ ],
142
+ };
143
+ });
144
+ // Tool: update_list
145
+ server.registerTool("update_list", {
146
+ description: `Updates an existing list by replacing its items with a new array.
147
+
148
+ WHEN TO USE:
149
+ - To modify the contents of an existing list
150
+ - To add or remove items from a list
151
+ - To reorder items in a list`,
152
+ inputSchema: {
153
+ list_id: z.string().describe("The list ID returned by create_list."),
154
+ items: z
155
+ .array(z.string())
156
+ .describe("The new array of items to replace the existing list contents."),
157
+ },
158
+ }, async ({ list_id, items }) => {
159
+ if (!lists.has(list_id)) {
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `Error: No list found with ID "${list_id}". The list may have been deleted or the ID is incorrect. Use create_list to create a new list.`,
165
+ },
166
+ ],
167
+ isError: true,
168
+ };
169
+ }
170
+ const oldCount = lists.get(list_id)?.length;
171
+ lists.set(list_id, items);
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `Successfully updated list "${list_id}". Changed from ${oldCount} items to ${items.length} items.`,
177
+ },
178
+ ],
179
+ };
180
+ });
181
+ // Tool: delete_list
182
+ server.registerTool("delete_list", {
183
+ description: `Deletes an existing list by its ID.
184
+
185
+ WHEN TO USE:
186
+ - To clean up lists that are no longer needed
187
+ - To free up memory after processing is complete`,
188
+ inputSchema: {
189
+ list_id: z.string().describe("The list ID returned by create_list."),
190
+ },
191
+ }, async ({ list_id }) => {
192
+ if (!lists.has(list_id)) {
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: `Error: No list found with ID "${list_id}". The list may have already been deleted or the ID is incorrect.`,
198
+ },
199
+ ],
200
+ isError: true,
201
+ };
202
+ }
203
+ const itemCount = lists.get(list_id)?.length;
204
+ lists.delete(list_id);
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: `Successfully deleted list "${list_id}" which contained ${itemCount} items.`,
210
+ },
211
+ ],
212
+ };
213
+ });
214
+ // Tool: list_all_lists
215
+ server.registerTool("list_all_lists", {
216
+ description: `Lists all existing lists and their item counts.
217
+
218
+ WHEN TO USE:
219
+ - To see all available lists in the current session
220
+ - To find a list ID you may have forgotten
221
+ - To check how many lists exist`,
222
+ inputSchema: {},
223
+ }, async () => {
224
+ if (lists.size === 0) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: "No lists exist. Use create_list to create a new list.",
230
+ },
231
+ ],
232
+ };
233
+ }
234
+ const listInfo = Array.from(lists.entries())
235
+ .map(([id, items]) => `- "${id}": ${items.length} items`)
236
+ .join("\n");
237
+ return {
238
+ content: [
239
+ {
240
+ type: "text",
241
+ text: `Found ${lists.size} list(s):\n\n${listInfo}`,
242
+ },
243
+ ],
244
+ };
245
+ });
246
+ // Tool: run_shell_across_list
247
+ server.registerTool("run_shell_across_list", {
248
+ 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.
249
+
250
+ WHEN TO USE:
251
+ - Running the same shell command across multiple files (e.g., linting, formatting, compiling)
252
+ - Batch processing with command-line tools
253
+ - Any operation where you need to execute shell commands on a collection of items
254
+
255
+ HOW IT WORKS:
256
+ 1. Each item in the list is substituted into the command where $item appears
257
+ 2. Commands run in batches of ${BATCH_SIZE} at a time to avoid overwhelming the system
258
+ 3. Output streams directly to files as the commands execute
259
+ 4. This tool waits for all commands to complete before returning
260
+
261
+ AFTER COMPLETION:
262
+ - Read the stdout files to check results
263
+ - Check stderr files if you encounter errors or unexpected output
264
+ - Files are named based on the item (e.g., "myfile.ts.stdout.txt")
265
+
266
+ VARIABLE SUBSTITUTION:
267
+ - Use $item in your command - it will be replaced with each list item (properly shell-escaped)
268
+ - Example: "cat $item" becomes "cat 'src/file.ts'" for item "src/file.ts"`,
269
+ inputSchema: {
270
+ list_id: z
271
+ .string()
272
+ .describe("The list ID returned by create_list. This identifies which list of items to iterate over."),
273
+ command: z
274
+ .string()
275
+ .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'"),
276
+ },
277
+ }, async ({ list_id, command }) => {
278
+ const items = lists.get(list_id);
279
+ if (!items) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ 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.`,
285
+ },
286
+ ],
287
+ isError: true,
288
+ };
289
+ }
290
+ // Create output directory
291
+ const runId = randomUUID();
292
+ const runDir = join(outputDir, runId);
293
+ await mkdir(runDir, { recursive: true });
294
+ const results = [];
295
+ const tasks = [];
296
+ for (let i = 0; i < items.length; i++) {
297
+ const item = items[i];
298
+ // Replace $item with the actual item value (properly escaped)
299
+ const escapedItem = item.replace(/'/g, "'\\''");
300
+ const expandedCommand = command.replace(/\$item/g, `'${escapedItem}'`);
301
+ const safeFilename = toSafeFilename(item);
302
+ const stdoutFile = join(runDir, `${safeFilename}.stdout.txt`);
303
+ const stderrFile = join(runDir, `${safeFilename}.stderr.txt`);
304
+ tasks.push({
305
+ command: expandedCommand,
306
+ stdoutFile,
307
+ stderrFile,
308
+ });
309
+ results.push({
310
+ item,
311
+ files: { stdout: stdoutFile, stderr: stderrFile },
312
+ });
313
+ }
314
+ // Run commands in batches of 10
315
+ await runInBatches(tasks);
316
+ // Build prose response
317
+ const fileList = results
318
+ .map((r) => `- ${r.item}: stdout at "${r.files.stdout}", stderr at "${r.files.stderr}"`)
319
+ .join("\n");
320
+ const numBatches = Math.ceil(items.length / BATCH_SIZE);
321
+ return {
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: `Completed ${results.length} shell commands in ${numBatches} batch(es) of up to ${BATCH_SIZE} parallel commands each. Output has been streamed to files.
326
+
327
+ OUTPUT FILES:
328
+ ${fileList}
329
+
330
+ NEXT STEPS:
331
+ 1. Read the stdout files to check the results of each command
332
+ 2. If there are errors, check the corresponding stderr files for details
333
+
334
+ All commands have completed and output files are ready to read.`,
335
+ },
336
+ ],
337
+ };
338
+ });
339
+ // Determine which agents are enabled based on PAR5_DISABLE_* env vars
340
+ const ALL_AGENTS = ["claude", "gemini", "codex"];
341
+ const ENABLED_AGENTS = ALL_AGENTS.filter((agent) => {
342
+ const disableVar = `PAR5_DISABLE_${agent.toUpperCase()}`;
343
+ return !process.env[disableVar];
344
+ });
345
+ // Tool: run_agent_across_list (only registered if at least one agent is enabled)
346
+ if (ENABLED_AGENTS.length > 0) {
347
+ const agentDescriptions = {
348
+ claude: "claude: Claude Code CLI (uses --dangerously-skip-permissions for autonomous operation)",
349
+ gemini: "gemini: Google Gemini CLI (uses --yolo for auto-accept)",
350
+ codex: "codex: OpenAI Codex CLI (uses --dangerously-bypass-approvals-and-sandbox for autonomous operation)",
351
+ };
352
+ const availableAgentsDoc = ENABLED_AGENTS.map((a) => `- ${agentDescriptions[a]}`).join("\n");
353
+ server.registerTool("run_agent_across_list", {
354
+ description: `Spawns an AI coding agent for each item in a previously created list. Agents run in batches of ${BATCH_SIZE} parallel processes with automatic permission skipping enabled.
355
+
356
+ WHEN TO USE:
357
+ - Performing complex code analysis, refactoring, or generation across multiple files
358
+ - Tasks that require AI reasoning rather than simple shell commands
359
+ - When you need to delegate work to multiple AI agents working in parallel
360
+
361
+ AVAILABLE AGENTS:
362
+ ${availableAgentsDoc}
363
+
364
+ HOW IT WORKS:
365
+ 1. Each item in the list is substituted into the prompt where {{item}} appears
366
+ 2. Agents run in batches of ${BATCH_SIZE} at a time to avoid overwhelming the system
367
+ 3. Each agent has a 5-minute timeout
368
+ 4. Output streams directly to files as the agents work
369
+ 5. This tool waits for all agents to complete before returning
370
+
371
+ AFTER COMPLETION:
372
+ - Read the stdout files to check the results from each agent
373
+ - Check stderr files if you encounter errors
374
+ - Files are named based on the item (e.g., "myfile.ts.stdout.txt")
375
+
376
+ VARIABLE SUBSTITUTION:
377
+ - Use {{item}} in your prompt - it will be replaced with each list item
378
+ - Example: "Review {{item}} for bugs" becomes "Review src/file.ts for bugs" for item "src/file.ts"`,
379
+ inputSchema: {
380
+ list_id: z
381
+ .string()
382
+ .describe("The list ID returned by create_list. This identifies which list of items to iterate over."),
383
+ agent: z
384
+ .enum(ENABLED_AGENTS)
385
+ .describe(`Which AI agent to use: ${ENABLED_AGENTS.map((a) => `'${a}'`).join(", ")}. All agents run with permission-skipping flags for autonomous operation.`),
386
+ prompt: z
387
+ .string()
388
+ .describe("The prompt to send to each agent. Use {{item}} as a placeholder - it will be replaced with the current item value. Example: 'Review {{item}} and suggest improvements' or 'Add error handling to {{item}}'"),
389
+ },
390
+ }, async ({ list_id, agent, prompt }) => {
391
+ const items = lists.get(list_id);
392
+ if (!items) {
393
+ return {
394
+ content: [
395
+ {
396
+ type: "text",
397
+ 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.`,
398
+ },
399
+ ],
400
+ isError: true,
401
+ };
402
+ }
403
+ // Create output directory
404
+ const runId = randomUUID();
405
+ const runDir = join(outputDir, runId);
406
+ await mkdir(runDir, { recursive: true });
407
+ const results = [];
408
+ const tasks = [];
409
+ // Build the agent command with skip permission flags and streaming output
410
+ // Additional args can be passed via PAR5_AGENT_ARGS (all agents) or PAR5_CLAUDE_ARGS, PAR5_GEMINI_ARGS, PAR5_CODEX_ARGS (per-agent)
411
+ const getAgentCommand = (agentName, expandedPrompt) => {
412
+ const escapedPrompt = expandedPrompt.replace(/'/g, "'\\''");
413
+ const agentArgs = process.env.PAR5_AGENT_ARGS || "";
414
+ switch (agentName) {
415
+ case "claude": {
416
+ // Claude Code CLI with --dangerously-skip-permissions and streaming output
417
+ const claudeArgs = process.env.PAR5_CLAUDE_ARGS || "";
418
+ return `claude --dangerously-skip-permissions --output-format stream-json --verbose ${agentArgs} ${claudeArgs} -p '${escapedPrompt}'`;
419
+ }
420
+ case "gemini": {
421
+ // Gemini CLI with yolo mode and streaming JSON output
422
+ const geminiArgs = process.env.PAR5_GEMINI_ARGS || "";
423
+ return `gemini --yolo --output-format stream-json ${agentArgs} ${geminiArgs} '${escapedPrompt}'`;
424
+ }
425
+ case "codex": {
426
+ // Codex CLI exec subcommand with full-auto flag and JSON streaming output
427
+ const codexArgs = process.env.PAR5_CODEX_ARGS || "";
428
+ return `codex exec --dangerously-bypass-approvals-and-sandbox ${agentArgs} ${codexArgs} '${escapedPrompt}'`;
429
+ }
430
+ default:
431
+ throw new Error(`Unknown agent: ${agentName}`);
432
+ }
433
+ };
434
+ for (let i = 0; i < items.length; i++) {
435
+ const item = items[i];
436
+ // Replace {{item}} with the actual item value
437
+ const expandedPrompt = prompt.replace(/\{\{item\}\}/g, item);
438
+ const safeFilename = toSafeFilename(item);
439
+ const stdoutFile = join(runDir, `${safeFilename}.stdout.txt`);
440
+ const stderrFile = join(runDir, `${safeFilename}.stderr.txt`);
441
+ tasks.push({
442
+ command: getAgentCommand(agent, expandedPrompt),
443
+ stdoutFile,
444
+ stderrFile,
445
+ timeout: 300000, // 5 minute timeout per item
446
+ });
447
+ results.push({
448
+ item,
449
+ files: { stdout: stdoutFile, stderr: stderrFile },
450
+ });
451
+ }
452
+ // Run agents in batches of 10
453
+ await runInBatches(tasks);
454
+ // Build prose response
455
+ const fileList = results
456
+ .map((r) => `- ${r.item}: stdout at "${r.files.stdout}", stderr at "${r.files.stderr}"`)
457
+ .join("\n");
458
+ const agentNames = {
459
+ claude: "Claude Code",
460
+ gemini: "Google Gemini",
461
+ codex: "OpenAI Codex",
462
+ };
463
+ const numBatches = Math.ceil(items.length / BATCH_SIZE);
464
+ return {
465
+ content: [
466
+ {
467
+ type: "text",
468
+ text: `Completed ${results.length} ${agentNames[agent]} agents in ${numBatches} batch(es) of up to ${BATCH_SIZE} parallel agents each. Output has been streamed to files.
469
+
470
+ OUTPUT FILES:
471
+ ${fileList}
472
+
473
+ NEXT STEPS:
474
+ 1. Read the stdout files to check the results from each agent
475
+ 2. If there are errors, check the corresponding stderr files for details
476
+
477
+ All agents have completed (with a 5-minute timeout per agent) and output files are ready to read.`,
478
+ },
479
+ ],
480
+ };
481
+ });
482
+ }
483
+ // Start the server
484
+ async function main() {
485
+ const transport = new StdioServerTransport();
486
+ await server.connect(transport);
487
+ }
488
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "par5-mcp",
3
+ "version": "0.0.0",
4
+ "description": "MCP server for parallel list operations - run shell commands and AI agents across lists in parallel",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "par5-mcp": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "parallel",
18
+ "batch",
19
+ "shell",
20
+ "ai-agents",
21
+ "claude",
22
+ "gemini",
23
+ "codex",
24
+ "automation"
25
+ ],
26
+ "author": "jrandolf",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/jrandolf/par5-mcp.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/jrandolf/par5-mcp/issues"
34
+ },
35
+ "homepage": "https://github.com/jrandolf/par5-mcp#readme",
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "type": "module",
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.25.1",
42
+ "zod": "^4.2.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.0.3",
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "node",
6
+ "bump-minor-pre-major": true,
7
+ "bump-patch-for-minor-pre-major": true,
8
+ "initial-version": "0.1.0",
9
+ "include-component-in-tag": false
10
+ }
11
+ }
12
+ }