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.
- package/.github/workflows/publish.yml +41 -0
- package/.release-please-manifest.json +3 -0
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +488 -0
- package/package.json +48 -0
- package/release-please-config.json +12 -0
|
@@ -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 }}
|
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|