switchman-dev 0.1.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/CLAUDE.md +98 -0
- package/README.md +243 -0
- package/examples/README.md +117 -0
- package/examples/setup.sh +102 -0
- package/examples/taskapi/.switchman/switchman.db +0 -0
- package/examples/taskapi/package-lock.json +4736 -0
- package/examples/taskapi/package.json +18 -0
- package/examples/taskapi/src/db.js +179 -0
- package/examples/taskapi/src/middleware/auth.js +96 -0
- package/examples/taskapi/src/middleware/validate.js +133 -0
- package/examples/taskapi/src/routes/tasks.js +65 -0
- package/examples/taskapi/src/routes/users.js +38 -0
- package/examples/taskapi/src/server.js +7 -0
- package/examples/taskapi/tests/api.test.js +112 -0
- package/examples/teardown.sh +37 -0
- package/examples/walkthrough.sh +172 -0
- package/examples/worktrees/agent-rate-limiting/package-lock.json +4736 -0
- package/examples/worktrees/agent-rate-limiting/package.json +18 -0
- package/examples/worktrees/agent-rate-limiting/src/db.js +179 -0
- package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-rate-limiting/src/routes/users.js +38 -0
- package/examples/worktrees/agent-rate-limiting/src/server.js +7 -0
- package/examples/worktrees/agent-rate-limiting/tests/api.test.js +112 -0
- package/examples/worktrees/agent-tests/package-lock.json +4736 -0
- package/examples/worktrees/agent-tests/package.json +18 -0
- package/examples/worktrees/agent-tests/src/db.js +179 -0
- package/examples/worktrees/agent-tests/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-tests/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-tests/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-tests/src/routes/users.js +38 -0
- package/examples/worktrees/agent-tests/src/server.js +7 -0
- package/examples/worktrees/agent-tests/tests/api.test.js +112 -0
- package/examples/worktrees/agent-validation/package-lock.json +4736 -0
- package/examples/worktrees/agent-validation/package.json +18 -0
- package/examples/worktrees/agent-validation/src/db.js +179 -0
- package/examples/worktrees/agent-validation/src/middleware/auth.js +96 -0
- package/examples/worktrees/agent-validation/src/middleware/validate.js +133 -0
- package/examples/worktrees/agent-validation/src/routes/tasks.js +65 -0
- package/examples/worktrees/agent-validation/src/routes/users.js +38 -0
- package/examples/worktrees/agent-validation/src/server.js +7 -0
- package/examples/worktrees/agent-validation/tests/api.test.js +112 -0
- package/package.json +29 -0
- package/src/cli/index.js +602 -0
- package/src/core/db.js +240 -0
- package/src/core/detector.js +172 -0
- package/src/core/git.js +265 -0
- package/src/mcp/server.js +555 -0
- package/tests/test.js +259 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* switchman-mcp-server
|
|
4
|
+
*
|
|
5
|
+
* MCP server exposing Switchman's coordination primitives to AI coding agents.
|
|
6
|
+
* Transport: stdio (local subprocess, one session per agent process).
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* switchman_task_next — get the next pending task (agents poll this)
|
|
10
|
+
* switchman_task_add — add a new task to the queue
|
|
11
|
+
* switchman_task_claim — claim files for a task (conflict-safe)
|
|
12
|
+
* switchman_task_done — mark a task complete + release file claims
|
|
13
|
+
* switchman_task_fail — mark a task failed + release file claims
|
|
14
|
+
* switchman_scan — scan all worktrees for conflicts right now
|
|
15
|
+
* switchman_status — full system overview (tasks, claims, worktrees)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
import { findRepoRoot } from '../core/git.js';
|
|
23
|
+
import {
|
|
24
|
+
openDb,
|
|
25
|
+
createTask,
|
|
26
|
+
assignTask,
|
|
27
|
+
completeTask,
|
|
28
|
+
failTask,
|
|
29
|
+
listTasks,
|
|
30
|
+
getTask,
|
|
31
|
+
getNextPendingTask,
|
|
32
|
+
claimFiles,
|
|
33
|
+
releaseFileClaims,
|
|
34
|
+
getActiveFileClaims,
|
|
35
|
+
checkFileConflicts,
|
|
36
|
+
listWorktrees,
|
|
37
|
+
} from '../core/db.js';
|
|
38
|
+
import { scanAllWorktrees } from '../core/detector.js';
|
|
39
|
+
|
|
40
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the repo root and open the database.
|
|
44
|
+
* Throws a structured error if not inside a git repo or not initialised.
|
|
45
|
+
*/
|
|
46
|
+
function getContext() {
|
|
47
|
+
const repoRoot = findRepoRoot();
|
|
48
|
+
const db = openDb(repoRoot);
|
|
49
|
+
return { repoRoot, db };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Return a tool error response with a clear, actionable message. */
|
|
53
|
+
function toolError(message) {
|
|
54
|
+
return {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Return a successful tool response. */
|
|
61
|
+
function toolOk(text, structured = undefined) {
|
|
62
|
+
const response = {
|
|
63
|
+
content: [{ type: 'text', text: typeof text === 'string' ? text : JSON.stringify(text, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
if (structured !== undefined) response.structuredContent = structured;
|
|
66
|
+
return response;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Server ───────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: 'switchman-mcp-server',
|
|
73
|
+
version: '0.1.0',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── switchman_task_next ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
server.registerTool(
|
|
79
|
+
'switchman_task_next',
|
|
80
|
+
{
|
|
81
|
+
title: 'Get Next Pending Task',
|
|
82
|
+
description: `Returns the highest-priority pending task from the Switchman queue, then assigns it to the specified worktree so no other agent picks it up.
|
|
83
|
+
|
|
84
|
+
Call this at the start of each agent session to claim your work. Returns null if the queue is empty.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
- worktree (string): The git worktree name this agent is running in (e.g. "feature-auth"). Run 'git worktree list' to find yours.
|
|
88
|
+
- agent (string, optional): Human-readable agent identifier for logging (e.g. "claude-code", "cursor").
|
|
89
|
+
|
|
90
|
+
Returns JSON:
|
|
91
|
+
{
|
|
92
|
+
"task": {
|
|
93
|
+
"id": string, // Task ID to use in subsequent calls
|
|
94
|
+
"title": string,
|
|
95
|
+
"description": string | null,
|
|
96
|
+
"priority": number, // 1-10, higher = more urgent
|
|
97
|
+
"worktree": string,
|
|
98
|
+
"status": "in_progress"
|
|
99
|
+
} | null // null when queue is empty
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
- Agent starts up: call switchman_task_next with your worktree name
|
|
104
|
+
- Queue is empty: returns { "task": null } — agent should wait or stop
|
|
105
|
+
- After receiving a task: call switchman_task_claim to declare which files you'll edit`,
|
|
106
|
+
inputSchema: z.object({
|
|
107
|
+
worktree: z.string().min(1).describe("Your git worktree name. Run 'git worktree list' to find it."),
|
|
108
|
+
agent: z.string().optional().describe('Agent identifier for logging (e.g. "claude-code")'),
|
|
109
|
+
}),
|
|
110
|
+
annotations: {
|
|
111
|
+
readOnlyHint: false,
|
|
112
|
+
destructiveHint: false,
|
|
113
|
+
idempotentHint: false,
|
|
114
|
+
openWorldHint: false,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
async ({ worktree, agent }) => {
|
|
118
|
+
try {
|
|
119
|
+
const { db } = getContext();
|
|
120
|
+
const task = getNextPendingTask(db);
|
|
121
|
+
|
|
122
|
+
if (!task) {
|
|
123
|
+
db.close();
|
|
124
|
+
return toolOk(JSON.stringify({ task: null }), { task: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const assigned = assignTask(db, task.id, worktree, agent ?? null);
|
|
128
|
+
db.close();
|
|
129
|
+
|
|
130
|
+
if (!assigned) {
|
|
131
|
+
// Race condition: another agent grabbed it first — try again
|
|
132
|
+
return toolOk(
|
|
133
|
+
JSON.stringify({ task: null, message: 'Task was claimed by another agent. Call switchman_task_next again.' }),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = { task: { ...task, worktree, status: 'in_progress' } };
|
|
138
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return toolError(`${err.message}. Make sure switchman is initialised in this repo (run 'switchman init').`);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ── switchman_task_add ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
server.registerTool(
|
|
148
|
+
'switchman_task_add',
|
|
149
|
+
{
|
|
150
|
+
title: 'Add Task to Queue',
|
|
151
|
+
description: `Adds a new task to the Switchman task queue.
|
|
152
|
+
|
|
153
|
+
Use this to break down a large feature into parallelisable subtasks before spinning up worker agents.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
- title (string): Short description of the work (e.g. "Implement OAuth login flow")
|
|
157
|
+
- description (string, optional): Detailed implementation notes
|
|
158
|
+
- priority (number, optional): 1-10, default 5. Higher = picked up first.
|
|
159
|
+
- id (string, optional): Custom task ID. Auto-generated if omitted.
|
|
160
|
+
|
|
161
|
+
Returns JSON:
|
|
162
|
+
{
|
|
163
|
+
"task_id": string, // ID to reference in assign/claim/done calls
|
|
164
|
+
"title": string,
|
|
165
|
+
"priority": number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
- Add high-priority security task: { title: "Fix SQL injection in login", priority: 9 }
|
|
170
|
+
- Add background task: { title: "Update README", priority: 2 }`,
|
|
171
|
+
inputSchema: z.object({
|
|
172
|
+
title: z.string().min(1).max(200).describe('Short description of the work'),
|
|
173
|
+
description: z.string().max(2000).optional().describe('Detailed implementation notes'),
|
|
174
|
+
priority: z.number().int().min(1).max(10).default(5).describe('Priority 1-10, higher = picked up first'),
|
|
175
|
+
id: z.string().max(100).optional().describe('Custom task ID (auto-generated if omitted)'),
|
|
176
|
+
}),
|
|
177
|
+
annotations: {
|
|
178
|
+
readOnlyHint: false,
|
|
179
|
+
destructiveHint: false,
|
|
180
|
+
idempotentHint: false,
|
|
181
|
+
openWorldHint: false,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async ({ title, description, priority, id }) => {
|
|
185
|
+
try {
|
|
186
|
+
const { db } = getContext();
|
|
187
|
+
const taskId = createTask(db, { id, title, description, priority });
|
|
188
|
+
db.close();
|
|
189
|
+
|
|
190
|
+
const result = { task_id: taskId, title, priority };
|
|
191
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return toolError(err.message);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// ── switchman_task_claim ───────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
server.registerTool(
|
|
201
|
+
'switchman_task_claim',
|
|
202
|
+
{
|
|
203
|
+
title: 'Claim Files for a Task',
|
|
204
|
+
description: `Declares which files this agent intends to edit for a task.
|
|
205
|
+
|
|
206
|
+
Call this immediately after receiving a task, before making any edits. Switchman checks whether any of the files are already claimed by another active task in a different worktree and warns you before a conflict can occur.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
- task_id (string): The task ID returned by switchman_task_next or switchman_task_add
|
|
210
|
+
- worktree (string): Your git worktree name
|
|
211
|
+
- files (array of strings): File paths you plan to edit, relative to repo root (e.g. ["src/auth/login.js", "tests/auth.test.js"])
|
|
212
|
+
- agent (string, optional): Agent identifier for logging
|
|
213
|
+
- force (boolean, optional): If true, claim even if conflicts exist (default: false)
|
|
214
|
+
|
|
215
|
+
Returns JSON:
|
|
216
|
+
{
|
|
217
|
+
"claimed": string[], // Files successfully claimed
|
|
218
|
+
"conflicts": [ // Files already claimed by other worktrees
|
|
219
|
+
{
|
|
220
|
+
"file": string,
|
|
221
|
+
"claimed_by_worktree": string,
|
|
222
|
+
"claimed_by_task": string
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
"safe_to_proceed": boolean // true if no conflicts (or force=true)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
- Before editing auth files: files: ["src/auth/login.js", "src/auth/token.js"]
|
|
230
|
+
- If conflicts returned: coordinate with other agents or choose different files
|
|
231
|
+
- Do NOT use force=true unless you've confirmed the conflict is safe to override`,
|
|
232
|
+
inputSchema: z.object({
|
|
233
|
+
task_id: z.string().min(1).describe('Task ID from switchman_task_next'),
|
|
234
|
+
worktree: z.string().min(1).describe('Your git worktree name'),
|
|
235
|
+
files: z.array(z.string().min(1)).min(1).max(500).describe('File paths to claim, relative to repo root'),
|
|
236
|
+
agent: z.string().optional().describe('Agent identifier for logging'),
|
|
237
|
+
force: z.boolean().default(false).describe('Claim even if conflicts exist (use with caution)'),
|
|
238
|
+
}),
|
|
239
|
+
annotations: {
|
|
240
|
+
readOnlyHint: false,
|
|
241
|
+
destructiveHint: false,
|
|
242
|
+
idempotentHint: true,
|
|
243
|
+
openWorldHint: false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
async ({ task_id, worktree, files, agent, force }) => {
|
|
247
|
+
try {
|
|
248
|
+
const { db } = getContext();
|
|
249
|
+
|
|
250
|
+
// Check for conflicts first
|
|
251
|
+
const conflicts = checkFileConflicts(db, files, worktree);
|
|
252
|
+
|
|
253
|
+
if (conflicts.length > 0 && !force) {
|
|
254
|
+
db.close();
|
|
255
|
+
const result = {
|
|
256
|
+
claimed: [],
|
|
257
|
+
conflicts: conflicts.map((c) => ({
|
|
258
|
+
file: c.file,
|
|
259
|
+
claimed_by_worktree: c.claimedBy.worktree,
|
|
260
|
+
claimed_by_task: c.claimedBy.task_title,
|
|
261
|
+
})),
|
|
262
|
+
safe_to_proceed: false,
|
|
263
|
+
};
|
|
264
|
+
return toolOk(
|
|
265
|
+
`Conflicts detected — ${conflicts.length} file(s) already claimed by other worktrees.\n` +
|
|
266
|
+
JSON.stringify(result, null, 2),
|
|
267
|
+
result,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
claimFiles(db, task_id, worktree, files, agent ?? null);
|
|
272
|
+
db.close();
|
|
273
|
+
|
|
274
|
+
const result = {
|
|
275
|
+
claimed: files,
|
|
276
|
+
conflicts: conflicts.map((c) => ({
|
|
277
|
+
file: c.file,
|
|
278
|
+
claimed_by_worktree: c.claimedBy.worktree,
|
|
279
|
+
claimed_by_task: c.claimedBy.task_title,
|
|
280
|
+
})),
|
|
281
|
+
safe_to_proceed: true,
|
|
282
|
+
};
|
|
283
|
+
return toolOk(
|
|
284
|
+
`Claimed ${files.length} file(s) for task ${task_id}.\n` + JSON.stringify(result, null, 2),
|
|
285
|
+
result,
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return toolError(err.message);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// ── switchman_task_done ────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
server.registerTool(
|
|
296
|
+
'switchman_task_done',
|
|
297
|
+
{
|
|
298
|
+
title: 'Mark Task Complete',
|
|
299
|
+
description: `Marks a task as done and always releases all file claims so other agents can pick up those files.
|
|
300
|
+
|
|
301
|
+
Call this when you have finished your implementation and committed your changes.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
- task_id (string): The task ID to complete
|
|
305
|
+
|
|
306
|
+
Returns JSON:
|
|
307
|
+
{
|
|
308
|
+
"task_id": string,
|
|
309
|
+
"status": "done",
|
|
310
|
+
"files_released": true
|
|
311
|
+
}`,
|
|
312
|
+
inputSchema: z.object({
|
|
313
|
+
task_id: z.string().min(1).describe('The task ID to mark complete'),
|
|
314
|
+
}),
|
|
315
|
+
annotations: {
|
|
316
|
+
readOnlyHint: false,
|
|
317
|
+
destructiveHint: false,
|
|
318
|
+
idempotentHint: true,
|
|
319
|
+
openWorldHint: false,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
async ({ task_id }) => {
|
|
323
|
+
try {
|
|
324
|
+
const { db } = getContext();
|
|
325
|
+
completeTask(db, task_id);
|
|
326
|
+
releaseFileClaims(db, task_id);
|
|
327
|
+
db.close();
|
|
328
|
+
|
|
329
|
+
const result = { task_id, status: 'done', files_released: true };
|
|
330
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return toolError(err.message);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// ── switchman_task_fail ────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
server.registerTool(
|
|
340
|
+
'switchman_task_fail',
|
|
341
|
+
{
|
|
342
|
+
title: 'Mark Task Failed',
|
|
343
|
+
description: `Marks a task as failed, records the reason, and releases all file claims.
|
|
344
|
+
|
|
345
|
+
Call this if you cannot complete the task — the task will be visible in the queue as failed so a human can review it. File claims are always released on failure so other agents aren't blocked.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
- task_id (string): The task ID to mark as failed
|
|
349
|
+
- reason (string): Brief explanation of why the task failed
|
|
350
|
+
|
|
351
|
+
Returns JSON:
|
|
352
|
+
{
|
|
353
|
+
"task_id": string,
|
|
354
|
+
"status": "failed",
|
|
355
|
+
"reason": string,
|
|
356
|
+
"files_released": true
|
|
357
|
+
}`,
|
|
358
|
+
inputSchema: z.object({
|
|
359
|
+
task_id: z.string().min(1).describe('The task ID to mark as failed'),
|
|
360
|
+
reason: z.string().min(1).max(500).describe('Brief explanation of why the task failed'),
|
|
361
|
+
}),
|
|
362
|
+
annotations: {
|
|
363
|
+
readOnlyHint: false,
|
|
364
|
+
destructiveHint: false,
|
|
365
|
+
idempotentHint: true,
|
|
366
|
+
openWorldHint: false,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
async ({ task_id, reason }) => {
|
|
370
|
+
try {
|
|
371
|
+
const { db } = getContext();
|
|
372
|
+
failTask(db, task_id, reason);
|
|
373
|
+
releaseFileClaims(db, task_id);
|
|
374
|
+
db.close();
|
|
375
|
+
|
|
376
|
+
const result = { task_id, status: 'failed', reason, files_released: true };
|
|
377
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
return toolError(err.message);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// ── switchman_scan ─────────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
server.registerTool(
|
|
387
|
+
'switchman_scan',
|
|
388
|
+
{
|
|
389
|
+
title: 'Scan for Conflicts',
|
|
390
|
+
description: `Scans all git worktrees for conflicts — both uncommitted file overlaps and branch-level merge conflicts.
|
|
391
|
+
|
|
392
|
+
Run this before starting work on a task, before merging a branch, or any time you want to verify the workspace is clean. This is a read-only operation that never modifies any files.
|
|
393
|
+
|
|
394
|
+
Two detection layers:
|
|
395
|
+
1. Uncommitted file overlaps — files being edited in multiple worktrees right now
|
|
396
|
+
2. Branch-level merge conflicts — branches that would conflict when merged (uses git merge-tree)
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
- (none required)
|
|
400
|
+
|
|
401
|
+
Returns JSON:
|
|
402
|
+
{
|
|
403
|
+
"worktrees": [{ "name": string, "branch": string, "changed_files": number }],
|
|
404
|
+
"file_conflicts": [ // Files touched in multiple worktrees
|
|
405
|
+
{ "file": string, "worktrees": string[] }
|
|
406
|
+
],
|
|
407
|
+
"branch_conflicts": [ // Branches with merge conflicts
|
|
408
|
+
{
|
|
409
|
+
"type": "merge_conflict" | "file_overlap",
|
|
410
|
+
"worktree_a": string, "branch_a": string,
|
|
411
|
+
"worktree_b": string, "branch_b": string,
|
|
412
|
+
"conflicting_files": string[]
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
"safe_to_proceed": boolean, // true when no conflicts found
|
|
416
|
+
"summary": string
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
Examples:
|
|
420
|
+
- Before editing files: ensure safe_to_proceed is true
|
|
421
|
+
- If file_conflicts found: coordinate with agents in other worktrees
|
|
422
|
+
- If branch_conflicts found: resolve before merging to main`,
|
|
423
|
+
inputSchema: z.object({}),
|
|
424
|
+
annotations: {
|
|
425
|
+
readOnlyHint: true,
|
|
426
|
+
destructiveHint: false,
|
|
427
|
+
idempotentHint: true,
|
|
428
|
+
openWorldHint: false,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
async () => {
|
|
432
|
+
try {
|
|
433
|
+
const { repoRoot, db } = getContext();
|
|
434
|
+
const report = await scanAllWorktrees(db, repoRoot);
|
|
435
|
+
db.close();
|
|
436
|
+
|
|
437
|
+
const result = {
|
|
438
|
+
worktrees: report.worktrees.map((wt) => ({
|
|
439
|
+
name: wt.name,
|
|
440
|
+
branch: wt.branch ?? 'unknown',
|
|
441
|
+
changed_files: (report.fileMap?.[wt.name] ?? []).length,
|
|
442
|
+
})),
|
|
443
|
+
file_conflicts: report.fileConflicts,
|
|
444
|
+
branch_conflicts: report.conflicts.map((c) => ({
|
|
445
|
+
type: c.type,
|
|
446
|
+
worktree_a: c.worktreeA,
|
|
447
|
+
branch_a: c.branchA,
|
|
448
|
+
worktree_b: c.worktreeB,
|
|
449
|
+
branch_b: c.branchB,
|
|
450
|
+
conflicting_files: c.conflictingFiles,
|
|
451
|
+
})),
|
|
452
|
+
safe_to_proceed: report.conflicts.length === 0 && report.fileConflicts.length === 0,
|
|
453
|
+
summary: report.summary,
|
|
454
|
+
};
|
|
455
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return toolError(`Scan failed: ${err.message}. Ensure switchman is initialised ('switchman init').`);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// ── switchman_status ───────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
server.registerTool(
|
|
465
|
+
'switchman_status',
|
|
466
|
+
{
|
|
467
|
+
title: 'Get System Status',
|
|
468
|
+
description: `Returns a full overview of the Switchman coordination state: task queue counts, active tasks, file claims, and worktree list.
|
|
469
|
+
|
|
470
|
+
Use this to understand the current state before starting work, or to check what other agents are doing.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
- (none required)
|
|
474
|
+
|
|
475
|
+
Returns JSON:
|
|
476
|
+
{
|
|
477
|
+
"tasks": {
|
|
478
|
+
"pending": number,
|
|
479
|
+
"in_progress": number,
|
|
480
|
+
"done": number,
|
|
481
|
+
"failed": number,
|
|
482
|
+
"active": [{ "id": string, "title": string, "worktree": string, "priority": number }]
|
|
483
|
+
},
|
|
484
|
+
"file_claims": {
|
|
485
|
+
"total_active": number,
|
|
486
|
+
"by_worktree": { [worktree: string]: string[] } // worktree -> files
|
|
487
|
+
},
|
|
488
|
+
"worktrees": [{ "name": string, "branch": string, "agent": string | null, "status": string }],
|
|
489
|
+
"repo_root": string
|
|
490
|
+
}`,
|
|
491
|
+
inputSchema: z.object({}),
|
|
492
|
+
annotations: {
|
|
493
|
+
readOnlyHint: true,
|
|
494
|
+
destructiveHint: false,
|
|
495
|
+
idempotentHint: true,
|
|
496
|
+
openWorldHint: false,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
async () => {
|
|
500
|
+
try {
|
|
501
|
+
const { repoRoot, db } = getContext();
|
|
502
|
+
|
|
503
|
+
const tasks = listTasks(db);
|
|
504
|
+
const claims = getActiveFileClaims(db);
|
|
505
|
+
const worktrees = listWorktrees(db);
|
|
506
|
+
db.close();
|
|
507
|
+
|
|
508
|
+
const byWorktree = {};
|
|
509
|
+
for (const c of claims) {
|
|
510
|
+
if (!byWorktree[c.worktree]) byWorktree[c.worktree] = [];
|
|
511
|
+
byWorktree[c.worktree].push(c.file_path);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const result = {
|
|
515
|
+
tasks: {
|
|
516
|
+
pending: tasks.filter((t) => t.status === 'pending').length,
|
|
517
|
+
in_progress: tasks.filter((t) => t.status === 'in_progress').length,
|
|
518
|
+
done: tasks.filter((t) => t.status === 'done').length,
|
|
519
|
+
failed: tasks.filter((t) => t.status === 'failed').length,
|
|
520
|
+
active: tasks
|
|
521
|
+
.filter((t) => t.status === 'in_progress')
|
|
522
|
+
.map((t) => ({ id: t.id, title: t.title, worktree: t.worktree, priority: t.priority })),
|
|
523
|
+
},
|
|
524
|
+
file_claims: {
|
|
525
|
+
total_active: claims.length,
|
|
526
|
+
by_worktree: byWorktree,
|
|
527
|
+
},
|
|
528
|
+
worktrees: worktrees.map((wt) => ({
|
|
529
|
+
name: wt.name,
|
|
530
|
+
branch: wt.branch,
|
|
531
|
+
agent: wt.agent ?? null,
|
|
532
|
+
status: wt.status,
|
|
533
|
+
})),
|
|
534
|
+
repo_root: repoRoot,
|
|
535
|
+
};
|
|
536
|
+
return toolOk(JSON.stringify(result, null, 2), result);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return toolError(err.message);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// ─── Transport ────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
async function main() {
|
|
546
|
+
const transport = new StdioServerTransport();
|
|
547
|
+
await server.connect(transport);
|
|
548
|
+
// stdio servers must not write to stdout — all logging goes to stderr
|
|
549
|
+
process.stderr.write('switchman MCP server running (stdio)\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
main().catch((err) => {
|
|
553
|
+
process.stderr.write(`switchman MCP server fatal error: ${err.message}\n`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
});
|