dispatch-agents 0.4.1 → 0.5.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/README.md CHANGED
@@ -49,6 +49,41 @@ npm link
49
49
  - `git` — for worktree management
50
50
  - iTerm2 (recommended) — for native tab integration via `tmux -CC`
51
51
 
52
+ ## MCP Server
53
+
54
+ Dispatch includes an MCP server so Claude Code can orchestrate agents directly — no shell commands needed.
55
+
56
+ ### Setup
57
+
58
+ After installing dispatch, register the MCP server with Claude Code:
59
+
60
+ ```bash
61
+ claude mcp add --scope user dispatch node $(which dispatch-mcp)
62
+ ```
63
+
64
+ This exposes 6 tools to Claude Code:
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `dispatch_run` | Launch an agent with a prompt (inline text, not a file) |
69
+ | `dispatch_list` | List all running agents with status |
70
+ | `dispatch_stop` | Stop a running agent |
71
+ | `dispatch_resume` | Resume a stopped agent |
72
+ | `dispatch_cleanup` | Remove worktrees and optionally branches |
73
+ | `dispatch_logs` | Get recent output from an agent |
74
+
75
+ ### How it works
76
+
77
+ The MCP server wraps the dispatch CLI over stdio using the [Model Context Protocol](https://modelcontextprotocol.io). When Claude Code calls `dispatch_run`, the server writes the prompt to a temp file, runs `dispatch run --prompt-file <file>`, and cleans up. Interactive agents open iTerm tabs; headless agents run in the background.
78
+
79
+ ### Working directory
80
+
81
+ By default the MCP server uses the directory Claude Code is running in. To override, set `DISPATCH_CWD`:
82
+
83
+ ```bash
84
+ claude mcp add --scope user dispatch -e DISPATCH_CWD=/path/to/repo node $(which dispatch-mcp)
85
+ ```
86
+
52
87
  ## Usage
53
88
 
54
89
  ### Launch an agent
package/dist/cli.js CHANGED
@@ -539,6 +539,7 @@ async function cmdRun(args, config) {
539
539
  let extraArgs = "";
540
540
  let skipWorktree = false;
541
541
  let nameOverride = "";
542
+ let noAttach = false;
542
543
  let i = 0;
543
544
  while (i < args.length) {
544
545
  const arg = args[i];
@@ -575,6 +576,10 @@ async function cmdRun(args, config) {
575
576
  skipWorktree = true;
576
577
  i++;
577
578
  break;
579
+ case "--no-attach":
580
+ noAttach = true;
581
+ i++;
582
+ break;
578
583
  case "--name":
579
584
  case "-n":
580
585
  nameOverride = args[++i];
@@ -614,7 +619,7 @@ async function cmdRun(args, config) {
614
619
  await launchAgent(input, headless, extraArgs, skipWorktree, promptFile, nameOverride, config);
615
620
  }
616
621
  console.log();
617
- if (!headless && inputs.length === 1) {
622
+ if (!headless && inputs.length === 1 && !noAttach) {
618
623
  log.info("Attaching to tmux session...");
619
624
  log.dim(" Detach with: Ctrl-B then D");
620
625
  console.log();
@@ -703,10 +708,11 @@ function cmdStop(args) {
703
708
  function cmdResume(args, config) {
704
709
  const id = args[0];
705
710
  if (!id) {
706
- log.error("Usage: dispatch resume <agent-id> [--headless]");
711
+ log.error("Usage: dispatch resume <agent-id> [--headless] [--no-attach]");
707
712
  process.exit(1);
708
713
  }
709
714
  const headless = args.includes("--headless") || args.includes("-H");
715
+ const noAttach = args.includes("--no-attach");
710
716
  ensureTmux();
711
717
  const wtPath = worktreePath(id, config);
712
718
  if (!existsSync2(wtPath)) {
@@ -725,7 +731,7 @@ function cmdResume(args, config) {
725
731
  `tmux send-keys -t "${tmuxTarget(id)}" "unset CLAUDECODE && claude --continue ${modelFlag}" Enter`
726
732
  );
727
733
  log.ok(`Resumed agent: ${id} (interactive)`);
728
- tmuxAttach();
734
+ if (!noAttach) tmuxAttach();
729
735
  } else {
730
736
  const resumePrompt = "Continue working on the task.";
731
737
  const claudeCmd = buildClaudeCmd(
package/dist/mcp.js ADDED
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { execSync } from "child_process";
11
+ import {
12
+ writeFileSync,
13
+ unlinkSync,
14
+ readFileSync,
15
+ existsSync
16
+ } from "fs";
17
+ import { tmpdir } from "os";
18
+ import { join } from "path";
19
+ import { randomBytes } from "crypto";
20
+ function stripAnsi(str) {
21
+ return str.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
22
+ }
23
+ function dispatch(args) {
24
+ const cwd = process.env.DISPATCH_CWD || process.cwd();
25
+ try {
26
+ const output = execSync(`dispatch ${args}`, {
27
+ encoding: "utf-8",
28
+ cwd,
29
+ timeout: 12e4,
30
+ stdio: ["pipe", "pipe", "pipe"]
31
+ });
32
+ return stripAnsi(output).trim();
33
+ } catch (e) {
34
+ const out = [e.stdout, e.stderr, e.message].filter(Boolean).join("\n");
35
+ return stripAnsi(out).trim();
36
+ }
37
+ }
38
+ function makeTempPrompt(prompt) {
39
+ const name = `dispatch-mcp-${randomBytes(4).toString("hex")}.md`;
40
+ const path = join(tmpdir(), name);
41
+ writeFileSync(path, prompt);
42
+ return path;
43
+ }
44
+ var server = new Server(
45
+ { name: "dispatch", version: "0.1.0" },
46
+ { capabilities: { tools: {} } }
47
+ );
48
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
49
+ tools: [
50
+ {
51
+ name: "dispatch_run",
52
+ description: "Launch a Claude Code agent in an isolated git worktree. Pass the full task prompt inline. Returns agent ID and branch name.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ prompt: {
57
+ type: "string",
58
+ description: "Full task description/prompt for the agent"
59
+ },
60
+ ticket: {
61
+ type: "string",
62
+ description: "Linear ticket ID (e.g. HEY-907). Fetches title + description if LINEAR_API_KEY is set."
63
+ },
64
+ name: {
65
+ type: "string",
66
+ description: "Agent name and branch name (kebab-case). Defaults to ticket ID or derived from prompt."
67
+ },
68
+ headless: {
69
+ type: "boolean",
70
+ description: "Run in background (fire-and-forget). Default: false (interactive)."
71
+ },
72
+ model: {
73
+ type: "string",
74
+ description: "Claude model: sonnet, opus, haiku"
75
+ },
76
+ base_branch: {
77
+ type: "string",
78
+ description: "Branch to create worktree from. Default: dev."
79
+ },
80
+ max_turns: {
81
+ type: "number",
82
+ description: "Max agentic turns (headless only)"
83
+ }
84
+ },
85
+ required: ["prompt"]
86
+ }
87
+ },
88
+ {
89
+ name: "dispatch_list",
90
+ description: "List all running dispatch agents with their status (running/idle/exited).",
91
+ inputSchema: { type: "object", properties: {} }
92
+ },
93
+ {
94
+ name: "dispatch_stop",
95
+ description: "Stop a running dispatch agent. Worktree and branch are preserved.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ agent_id: { type: "string", description: "Agent ID to stop" }
100
+ },
101
+ required: ["agent_id"]
102
+ }
103
+ },
104
+ {
105
+ name: "dispatch_resume",
106
+ description: "Resume a previously stopped agent. Picks up where it left off.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ agent_id: {
111
+ type: "string",
112
+ description: "Agent ID to resume"
113
+ },
114
+ headless: {
115
+ type: "boolean",
116
+ description: "Resume in headless mode"
117
+ }
118
+ },
119
+ required: ["agent_id"]
120
+ }
121
+ },
122
+ {
123
+ name: "dispatch_cleanup",
124
+ description: "Remove an agent's worktree and optionally its branch.",
125
+ inputSchema: {
126
+ type: "object",
127
+ properties: {
128
+ agent_id: {
129
+ type: "string",
130
+ description: "Agent ID to clean up. Omit and set all=true for all agents."
131
+ },
132
+ all: {
133
+ type: "boolean",
134
+ description: "Clean up all worktrees"
135
+ },
136
+ delete_branch: {
137
+ type: "boolean",
138
+ description: "Also delete the git branch"
139
+ }
140
+ }
141
+ }
142
+ },
143
+ {
144
+ name: "dispatch_logs",
145
+ description: "Get recent output from a dispatch agent (log file or tmux capture).",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ agent_id: { type: "string", description: "Agent ID" },
150
+ lines: {
151
+ type: "number",
152
+ description: "Number of lines to return. Default: 50."
153
+ }
154
+ },
155
+ required: ["agent_id"]
156
+ }
157
+ }
158
+ ]
159
+ }));
160
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
161
+ const { name, arguments: args = {} } = request.params;
162
+ switch (name) {
163
+ case "dispatch_run": {
164
+ const {
165
+ prompt,
166
+ ticket,
167
+ name: agentName,
168
+ headless,
169
+ model,
170
+ base_branch,
171
+ max_turns
172
+ } = args;
173
+ const tmpFile = makeTempPrompt(prompt);
174
+ try {
175
+ const parts = ["run"];
176
+ parts.push(ticket || "prompt-file");
177
+ parts.push(`--prompt-file "${tmpFile}"`);
178
+ if (agentName) parts.push(`--name "${agentName}"`);
179
+ if (headless) parts.push("--headless");
180
+ if (model) parts.push(`--model ${model}`);
181
+ if (base_branch) parts.push(`--base ${base_branch}`);
182
+ if (max_turns) parts.push(`--max-turns ${max_turns}`);
183
+ if (headless) parts.push("--no-attach");
184
+ const output = dispatch(parts.join(" "));
185
+ return { content: [{ type: "text", text: output }] };
186
+ } finally {
187
+ try {
188
+ unlinkSync(tmpFile);
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ case "dispatch_list": {
194
+ const output = dispatch("list");
195
+ return {
196
+ content: [{ type: "text", text: output || "No agents running." }]
197
+ };
198
+ }
199
+ case "dispatch_stop": {
200
+ const { agent_id } = args;
201
+ const output = dispatch(`stop ${agent_id}`);
202
+ return { content: [{ type: "text", text: output }] };
203
+ }
204
+ case "dispatch_resume": {
205
+ const { agent_id, headless } = args;
206
+ const flags = headless ? "--headless" : "";
207
+ const output = dispatch(
208
+ `resume ${agent_id} ${flags} --no-attach`.trim()
209
+ );
210
+ return { content: [{ type: "text", text: output }] };
211
+ }
212
+ case "dispatch_cleanup": {
213
+ const { agent_id, all, delete_branch } = args;
214
+ const parts = ["cleanup"];
215
+ if (all) parts.push("--all");
216
+ else if (agent_id) parts.push(agent_id);
217
+ if (delete_branch) parts.push("--delete-branch");
218
+ const output = dispatch(parts.join(" "));
219
+ return { content: [{ type: "text", text: output }] };
220
+ }
221
+ case "dispatch_logs": {
222
+ const { agent_id, lines = 50 } = args;
223
+ const cwd = process.env.DISPATCH_CWD || process.cwd();
224
+ const logPath = join(cwd, ".worktrees", agent_id, ".dispatch.log");
225
+ if (existsSync(logPath)) {
226
+ const content = readFileSync(logPath, "utf-8");
227
+ const logLines = content.split("\n");
228
+ const lastLines = logLines.slice(-lines).join("\n");
229
+ return {
230
+ content: [{ type: "text", text: stripAnsi(lastLines) }]
231
+ };
232
+ }
233
+ try {
234
+ const output = execSync(
235
+ `tmux capture-pane -t "dispatch:${agent_id}" -p -S -${lines}`,
236
+ { encoding: "utf-8", timeout: 5e3 }
237
+ );
238
+ return {
239
+ content: [{ type: "text", text: stripAnsi(output).trim() }]
240
+ };
241
+ } catch {
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: `Agent '${agent_id}' not found or no output available.`
247
+ }
248
+ ]
249
+ };
250
+ }
251
+ }
252
+ default:
253
+ return {
254
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
255
+ isError: true
256
+ };
257
+ }
258
+ });
259
+ async function main() {
260
+ const transport = new StdioServerTransport();
261
+ await server.connect(transport);
262
+ }
263
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "dispatch-agents",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Orchestrate Claude Code agents in git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
7
- "dispatch": "./dist/cli.js"
7
+ "dispatch": "./dist/cli.js",
8
+ "dispatch-mcp": "./dist/mcp.js"
8
9
  },
9
10
  "scripts": {
10
11
  "build": "tsup",
@@ -26,6 +27,9 @@
26
27
  "type": "git",
27
28
  "url": "https://github.com/paperMoose/dispatch.git"
28
29
  },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.12.1"
32
+ },
29
33
  "devDependencies": {
30
34
  "tsup": "^8.0.0",
31
35
  "tsx": "^4.0.0",