@programinglive/dev-workflow-mcp-server 1.0.9 → 1.0.11

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/index.js CHANGED
@@ -8,475 +8,34 @@ import {
8
8
  ListPromptsRequestSchema,
9
9
  GetPromptRequestSchema,
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
- import { exec as execCallback } from "child_process";
12
- import fs from "fs/promises";
13
- import path from "path";
14
11
  import { fileURLToPath } from "url";
15
- import { promisify } from "util";
16
-
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = path.dirname(__filename);
19
-
20
- // Workflow state management
21
- export class WorkflowState {
22
- constructor(stateFilePath = path.join(__dirname, ".workflow-state.json")) {
23
- this.stateFile = stateFilePath;
24
- this.state = {
25
- currentPhase: "idle",
26
- taskDescription: "",
27
- bugFixed: false,
28
- testsCreated: false,
29
- testsPassed: false,
30
- testsSkipped: false,
31
- testsSkippedReason: "",
32
- documentationCreated: false,
33
- readyToCommit: false,
34
- readyCheckCompleted: false,
35
- released: false,
36
- releaseCommand: "",
37
- releaseNotes: "",
38
- commitAndPushCompleted: false,
39
- lastCommitMessage: "",
40
- lastPushBranch: "",
41
- history: [],
42
- };
43
- }
44
-
45
- async load() {
46
- try {
47
- const data = await fs.readFile(this.stateFile, "utf-8");
48
- const parsed = JSON.parse(data);
49
- this.state = {
50
- ...this.state,
51
- ...parsed,
52
- };
53
- if (typeof this.state.testsCreated !== "boolean") {
54
- this.state.testsCreated = false;
55
- }
56
- if (typeof this.state.testsSkipped !== "boolean") {
57
- this.state.testsSkipped = false;
58
- }
59
- if (typeof this.state.testsSkippedReason !== "string") {
60
- this.state.testsSkippedReason = "";
61
- }
62
- if (typeof this.state.readyCheckCompleted !== "boolean") {
63
- this.state.readyCheckCompleted = false;
64
- }
65
- if (typeof this.state.released !== "boolean") {
66
- this.state.released = false;
67
- }
68
- if (typeof this.state.commitAndPushCompleted !== "boolean") {
69
- this.state.commitAndPushCompleted = false;
70
- }
71
- if (typeof this.state.releaseCommand !== "string") {
72
- this.state.releaseCommand = "";
73
- }
74
- if (typeof this.state.releaseNotes !== "string") {
75
- this.state.releaseNotes = "";
76
- }
77
- if (typeof this.state.lastCommitMessage !== "string") {
78
- this.state.lastCommitMessage = "";
79
- }
80
- if (typeof this.state.lastPushBranch !== "string") {
81
- this.state.lastPushBranch = "";
82
- }
83
- } catch (error) {
84
- // File doesn't exist yet, use default state
85
- }
86
- }
87
-
88
- async save() {
89
- await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
90
- }
12
+ import { exec } from "./exec.js";
13
+ import { normalizeRequestArgs, shellEscape } from "./utils.js";
14
+ import {
15
+ getCurrentBranch,
16
+ getLastCommitMessage,
17
+ getStagedChanges,
18
+ hasStagedChanges,
19
+ hasTestChanges,
20
+ hasWorkingChanges,
21
+ workingTreeSummary,
22
+ } from "./git-helpers.js";
23
+ import {
24
+ createCommitMessageParts,
25
+ determineReleaseTypeFromCommit,
26
+ } from "./commit-helpers.js";
27
+ import { WorkflowState } from "./workflow-state.js";
28
+ import { getToolList } from "./tools/definitions.js";
29
+ import { handleToolCall } from "./tools/handlers.js";
91
30
 
92
- reset() {
93
- this.state = {
94
- currentPhase: "idle",
95
- taskDescription: "",
96
- bugFixed: false,
97
- testsCreated: false,
98
- testsPassed: false,
99
- testsSkipped: false,
100
- testsSkippedReason: "",
101
- documentationCreated: false,
102
- readyToCommit: false,
103
- readyCheckCompleted: false,
104
- released: false,
105
- releaseCommand: "",
106
- releaseNotes: "",
107
- commitAndPushCompleted: false,
108
- lastCommitMessage: "",
109
- lastPushBranch: "",
110
- history: this.state.history,
111
- };
112
- }
113
-
114
- addToHistory(entry) {
115
- this.state.history.push({
116
- ...entry,
117
- timestamp: new Date().toISOString(),
118
- });
119
- }
120
- }
31
+ export { createCommitMessageParts, determineReleaseTypeFromCommit } from "./commit-helpers.js";
32
+ export { containsTestFilesInStatus, workingTreeSummary } from "./git-helpers.js";
33
+ export { WorkflowState } from "./workflow-state.js";
121
34
 
122
35
  export const workflowState = new WorkflowState();
123
36
  await workflowState.load();
124
37
 
125
- const exec = promisify(execCallback);
126
-
127
- const isWindows = process.platform === "win32";
128
-
129
- function shellEscape(value) {
130
- const stringValue = String(value);
131
- if (isWindows) {
132
- // Use PowerShell-friendly single quotes, escape embedded single quotes by doubling
133
- return `'${stringValue.replace(/'/g, "''")}'`;
134
- }
135
-
136
- return `'${stringValue.replace(/'/g, "'\\''")}'`;
137
- }
138
-
139
- function normalizeRequestArgs(rawArgs) {
140
- if (rawArgs === undefined || rawArgs === null) {
141
- return { args: {}, error: null };
142
- }
143
-
144
- if (typeof rawArgs === "string") {
145
- try {
146
- const parsed = JSON.parse(rawArgs);
147
- if (parsed && typeof parsed === "object") {
148
- return { args: parsed, error: null };
149
- }
150
-
151
- return {
152
- args: {},
153
- error:
154
- "⚠️ Tool arguments must be a JSON object. Please provide key/value pairs.",
155
- };
156
- } catch (parseError) {
157
- return {
158
- args: {},
159
- error:
160
- "⚠️ Unable to parse tool arguments. Please provide valid JSON-formatted data.",
161
- };
162
- }
163
- }
164
-
165
- if (typeof rawArgs === "object") {
166
- return { args: rawArgs, error: null };
167
- }
168
-
169
- return {
170
- args: {},
171
- error: "⚠️ Unsupported tool arguments format. Expected a JSON object.",
172
- };
173
- }
174
-
175
- async function hasStagedChanges() {
176
- try {
177
- const { stdout } = await exec("git status --porcelain");
178
- return stdout
179
- .split("\n")
180
- .filter(Boolean)
181
- .some((line) => {
182
- const indexStatus = line[0];
183
- return indexStatus && indexStatus !== " " && indexStatus !== "?";
184
- });
185
- } catch (error) {
186
- return false;
187
- }
188
- }
189
-
190
- async function hasWorkingChanges() {
191
- try {
192
- const { stdout } = await exec("git status --porcelain");
193
- return stdout.split("\n").some((line) => line && line.trim().length > 0);
194
- } catch (error) {
195
- return false;
196
- }
197
- }
198
-
199
- export function workingTreeSummary(statusOutput) {
200
- if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
201
- return {
202
- hasChanges: false,
203
- lines: [],
204
- };
205
- }
206
-
207
- const lines = statusOutput
208
- .split("\n")
209
- .map((line) => line.replace(/\s+/g, " ").trim())
210
- .filter(Boolean);
211
-
212
- return {
213
- hasChanges: lines.length > 0,
214
- lines,
215
- };
216
- }
217
-
218
- function isTestFilePath(filePath) {
219
- if (!filePath) {
220
- return false;
221
- }
222
-
223
- if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
224
- return true;
225
- }
226
-
227
- return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
228
- }
229
-
230
- function extractPathFromStatusLine(line) {
231
- if (!line || line.length < 4) {
232
- return "";
233
- }
234
-
235
- const rawPath = line.slice(3).trim();
236
- if (!rawPath) {
237
- return "";
238
- }
239
-
240
- if (rawPath.includes(" -> ")) {
241
- const parts = rawPath.split(" -> ");
242
- return parts[parts.length - 1].trim();
243
- }
244
-
245
- return rawPath;
246
- }
247
-
248
- export function containsTestFilesInStatus(statusOutput) {
249
- if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
250
- return false;
251
- }
252
-
253
- return statusOutput
254
- .split("\n")
255
- .map((line) => extractPathFromStatusLine(line))
256
- .some((path) => isTestFilePath(path));
257
- }
258
-
259
- function isDocumentationFilePath(filePath) {
260
- if (!filePath) {
261
- return false;
262
- }
263
-
264
- const normalized = filePath.toLowerCase();
265
- return (
266
- normalized.startsWith("docs/") ||
267
- normalized.startsWith("documentation/") ||
268
- normalized.endsWith(".md") ||
269
- normalized.endsWith(".rst") ||
270
- normalized.endsWith(".adoc") ||
271
- normalized.includes("readme")
272
- );
273
- }
274
-
275
- function parseNameStatusLine(line) {
276
- if (!line) {
277
- return null;
278
- }
279
-
280
- const parts = line.split("\t").map((part) => part.trim());
281
- const status = parts[0];
282
- if (!status) {
283
- return null;
284
- }
285
-
286
- if (status.startsWith("R") || status.startsWith("C")) {
287
- const originalPath = parts[1] || "";
288
- const newPath = parts[2] || originalPath;
289
- return {
290
- status,
291
- path: newPath,
292
- originalPath,
293
- };
294
- }
295
-
296
- return {
297
- status,
298
- path: parts[1] || "",
299
- originalPath: "",
300
- };
301
- }
302
-
303
- function formatFileList(paths) {
304
- const uniquePaths = [...new Set(paths.filter(Boolean))];
305
-
306
- if (uniquePaths.length === 0) {
307
- return "updates";
308
- }
309
-
310
- if (uniquePaths.length === 1) {
311
- return uniquePaths[0];
312
- }
313
-
314
- if (uniquePaths.length === 2) {
315
- return `${uniquePaths[0]} and ${uniquePaths[1]}`;
316
- }
317
-
318
- if (uniquePaths.length === 3) {
319
- return `${uniquePaths[0]}, ${uniquePaths[1]}, and ${uniquePaths[2]}`;
320
- }
321
-
322
- const remaining = uniquePaths.length - 3;
323
- return `${uniquePaths[0]}, ${uniquePaths[1]}, ${uniquePaths[2]} +${remaining} more`;
324
- }
325
-
326
- function determineCommitType(changes) {
327
- const hasTestChange = changes.some((change) =>
328
- [change.path, change.originalPath].some((p) => isTestFilePath(p))
329
- );
330
- const hasDocsChange = changes.some((change) =>
331
- [change.path, change.originalPath].some((p) => isDocumentationFilePath(p))
332
- );
333
-
334
- if (hasTestChange) {
335
- return "test";
336
- }
337
-
338
- if (hasDocsChange) {
339
- return "docs";
340
- }
341
-
342
- return "chore";
343
- }
344
-
345
- function describeChange(change) {
346
- const statusCode = change.status || "";
347
- const path = change.path || change.originalPath || "";
348
-
349
- if (statusCode.startsWith("R")) {
350
- return `renamed ${change.originalPath} -> ${change.path}`;
351
- }
352
-
353
- if (statusCode.startsWith("C")) {
354
- return `copied ${change.originalPath} -> ${change.path}`;
355
- }
356
-
357
- switch (statusCode[0]) {
358
- case "A":
359
- return `added ${path}`;
360
- case "M":
361
- return `modified ${path}`;
362
- case "D":
363
- return `removed ${path}`;
364
- case "U":
365
- return `updated ${path}`;
366
- default:
367
- return `${statusCode} ${path}`.trim();
368
- }
369
- }
370
-
371
- export function createCommitMessageParts(changes, providedSummary = "") {
372
- const summaryInput = typeof providedSummary === "string" ? providedSummary.trim() : "";
373
-
374
- if (!Array.isArray(changes) || changes.length === 0) {
375
- return {
376
- summary: summaryInput,
377
- body: "",
378
- };
379
- }
380
-
381
- const commitType = determineCommitType(changes);
382
- const summaryPaths = changes
383
- .map((change) => change.path || change.originalPath)
384
- .filter(Boolean);
385
- const generatedSummary = `${commitType}: update ${formatFileList(summaryPaths)}`;
386
- const summary = summaryInput || generatedSummary;
387
-
388
- const detailLines = changes
389
- .map((change) => describeChange(change))
390
- .filter((line) => line && line.trim().length > 0)
391
- .map((line) => `- ${line}`);
392
-
393
- return {
394
- summary,
395
- body: detailLines.join("\n"),
396
- };
397
- }
398
-
399
- async function getStagedChanges() {
400
- try {
401
- const { stdout } = await exec("git diff --cached --name-status");
402
- return stdout
403
- .split("\n")
404
- .map((line) => parseNameStatusLine(line))
405
- .filter(Boolean);
406
- } catch (error) {
407
- return [];
408
- }
409
- }
410
-
411
- async function hasTestChanges() {
412
- try {
413
- const { stdout } = await exec("git status --porcelain");
414
- return containsTestFilesInStatus(stdout);
415
- } catch (error) {
416
- return false;
417
- }
418
- }
419
-
420
- function normalizeCommitHeader(message) {
421
- if (typeof message !== "string") {
422
- return "";
423
- }
424
-
425
- const header = message.split("\n")[0] || "";
426
- return header.trim();
427
- }
428
-
429
- function containsBreakingChange(message) {
430
- if (typeof message !== "string") {
431
- return false;
432
- }
433
-
434
- return /breaking change/i.test(message);
435
- }
436
-
437
- export function determineReleaseTypeFromCommit(message) {
438
- const header = normalizeCommitHeader(message).toLowerCase();
439
- const hasBreaking = containsBreakingChange(message) || /!/.test(header.split(":")[0] || "");
440
-
441
- if (hasBreaking) {
442
- return "major";
443
- }
444
-
445
- const typeMatch = header.match(/^([a-z]+)(\(|:)/i);
446
- const commitType = typeMatch ? typeMatch[1] : "";
447
-
448
- if (commitType === "feat") {
449
- return "minor";
450
- }
451
-
452
- if (commitType === "fix" || commitType === "perf") {
453
- return "patch";
454
- }
455
-
456
- if (!commitType && containsBreakingChange(message)) {
457
- return "major";
458
- }
459
-
460
- return "patch";
461
- }
462
-
463
- async function getLastCommitMessage() {
464
- try {
465
- const { stdout } = await exec("git log -1 --pretty=%B");
466
- return stdout.trim();
467
- } catch (error) {
468
- return "";
469
- }
470
- }
471
-
472
- async function getCurrentBranch() {
473
- try {
474
- const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
475
- return stdout.trim();
476
- } catch (error) {
477
- return "";
478
- }
479
- }
38
+ const __filename = fileURLToPath(import.meta.url);
480
39
 
481
40
  // Create MCP server
482
41
  export const server = new Server(
@@ -493,844 +52,29 @@ export const server = new Server(
493
52
  );
494
53
 
495
54
  // Define tools
496
- server.setRequestHandler(ListToolsRequestSchema, async () => {
497
- return {
498
- tools: [
499
- {
500
- name: "start_task",
501
- description:
502
- "Start a new coding task. This is the first step - be conscious about what you're coding.",
503
- inputSchema: {
504
- type: "object",
505
- properties: {
506
- description: {
507
- type: "string",
508
- description: "Clear description of what you're going to code",
509
- },
510
- type: {
511
- type: "string",
512
- enum: ["feature", "bugfix", "refactor", "other"],
513
- description: "Type of task",
514
- },
515
- },
516
- required: ["description", "type"],
517
- },
518
- },
519
- {
520
- name: "mark_bug_fixed",
521
- description:
522
- "Mark that the bug/feature is fixed. Reminder: Now you MUST create tests!",
523
- inputSchema: {
524
- type: "object",
525
- properties: {
526
- summary: {
527
- type: "string",
528
- description: "Brief summary of what was fixed/implemented",
529
- },
530
- },
531
- required: ["summary"],
532
- },
533
- },
534
- {
535
- name: "create_tests",
536
- description: "Mark that tests covering the change have been created.",
537
- inputSchema: {
538
- type: "object",
539
- properties: {},
540
- },
541
- },
542
- {
543
- name: "skip_tests",
544
- description:
545
- "Record that tests were intentionally skipped, with a justification.",
546
- inputSchema: {
547
- type: "object",
548
- properties: {
549
- reason: {
550
- type: "string",
551
- description: "Why tests cannot be created or executed",
552
- },
553
- },
554
- required: ["reason"],
555
- },
556
- },
557
- {
558
- name: "run_tests",
559
- description:
560
- "Record test results. NEVER commit if tests fail! Only proceed if all tests are green.",
561
- inputSchema: {
562
- type: "object",
563
- properties: {
564
- passed: {
565
- type: "boolean",
566
- description: "Did all tests pass?",
567
- },
568
- testCommand: {
569
- type: "string",
570
- description: "The test command that was run",
571
- },
572
- details: {
573
- type: "string",
574
- description: "Test results details",
575
- },
576
- },
577
- required: ["passed", "testCommand"],
578
- },
579
- },
580
- {
581
- name: "create_documentation",
582
- description:
583
- "Mark that documentation has been created/updated. This is required before committing.",
584
- inputSchema: {
585
- type: "object",
586
- properties: {
587
- documentationType: {
588
- type: "string",
589
- enum: ["README", "inline-comments", "API-docs", "changelog", "other"],
590
- description: "Type of documentation created",
591
- },
592
- summary: {
593
- type: "string",
594
- description: "What was documented",
595
- },
596
- },
597
- required: ["documentationType", "summary"],
598
- },
599
- },
600
- {
601
- name: "check_ready_to_commit",
602
- description:
603
- "Check if all workflow steps are completed and you're ready to commit & push.",
604
- inputSchema: {
605
- type: "object",
606
- properties: {},
607
- },
608
- },
609
- {
610
- name: "commit_and_push",
611
- description:
612
- "Automatically run git add, commit, and push once the ready check passes.",
613
- inputSchema: {
614
- type: "object",
615
- properties: {
616
- commitMessage: {
617
- type: "string",
618
- description: "Commit message to use",
619
- },
620
- branch: {
621
- type: "string",
622
- description:
623
- "Optional branch name to push to (defaults to current branch)",
624
- },
625
- },
626
- required: [],
627
- },
628
- },
629
- {
630
- name: "perform_release",
631
- description:
632
- "Record release details after committing and pushing your changes.",
633
- inputSchema: {
634
- type: "object",
635
- properties: {
636
- command: {
637
- type: "string",
638
- description: "Release command that was executed",
639
- },
640
- notes: {
641
- type: "string",
642
- description: "Optional release notes",
643
- },
644
- },
645
- required: ["command"],
646
- },
647
- },
648
- {
649
- name: "complete_task",
650
- description:
651
- "Mark the task as complete after successful commit & push. Resets workflow for next task.",
652
- inputSchema: {
653
- type: "object",
654
- properties: {
655
- commitMessage: {
656
- type: "string",
657
- description: "The commit message used",
658
- },
659
- },
660
- required: ["commitMessage"],
661
- },
662
- },
663
- {
664
- name: "drop_task",
665
- description:
666
- "Abandon the current task and reset the workflow without completing it.",
667
- inputSchema: {
668
- type: "object",
669
- properties: {
670
- reason: {
671
- type: "string",
672
- description: "Optional explanation for dropping the task",
673
- },
674
- },
675
- },
676
- },
677
- {
678
- name: "get_workflow_status",
679
- description: "Get current workflow status and what needs to be done next.",
680
- inputSchema: {
681
- type: "object",
682
- properties: {},
683
- },
684
- },
685
- {
686
- name: "view_history",
687
- description: "View workflow history of completed tasks.",
688
- inputSchema: {
689
- type: "object",
690
- properties: {
691
- limit: {
692
- type: "number",
693
- description: "Number of recent tasks to show (default: 10)",
694
- },
695
- },
696
- },
697
- },
698
- ],
699
- };
700
- });
55
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
56
+ tools: getToolList(),
57
+ }));
701
58
 
702
59
  // Handle tool calls
703
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
704
- try {
705
- const { name } = request.params;
706
- const { args, error: argumentsError } = normalizeRequestArgs(
707
- request.params.arguments
708
- );
709
-
710
- if (argumentsError) {
711
- return {
712
- content: [
713
- {
714
- type: "text",
715
- text: argumentsError,
716
- },
717
- ],
718
- };
719
- }
720
-
721
- switch (name) {
722
- case "start_task": {
723
- workflowState.reset();
724
- workflowState.state.currentPhase = "coding";
725
- workflowState.state.taskDescription = args.description;
726
- workflowState.state.taskType = args.type;
727
- workflowState.state.testsSkipped = false;
728
- workflowState.state.testsSkippedReason = "";
729
- workflowState.state.commitAndPushCompleted = false;
730
- workflowState.state.lastCommitMessage = "";
731
- workflowState.state.lastPushBranch = "";
732
- await workflowState.save();
733
-
734
- return {
735
- content: [
736
- {
737
- type: "text",
738
- text: `✅ Task Started: ${args.description}\n\n🎯 Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Commit & push, then run 'perform_release'\n8. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`,
739
- },
740
- ],
741
- };
742
- }
743
-
744
- case "mark_bug_fixed": {
745
- if (workflowState.state.currentPhase === "idle") {
746
- return {
747
- content: [
748
- {
749
- type: "text",
750
- text: "⚠️ Please start a task first using 'start_task'!",
751
- },
752
- ],
753
- };
754
- }
755
-
756
- workflowState.state.bugFixed = true;
757
- workflowState.state.testsCreated = false;
758
- workflowState.state.currentPhase = "testing";
759
- workflowState.state.testsSkipped = false;
760
- workflowState.state.testsSkippedReason = "";
761
- workflowState.state.fixSummary = args.summary;
762
- workflowState.state.readyCheckCompleted = false;
763
- workflowState.state.released = false;
764
- workflowState.state.releaseCommand = "";
765
- workflowState.state.releaseNotes = "";
766
- workflowState.state.commitAndPushCompleted = false;
767
- workflowState.state.lastCommitMessage = "";
768
- workflowState.state.lastPushBranch = "";
769
- await workflowState.save();
770
-
771
- return {
772
- content: [
773
- {
774
- type: "text",
775
- text: `✅ Feature/Bug marked as fixed!\n\n⚠️ CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ⏳ Create tests for: ${args.summary}\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`,
776
- },
777
- ],
778
- };
779
- }
780
-
781
- case "create_tests": {
782
- if (!workflowState.state.bugFixed) {
783
- return {
784
- content: [
785
- {
786
- type: "text",
787
- text: "⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
788
- },
789
- ],
790
- };
791
- }
792
-
793
- workflowState.state.testsCreated = true;
794
- workflowState.state.testsSkipped = false;
795
- workflowState.state.testsSkippedReason = "";
796
- workflowState.state.readyCheckCompleted = false;
797
- workflowState.state.released = false;
798
- workflowState.state.commitAndPushCompleted = false;
799
- workflowState.state.lastCommitMessage = "";
800
- workflowState.state.lastPushBranch = "";
801
- await workflowState.save();
802
-
803
- return {
804
- content: [
805
- {
806
- type: "text",
807
- text: "✅ Tests recorded!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'.",
808
- },
809
- ],
810
- };
811
- }
812
-
813
- case "skip_tests": {
814
- if (workflowState.state.currentPhase === "idle") {
815
- return {
816
- content: [
817
- {
818
- type: "text",
819
- text: "⚠️ No active task. Use 'start_task' before skipping tests.",
820
- },
821
- ],
822
- };
823
- }
824
-
825
- if (!workflowState.state.bugFixed) {
826
- return {
827
- content: [
828
- {
829
- type: "text",
830
- text: "⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
831
- },
832
- ],
833
- };
834
- }
835
-
836
- const reason =
837
- typeof args.reason === "string" ? args.reason.trim() : "";
838
-
839
- if (!reason) {
840
- return {
841
- content: [
842
- {
843
- type: "text",
844
- text: "⚠️ Provide a non-empty justification when skipping tests.",
845
- },
846
- ],
847
- };
848
- }
849
-
850
- workflowState.state.testsCreated = true;
851
- workflowState.state.testsPassed = true;
852
- workflowState.state.testsSkipped = true;
853
- workflowState.state.testsSkippedReason = reason;
854
- workflowState.state.currentPhase = "documentation";
855
- workflowState.state.readyToCommit = false;
856
- workflowState.state.readyCheckCompleted = false;
857
- workflowState.state.released = false;
858
- workflowState.state.commitAndPushCompleted = false;
859
- workflowState.state.lastCommitMessage = "";
860
- workflowState.state.lastPushBranch = "";
861
- await workflowState.save();
862
-
863
- return {
864
- content: [
865
- {
866
- type: "text",
867
- text: `⚠️ Tests skipped for this task.
868
- Reason: ${reason}
869
-
870
- Manual verification is now required.
871
-
872
- Next Steps:
873
- 1. ✓ Fix/implement feature
874
- 2. ⚠️ Tests skipped (ensure manual QA)
875
- 3. ⏳ Create/update documentation
876
- 4. ⏳ Run 'check_ready_to_commit'
877
- 5. ⏳ Commit & push
878
- 6. ⏳ Run 'perform_release'
879
- 7. ⏳ Complete task`,
880
- },
881
- ],
882
- };
883
- }
884
-
885
- case "run_tests": {
886
- if (!workflowState.state.bugFixed) {
887
- return {
888
- content: [
889
- {
890
- type: "text",
891
- text: "⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
892
- },
893
- ],
894
- };
895
- }
896
-
897
- if (!workflowState.state.testsCreated) {
898
- return {
899
- content: [
900
- {
901
- type: "text",
902
- text: "⚠️ Please create tests first using 'create_tests' before recording test results!",
903
- },
904
- ],
905
- };
906
- }
907
-
908
- if (
909
- typeof args.passed !== "boolean" ||
910
- typeof args.testCommand !== "string" ||
911
- args.testCommand.trim() === ""
912
- ) {
913
- return {
914
- content: [
915
- {
916
- type: "text",
917
- text: "⚠️ Please provide both 'passed' (boolean) and 'testCommand' (non-empty string) when recording test results.",
918
- },
919
- ],
920
- };
921
- }
922
-
923
- workflowState.state.testsPassed = args.passed;
924
- workflowState.state.testCommand = args.testCommand;
925
- workflowState.state.testDetails =
926
- typeof args.details === "string" ? args.details : "";
927
- workflowState.state.testsSkipped = false;
928
- workflowState.state.testsSkippedReason = "";
929
- workflowState.state.readyCheckCompleted = false;
930
- workflowState.state.released = false;
931
- workflowState.state.commitAndPushCompleted = false;
932
- workflowState.state.lastCommitMessage = "";
933
- workflowState.state.lastPushBranch = "";
934
-
935
- if (!args.passed) {
936
- workflowState.state.currentPhase = "testing";
937
- await workflowState.save();
938
-
939
- return {
940
- content: [
941
- {
942
- type: "text",
943
- text: `❌ TESTS FAILED!\n\n🚫 STOP! DO NOT COMMIT OR PUSH!\n\nYou must:\n1. Fix the failing tests\n2. Run tests again until they pass\n\nTest command: ${args.testCommand}\n\nNever skip or ignore failing tests!`,
944
- },
945
- ],
946
- };
947
- }
948
-
949
- workflowState.state.currentPhase = "documentation";
950
- await workflowState.save();
951
-
952
- return {
953
- content: [
954
- {
955
- type: "text",
956
- text: `✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ⏳ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\nReminder: Document what you did before committing!`,
957
- },
958
- ],
959
- };
960
- }
961
-
962
- case "create_documentation": {
963
- if (!workflowState.state.testsPassed) {
964
- return {
965
- content: [
966
- {
967
- type: "text",
968
- text: "⚠️ Please ensure tests are passing first! Run 'run_tests' with passed=true.",
969
- },
970
- ],
971
- };
972
- }
973
-
974
- workflowState.state.documentationCreated = true;
975
- workflowState.state.documentationType = args.documentationType;
976
- workflowState.state.documentationSummary = args.summary;
977
- workflowState.state.currentPhase = "ready";
978
- workflowState.state.readyToCommit = true;
979
- workflowState.state.readyCheckCompleted = false;
980
- workflowState.state.released = false;
981
- workflowState.state.commitAndPushCompleted = false;
982
- workflowState.state.lastCommitMessage = "";
983
- workflowState.state.lastPushBranch = "";
984
- await workflowState.save();
985
-
986
- return {
987
- content: [
988
- {
989
- type: "text",
990
- text: `✅ Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n\n🎉 You're ready to verify your work!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ✓ Create documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Mark as complete with 'complete_task'\n\nRemember: git add . && git commit && git push`,
991
- },
992
- ],
993
- };
994
- }
995
-
996
- case "check_ready_to_commit": {
997
- const status = workflowState.state;
998
- const testsCreatedDone = status.testsSkipped || status.testsCreated;
999
- const testsPassedDone = status.testsSkipped || status.testsPassed;
1000
- const checks = [
1001
- { name: "Task started", done: status.currentPhase !== "idle" },
1002
- { name: "Feature/bug fixed", done: status.bugFixed },
1003
- {
1004
- name: status.testsSkipped
1005
- ? "Tests skipped (manual QA documented)"
1006
- : "Tests created",
1007
- done: testsCreatedDone,
1008
- },
1009
- {
1010
- name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
1011
- done: testsPassedDone,
1012
- },
1013
- { name: "Documentation created", done: status.documentationCreated },
1014
- ];
1015
-
1016
- const allDone = checks.every((c) => c.done);
1017
- const checkList = checks
1018
- .map((c) => `${c.done ? "✅" : "❌"} ${c.name}`)
1019
- .join("\n");
1020
-
1021
- workflowState.state.readyCheckCompleted = allDone;
1022
- if (allDone) {
1023
- workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
1024
- ? workflowState.state.released
1025
- ? "ready_to_complete"
1026
- : "release"
1027
- : "commit";
1028
- }
1029
- await workflowState.save();
1030
-
1031
- if (allDone) {
1032
- return {
1033
- content: [
1034
- {
1035
- type: "text",
1036
- text: `🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' with your commit message\n2. Run 'perform_release' to record the release\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to push to a non-default branch.\n\nTask: ${status.taskDescription}`,
1037
- },
1038
- ],
1039
- };
1040
- }
1041
-
1042
- return {
1043
- content: [
1044
- {
1045
- type: "text",
1046
- text: `⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`,
1047
- },
1048
- ],
1049
- };
1050
- }
1051
-
1052
- case "commit_and_push": {
1053
- if (!workflowState.state.readyToCommit) {
1054
- return {
1055
- content: [
1056
- {
1057
- type: "text",
1058
- text: "⚠️ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.",
1059
- },
1060
- ],
1061
- };
1062
- }
1063
-
1064
- if (!workflowState.state.readyCheckCompleted) {
1065
- return {
1066
- content: [
1067
- {
1068
- type: "text",
1069
- text: "⚠️ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!",
1070
- },
1071
- ],
1072
- };
1073
- }
1074
-
1075
- const providedCommitMessage =
1076
- typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
1077
-
1078
- if (!(await hasWorkingChanges())) {
1079
- return {
1080
- content: [
1081
- {
1082
- type: "text",
1083
- text: "⚠️ No changes detected. Make sure you have modifications to commit before running 'commit_and_push'.",
1084
- },
1085
- ],
1086
- };
1087
- }
1088
-
1089
- if (!(await hasTestChanges()) && !workflowState.state.testsSkipped) {
1090
- return {
1091
- content: [
1092
- {
1093
- type: "text",
1094
- text: "⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.",
1095
- },
1096
- ],
1097
- };
1098
- }
1099
-
1100
- try {
1101
- await exec("git add .");
1102
- } catch (error) {
1103
- return {
1104
- content: [
1105
- {
1106
- type: "text",
1107
- text: `❌ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`,
1108
- },
1109
- ],
1110
- };
1111
- }
1112
-
1113
- const stagedChanges = await getStagedChanges();
1114
- const { summary: generatedSummary, body: commitBody } =
1115
- createCommitMessageParts(stagedChanges, providedCommitMessage);
1116
-
1117
- if (!generatedSummary) {
1118
- return {
1119
- content: [
1120
- {
1121
- type: "text",
1122
- text: "⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.",
1123
- },
1124
- ],
1125
- };
1126
- }
1127
-
1128
- try {
1129
- if (commitBody) {
1130
- await exec(
1131
- `git commit -m ${shellEscape(generatedSummary)} -m ${shellEscape(commitBody)}`
1132
- );
1133
- } else {
1134
- await exec(`git commit -m ${shellEscape(generatedSummary)}`);
1135
- }
1136
- } catch (error) {
1137
- const output = error.stderr || error.stdout || error.message || "Unknown error";
1138
- if (output.includes("nothing to commit")) {
1139
- return {
1140
- content: [
1141
- {
1142
- type: "text",
1143
- text: "⚠️ Nothing to commit. Make additional changes before running 'commit_and_push'.",
1144
- },
1145
- ],
1146
- };
1147
- }
1148
-
1149
- return {
1150
- content: [
1151
- {
1152
- type: "text",
1153
- text: `❌ git commit failed:\n\n${output}`,
1154
- },
1155
- ],
1156
- };
1157
- }
1158
-
1159
- const requestedBranch =
1160
- typeof args.branch === "string" ? args.branch.trim() : "";
1161
- const currentBranch = await getCurrentBranch();
1162
- const pushCommand = requestedBranch
1163
- ? `git push origin ${shellEscape(requestedBranch)}`
1164
- : "git push";
1165
-
1166
- try {
1167
- await exec(pushCommand);
1168
- } catch (error) {
1169
- return {
1170
- content: [
1171
- {
1172
- type: "text",
1173
- text: `❌ git push failed:\n\n${error.stderr || error.stdout || error.message}`,
1174
- },
1175
- ],
1176
- };
1177
- }
1178
-
1179
- workflowState.state.commitAndPushCompleted = true;
1180
- workflowState.state.lastCommitMessage = generatedSummary;
1181
- workflowState.state.lastPushBranch =
1182
- requestedBranch || currentBranch || "";
1183
- workflowState.state.currentPhase = workflowState.state.released
1184
- ? "ready_to_complete"
1185
- : "release";
1186
- await workflowState.save();
1187
-
1188
- return {
1189
- content: [
1190
- {
1191
- type: "text",
1192
- text: `✅ Commit & push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${
1193
- requestedBranch || currentBranch || "(default upstream)"
1194
- }\n\nNext: Run 'perform_release' to record the release.`,
1195
- },
1196
- ],
1197
- };
1198
- }
1199
-
1200
- case "perform_release": {
1201
- if (workflowState.state.currentPhase === "idle") {
1202
- return {
1203
- content: [
1204
- {
1205
- type: "text",
1206
- text: "⚠️ Please start a task first using 'start_task'!",
1207
- },
1208
- ],
1209
- };
1210
- }
1211
-
1212
- if (!workflowState.state.commitAndPushCompleted) {
1213
- return {
1214
- content: [
1215
- {
1216
- type: "text",
1217
- text: "⚠️ Please run 'commit_and_push' after the ready check before recording a release!",
1218
- },
1219
- ],
1220
- };
1221
- }
1222
-
1223
- if (!workflowState.state.readyCheckCompleted) {
1224
- return {
1225
- content: [
1226
- {
1227
- type: "text",
1228
- text: "⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!",
1229
- },
1230
- ],
1231
- };
1232
- }
1233
-
1234
- const releaseCommand =
1235
- typeof args.command === "string" ? args.command.trim() : "";
1236
-
1237
- if (!releaseCommand) {
1238
- return {
1239
- content: [
1240
- {
1241
- type: "text",
1242
- text: "⚠️ Please provide a non-empty 'command' when using 'perform_release'.",
1243
- },
1244
- ],
1245
- };
1246
- }
1247
-
1248
- if (await hasWorkingChanges()) {
1249
- return {
1250
- content: [
1251
- {
1252
- type: "text",
1253
- text: "⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.",
1254
- },
1255
- ],
1256
- };
1257
- }
1258
-
1259
- const lastCommitMessage = await getLastCommitMessage();
1260
- const releaseType = determineReleaseTypeFromCommit(
1261
- lastCommitMessage || workflowState.state.lastCommitMessage
1262
- );
1263
-
1264
- const finalCommand = releaseCommand.includes("--release-as")
1265
- ? releaseCommand
1266
- : `${releaseCommand} -- --release-as ${releaseType}`;
1267
-
1268
- try {
1269
- await exec(finalCommand);
1270
- } catch (error) {
1271
- return {
1272
- content: [
1273
- {
1274
- type: "text",
1275
- text: `❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`,
1276
- },
1277
- ],
1278
- };
1279
- }
1280
-
1281
- workflowState.state.released = true;
1282
- workflowState.state.releaseCommand = finalCommand;
1283
- workflowState.state.releaseNotes =
1284
- typeof args.notes === "string" ? args.notes : "";
1285
- workflowState.state.currentPhase = "ready_to_complete";
1286
- await workflowState.save();
1287
-
1288
- const limit = args.limit || 10;
1289
- const history = workflowState.state.history.slice(-limit).reverse();
1290
-
1291
- if (history.length === 0) {
1292
- return {
1293
- content: [
1294
- {
1295
- type: "text",
1296
- text: "📜 No workflow history yet.",
1297
- },
1298
- ],
1299
- };
1300
- }
1301
-
1302
- const historyText = history
1303
- .map(
1304
- (entry, index) =>
1305
- `${index + 1}. [${new Date(entry.timestamp).toLocaleString()}]\n Task: ${entry.taskDescription}\n Type: ${entry.taskType}\n Commit: ${entry.commitMessage}\n`
1306
- )
1307
- .join("\n");
1308
-
1309
- return {
1310
- content: [
1311
- {
1312
- type: "text",
1313
- text: `📜 Workflow History (last ${history.length} tasks)\n\n${historyText}`,
1314
- },
1315
- ],
1316
- };
1317
- }
1318
-
1319
- default:
1320
- throw new Error(`Unknown tool: ${name}`);
1321
- }
1322
- } catch (error) {
1323
- console.error("Tool handler error:", error);
1324
- return {
1325
- content: [
1326
- {
1327
- type: "text",
1328
- text: "⚠️ Internal server error while processing the tool request. Please try again.",
1329
- },
1330
- ],
1331
- };
1332
- }
1333
- });
60
+ server.setRequestHandler(CallToolRequestSchema, (request) =>
61
+ handleToolCall({
62
+ request,
63
+ normalizeRequestArgs,
64
+ workflowState,
65
+ exec,
66
+ git: {
67
+ hasWorkingChanges,
68
+ hasTestChanges,
69
+ hasStagedChanges,
70
+ getStagedChanges,
71
+ getCurrentBranch,
72
+ getLastCommitMessage,
73
+ workingTreeSummary,
74
+ },
75
+ utils: { shellEscape, determineReleaseTypeFromCommit, createCommitMessageParts },
76
+ })
77
+ );
1334
78
 
1335
79
  // Helper function
1336
80
  export function getNextStep(status) {
@@ -1395,7 +139,8 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1395
139
 
1396
140
  6. 🚀 COMMIT & PUSH
1397
141
  - Use 'check_ready_to_commit' to verify
1398
- - git add . && git commit && git push
142
+ - Use 'commit_and_push' (stages, commits, and pushes)
143
+ - Then use 'perform_release' to handle versioning and tags
1399
144
  - Use 'complete_task' to finish
1400
145
 
1401
146
  Remember: No shortcuts! Each step is important for code quality.`,