@programinglive/dev-workflow-mcp-server 1.0.1 โ†’ 1.0.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.4](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.3...v1.0.4) (2025-10-20)
6
+
7
+
8
+ ### โœจ Features
9
+
10
+ * enforce release step after ready check ([0725496](https://github.com/programinglive/dev-workflow-mcp-server/commit/07254967becd515cb2a4261d62af01dfeb842175))
11
+
12
+
13
+ ### ๐Ÿ› Bug Fixes
14
+
15
+ * support windows-friendly commit quoting ([47881fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/47881fd0db54da3fabe368b1a135902e47ef873e))
16
+
17
+ ### [1.0.3](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.2...v1.0.3) (2025-10-20)
18
+
19
+
20
+ ### ๐Ÿงน Chores
21
+
22
+ * add gitignore ([5f695fb](https://github.com/programinglive/dev-workflow-mcp-server/commit/5f695fbc71515c7f1ffbc44fd74b52314c966ee6))
23
+
24
+
25
+ ### ๐Ÿ“ Documentation
26
+
27
+ * clarify cross-platform installation ([52b5ee7](https://github.com/programinglive/dev-workflow-mcp-server/commit/52b5ee7061e8f968ff94e6c226586fc1b7e2a057))
28
+
29
+ ### [1.0.2](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.1...v1.0.2) (2025-10-19)
30
+
31
+
32
+ ### ๐Ÿ› Bug Fixes
33
+
34
+ * guard run_tests arguments ([2004c59](https://github.com/programinglive/dev-workflow-mcp-server/commit/2004c590fabe359d068122cf240a731382f81ca0))
35
+ * normalize tool call arguments ([44d5a3d](https://github.com/programinglive/dev-workflow-mcp-server/commit/44d5a3d35f191012445fa02cee6254afbc993c98))
36
+
37
+
38
+ ### ๐Ÿ“ Documentation
39
+
40
+ * clarify tool argument requirements ([4528614](https://github.com/programinglive/dev-workflow-mcp-server/commit/45286146661ad850eccb04849c11819e91ddd580))
41
+
5
42
  ### [1.0.1](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.0...v1.0.1) (2025-10-18)
6
43
 
7
44
 
package/README.md CHANGED
@@ -11,7 +11,8 @@ This MCP server guides you through a disciplined development workflow:
11
11
  3. **Create Tests** - Always test your changes
12
12
  4. **Run Tests** - Tests must pass (GREEN)
13
13
  5. **Document** - Update documentation
14
- 6. **Commit & Push** - Only when all steps are complete
14
+ 6. **Commit & Push** - Let the server drive the git commands once verification passes
15
+ 7. **Release** - Record the release details before closing out the task
15
16
 
16
17
  ## ๐Ÿš€ Installation
17
18
 
@@ -23,40 +24,115 @@ npm install
23
24
 
24
25
  ### 2. Configure in Windsurf/Claude Desktop
25
26
 
26
- Add to your MCP settings file:
27
+ Point your MCP client to the server entry point. Replace `<PROJECT_ROOT>` with the absolute path to this repository on your machine.
27
28
 
28
- **For Windsurf** (`~/Library/Application Support/Windsurf/config.json`):
29
+ #### macOS
30
+
31
+ - **Windsurf** (`~/Library/Application Support/Windsurf/config.json`):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "dev-workflow": {
37
+ "command": "node",
38
+ "args": ["<PROJECT_ROOT>/index.js"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ - **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "dev-workflow": {
50
+ "command": "node",
51
+ "args": ["<PROJECT_ROOT>/index.js"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ #### Windows
58
+
59
+ - **Windsurf** (`%APPDATA%\Windsurf\config.json`):
29
60
 
30
61
  ```json
31
62
  {
32
63
  "mcpServers": {
33
64
  "dev-workflow": {
34
65
  "command": "node",
35
- "args": ["/Users/mahardhika/code/project/mine/app/CascadeProjects/windsurf-project/index.js"]
66
+ "args": ["<PROJECT_ROOT>\\index.js"]
36
67
  }
37
68
  }
38
69
  }
39
70
  ```
40
71
 
41
- **For Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
72
+ - **Claude Desktop** (`%APPDATA%\Claude\claude_desktop_config.json`):
42
73
 
43
74
  ```json
44
75
  {
45
76
  "mcpServers": {
46
77
  "dev-workflow": {
47
78
  "command": "node",
48
- "args": ["/Users/mahardhika/code/project/mine/app/CascadeProjects/windsurf-project/index.js"]
79
+ "args": ["<PROJECT_ROOT>\\index.js"]
49
80
  }
50
81
  }
51
82
  }
52
83
  ```
53
84
 
85
+ #### Linux
86
+
87
+ - **Windsurf** (`~/.config/windsurf/config.json`):
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "dev-workflow": {
93
+ "command": "node",
94
+ "args": ["<PROJECT_ROOT>/index.js"]
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ - **Claude Desktop** (`~/.config/claude/claude_desktop_config.json`):
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "dev-workflow": {
106
+ "command": "node",
107
+ "args": ["<PROJECT_ROOT>/index.js"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ > **Note:** On Windows paths in JSON require escaped backslashes (e.g., `"C:\\path\\to\\project"`).
114
+
54
115
  ### 3. Restart Windsurf/Claude Desktop
55
116
 
56
117
  After adding the configuration, restart the application to load the MCP server.
57
118
 
58
119
  ## ๐Ÿ› ๏ธ Available Tools
59
120
 
121
+ ### Tool Argument Requirements
122
+
123
+ All tool invocations validate their `arguments` payload before running:
124
+ - Strings are parsed as JSON and must resolve to an object (key/value pairs).
125
+ - Passing non-object data (numbers, arrays, plain text) triggers a guidance error.
126
+ - Missing or malformed arguments safely default to empty input so the tool can respond with actionable reminders.
127
+
128
+ Example (stringified JSON object):
129
+ ```
130
+ {
131
+ "name": "start_task",
132
+ "arguments": "{\"description\":\"Add reporting endpoint\",\"type\":\"feature\"}"
133
+ }
134
+ ```
135
+
60
136
  ### `start_task`
61
137
  Start a new coding task. This is your first step - be conscious about what you're coding.
62
138
 
@@ -77,6 +153,11 @@ Mark that the bug/feature is fixed. **Reminder: Now you MUST create tests!**
77
153
  **Parameters:**
78
154
  - `summary` (string, required): Brief summary of what was fixed/implemented
79
155
 
156
+ ### `create_tests`
157
+ Confirm that you've created the necessary tests covering your change. Required before recording test results.
158
+
159
+ **Parameters:** _none_
160
+
80
161
  ### `run_tests`
81
162
  Record test results. **NEVER commit if tests fail!** Only proceed if all tests are green.
82
163
 
@@ -103,6 +184,20 @@ Mark that documentation has been created/updated. This is required before commit
103
184
  ### `check_ready_to_commit`
104
185
  Check if all workflow steps are completed and you're ready to commit & push.
105
186
 
187
+ ### `commit_and_push`
188
+ Automatically run `git add`, `git commit`, and `git push` after the ready check passes.
189
+
190
+ **Parameters:**
191
+ - `commitMessage` (string, required): Conventional commit message to use
192
+ - `branch` (string, optional): Target branch to push (defaults to current branch)
193
+
194
+ ### `perform_release`
195
+ Record the release after you've committed and pushed. Required before you can complete the task.
196
+
197
+ **Parameters:**
198
+ - `command` (string, required): Release command that was executed (e.g., `npm run release`)
199
+ - `notes` (string, optional): Additional release notes
200
+
106
201
  ### `complete_task`
107
202
  Mark the task as complete after successful commit & push. Resets workflow for next task.
108
203
 
@@ -165,13 +260,16 @@ Here's how you'd use this MCP server in a typical coding session:
165
260
  ```
166
261
 
167
262
  8. **Commit & Push:**
168
- ```bash
169
- git add .
170
- git commit -m "feat: add user profile page with tests and docs"
171
- git push
263
+ ```
264
+ "Commit and push: commitMessage='feat: add user profile page with tests and docs'"
265
+ ```
266
+
267
+ 9. **Record release:**
268
+ ```
269
+ "Record release: command='npm run release', notes='v1.2.3'"
172
270
  ```
173
271
 
174
- 9. **Complete:**
272
+ 10. **Complete:**
175
273
  ```
176
274
  "Complete the task with commit message: 'feat: add user profile page'"
177
275
  ```
package/index.js CHANGED
@@ -25,9 +25,17 @@ export class WorkflowState {
25
25
  currentPhase: "idle",
26
26
  taskDescription: "",
27
27
  bugFixed: false,
28
+ testsCreated: false,
28
29
  testsPassed: false,
29
30
  documentationCreated: false,
30
31
  readyToCommit: false,
32
+ readyCheckCompleted: false,
33
+ released: false,
34
+ releaseCommand: "",
35
+ releaseNotes: "",
36
+ commitAndPushCompleted: false,
37
+ lastCommitMessage: "",
38
+ lastPushBranch: "",
31
39
  history: [],
32
40
  };
33
41
  }
@@ -35,7 +43,35 @@ export class WorkflowState {
35
43
  async load() {
36
44
  try {
37
45
  const data = await fs.readFile(this.stateFile, "utf-8");
38
- this.state = JSON.parse(data);
46
+ const parsed = JSON.parse(data);
47
+ this.state = {
48
+ ...this.state,
49
+ ...parsed,
50
+ };
51
+ if (typeof this.state.testsCreated !== "boolean") {
52
+ this.state.testsCreated = false;
53
+ }
54
+ if (typeof this.state.readyCheckCompleted !== "boolean") {
55
+ this.state.readyCheckCompleted = false;
56
+ }
57
+ if (typeof this.state.released !== "boolean") {
58
+ this.state.released = false;
59
+ }
60
+ if (typeof this.state.commitAndPushCompleted !== "boolean") {
61
+ this.state.commitAndPushCompleted = false;
62
+ }
63
+ if (typeof this.state.releaseCommand !== "string") {
64
+ this.state.releaseCommand = "";
65
+ }
66
+ if (typeof this.state.releaseNotes !== "string") {
67
+ this.state.releaseNotes = "";
68
+ }
69
+ if (typeof this.state.lastCommitMessage !== "string") {
70
+ this.state.lastCommitMessage = "";
71
+ }
72
+ if (typeof this.state.lastPushBranch !== "string") {
73
+ this.state.lastPushBranch = "";
74
+ }
39
75
  } catch (error) {
40
76
  // File doesn't exist yet, use default state
41
77
  }
@@ -50,9 +86,17 @@ export class WorkflowState {
50
86
  currentPhase: "idle",
51
87
  taskDescription: "",
52
88
  bugFixed: false,
89
+ testsCreated: false,
53
90
  testsPassed: false,
54
91
  documentationCreated: false,
55
92
  readyToCommit: false,
93
+ readyCheckCompleted: false,
94
+ released: false,
95
+ releaseCommand: "",
96
+ releaseNotes: "",
97
+ commitAndPushCompleted: false,
98
+ lastCommitMessage: "",
99
+ lastPushBranch: "",
56
100
  history: this.state.history,
57
101
  };
58
102
  }
@@ -70,6 +114,54 @@ await workflowState.load();
70
114
 
71
115
  const exec = promisify(execCallback);
72
116
 
117
+ const isWindows = process.platform === "win32";
118
+
119
+ function shellEscape(value) {
120
+ const stringValue = String(value);
121
+ if (isWindows) {
122
+ // Use PowerShell-friendly single quotes, escape embedded single quotes by doubling
123
+ return `'${stringValue.replace(/'/g, "''")}'`;
124
+ }
125
+
126
+ return `'${stringValue.replace(/'/g, "'\\''")}'`;
127
+ }
128
+
129
+ function normalizeRequestArgs(rawArgs) {
130
+ if (rawArgs === undefined || rawArgs === null) {
131
+ return { args: {}, error: null };
132
+ }
133
+
134
+ if (typeof rawArgs === "string") {
135
+ try {
136
+ const parsed = JSON.parse(rawArgs);
137
+ if (parsed && typeof parsed === "object") {
138
+ return { args: parsed, error: null };
139
+ }
140
+
141
+ return {
142
+ args: {},
143
+ error:
144
+ "โš ๏ธ Tool arguments must be a JSON object. Please provide key/value pairs.",
145
+ };
146
+ } catch (parseError) {
147
+ return {
148
+ args: {},
149
+ error:
150
+ "โš ๏ธ Unable to parse tool arguments. Please provide valid JSON-formatted data.",
151
+ };
152
+ }
153
+ }
154
+
155
+ if (typeof rawArgs === "object") {
156
+ return { args: rawArgs, error: null };
157
+ }
158
+
159
+ return {
160
+ args: {},
161
+ error: "โš ๏ธ Unsupported tool arguments format. Expected a JSON object.",
162
+ };
163
+ }
164
+
73
165
  async function hasStagedChanges() {
74
166
  try {
75
167
  const { stdout } = await exec("git status --porcelain");
@@ -85,6 +177,24 @@ async function hasStagedChanges() {
85
177
  }
86
178
  }
87
179
 
180
+ async function hasWorkingChanges() {
181
+ try {
182
+ const { stdout } = await exec("git status --porcelain");
183
+ return stdout.split("\n").some((line) => line && line.trim().length > 0);
184
+ } catch (error) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ async function getCurrentBranch() {
190
+ try {
191
+ const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
192
+ return stdout.trim();
193
+ } catch (error) {
194
+ return "";
195
+ }
196
+ }
197
+
88
198
  // Create MCP server
89
199
  export const server = new Server(
90
200
  {
@@ -138,6 +248,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
138
248
  required: ["summary"],
139
249
  },
140
250
  },
251
+ {
252
+ name: "create_tests",
253
+ description: "Mark that tests covering the change have been created.",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {},
257
+ },
258
+ },
141
259
  {
142
260
  name: "run_tests",
143
261
  description:
@@ -190,6 +308,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
190
308
  properties: {},
191
309
  },
192
310
  },
311
+ {
312
+ name: "commit_and_push",
313
+ description:
314
+ "Automatically run git add, commit, and push once the ready check passes.",
315
+ inputSchema: {
316
+ type: "object",
317
+ properties: {
318
+ commitMessage: {
319
+ type: "string",
320
+ description: "Commit message to use",
321
+ },
322
+ branch: {
323
+ type: "string",
324
+ description:
325
+ "Optional branch name to push to (defaults to current branch)",
326
+ },
327
+ },
328
+ required: ["commitMessage"],
329
+ },
330
+ },
331
+ {
332
+ name: "perform_release",
333
+ description:
334
+ "Record release details after committing and pushing your changes.",
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ command: {
339
+ type: "string",
340
+ description: "Release command that was executed",
341
+ },
342
+ notes: {
343
+ type: "string",
344
+ description: "Optional release notes",
345
+ },
346
+ },
347
+ required: ["command"],
348
+ },
349
+ },
193
350
  {
194
351
  name: "complete_task",
195
352
  description:
@@ -232,95 +389,184 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
232
389
 
233
390
  // Handle tool calls
234
391
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
235
- const { name, arguments: args } = request.params;
236
-
237
- switch (name) {
238
- case "start_task": {
239
- workflowState.reset();
240
- workflowState.state.currentPhase = "coding";
241
- workflowState.state.taskDescription = args.description;
242
- workflowState.state.taskType = args.type;
243
- await workflowState.save();
392
+ try {
393
+ const { name } = request.params;
394
+ const { args, error: argumentsError } = normalizeRequestArgs(
395
+ request.params.arguments
396
+ );
244
397
 
398
+ if (argumentsError) {
245
399
  return {
246
400
  content: [
247
401
  {
248
402
  type: "text",
249
- 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. โณ Commit & push\n\nReminder: Focus on writing clean, maintainable code!`,
403
+ text: argumentsError,
250
404
  },
251
405
  ],
252
406
  };
253
407
  }
254
408
 
255
- case "mark_bug_fixed": {
256
- if (workflowState.state.currentPhase === "idle") {
409
+ switch (name) {
410
+ case "start_task": {
411
+ workflowState.reset();
412
+ workflowState.state.currentPhase = "coding";
413
+ workflowState.state.taskDescription = args.description;
414
+ workflowState.state.taskType = args.type;
415
+ workflowState.state.commitAndPushCompleted = false;
416
+ workflowState.state.lastCommitMessage = "";
417
+ workflowState.state.lastPushBranch = "";
418
+ await workflowState.save();
419
+
257
420
  return {
258
421
  content: [
259
422
  {
260
423
  type: "text",
261
- text: "โš ๏ธ Please start a task first using 'start_task'!",
424
+ 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!`,
262
425
  },
263
426
  ],
264
427
  };
265
428
  }
266
429
 
267
- workflowState.state.bugFixed = true;
268
- workflowState.state.currentPhase = "testing";
269
- workflowState.state.fixSummary = args.summary;
270
- await workflowState.save();
430
+ case "mark_bug_fixed": {
431
+ if (workflowState.state.currentPhase === "idle") {
432
+ return {
433
+ content: [
434
+ {
435
+ type: "text",
436
+ text: "โš ๏ธ Please start a task first using 'start_task'!",
437
+ },
438
+ ],
439
+ };
440
+ }
271
441
 
272
- return {
273
- content: [
274
- {
275
- type: "text",
276
- 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. โณ Commit & push\n\n๐Ÿšซ DO NOT SKIP TESTING!`,
277
- },
278
- ],
279
- };
280
- }
442
+ workflowState.state.bugFixed = true;
443
+ workflowState.state.testsCreated = false;
444
+ workflowState.state.currentPhase = "testing";
445
+ workflowState.state.fixSummary = args.summary;
446
+ workflowState.state.readyCheckCompleted = false;
447
+ workflowState.state.released = false;
448
+ workflowState.state.releaseCommand = "";
449
+ workflowState.state.releaseNotes = "";
450
+ workflowState.state.readyToCommit = false;
451
+ workflowState.state.commitAndPushCompleted = false;
452
+ workflowState.state.lastCommitMessage = "";
453
+ workflowState.state.lastPushBranch = "";
454
+ await workflowState.save();
281
455
 
282
- case "run_tests": {
283
- if (!workflowState.state.bugFixed) {
284
456
  return {
285
457
  content: [
286
458
  {
287
459
  type: "text",
288
- text: "โš ๏ธ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
460
+ 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!`,
289
461
  },
290
462
  ],
291
463
  };
292
464
  }
293
465
 
294
- workflowState.state.testsPassed = args.passed;
295
- workflowState.state.testCommand = args.testCommand;
296
- workflowState.state.testDetails = args.details || "";
466
+ case "create_tests": {
467
+ if (!workflowState.state.bugFixed) {
468
+ return {
469
+ content: [
470
+ {
471
+ type: "text",
472
+ text: "โš ๏ธ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
473
+ },
474
+ ],
475
+ };
476
+ }
297
477
 
298
- if (!args.passed) {
299
- workflowState.state.currentPhase = "testing";
478
+ workflowState.state.testsCreated = true;
479
+ workflowState.state.readyCheckCompleted = false;
480
+ workflowState.state.released = false;
481
+ workflowState.state.commitAndPushCompleted = false;
482
+ workflowState.state.lastCommitMessage = "";
483
+ workflowState.state.lastPushBranch = "";
300
484
  await workflowState.save();
301
485
 
302
486
  return {
303
487
  content: [
304
488
  {
305
489
  type: "text",
306
- 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!`,
490
+ 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'.",
307
491
  },
308
492
  ],
309
493
  };
310
494
  }
311
495
 
312
- workflowState.state.currentPhase = "documentation";
313
- await workflowState.save();
496
+ case "run_tests": {
497
+ if (!workflowState.state.bugFixed) {
498
+ return {
499
+ content: [
500
+ {
501
+ type: "text",
502
+ text: "โš ๏ธ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
503
+ },
504
+ ],
505
+ };
506
+ }
314
507
 
315
- return {
316
- content: [
317
- {
318
- type: "text",
319
- 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. โณ Commit & push\n\nReminder: Document what you did before committing!`,
320
- },
321
- ],
322
- };
323
- }
508
+ if (!workflowState.state.testsCreated) {
509
+ return {
510
+ content: [
511
+ {
512
+ type: "text",
513
+ text: "โš ๏ธ Please create tests first using 'create_tests' before recording test results!",
514
+ },
515
+ ],
516
+ };
517
+ }
518
+
519
+ if (
520
+ typeof args.passed !== "boolean" ||
521
+ typeof args.testCommand !== "string" ||
522
+ args.testCommand.trim() === ""
523
+ ) {
524
+ return {
525
+ content: [
526
+ {
527
+ type: "text",
528
+ text: "โš ๏ธ Please provide both 'passed' (boolean) and 'testCommand' (non-empty string) when recording test results.",
529
+ },
530
+ ],
531
+ };
532
+ }
533
+
534
+ workflowState.state.testsPassed = args.passed;
535
+ workflowState.state.testCommand = args.testCommand;
536
+ workflowState.state.testDetails =
537
+ typeof args.details === "string" ? args.details : "";
538
+ workflowState.state.readyCheckCompleted = false;
539
+ workflowState.state.released = false;
540
+ workflowState.state.commitAndPushCompleted = false;
541
+ workflowState.state.lastCommitMessage = "";
542
+ workflowState.state.lastPushBranch = "";
543
+
544
+ if (!args.passed) {
545
+ workflowState.state.currentPhase = "testing";
546
+ await workflowState.save();
547
+
548
+ return {
549
+ content: [
550
+ {
551
+ type: "text",
552
+ 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!`,
553
+ },
554
+ ],
555
+ };
556
+ }
557
+
558
+ workflowState.state.currentPhase = "documentation";
559
+ await workflowState.save();
560
+
561
+ return {
562
+ content: [
563
+ {
564
+ type: "text",
565
+ 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!`,
566
+ },
567
+ ],
568
+ };
569
+ }
324
570
 
325
571
  case "create_documentation": {
326
572
  if (!workflowState.state.testsPassed) {
@@ -339,13 +585,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
339
585
  workflowState.state.documentationSummary = args.summary;
340
586
  workflowState.state.currentPhase = "ready";
341
587
  workflowState.state.readyToCommit = true;
588
+ workflowState.state.readyCheckCompleted = false;
589
+ workflowState.state.released = false;
590
+ workflowState.state.commitAndPushCompleted = false;
591
+ workflowState.state.lastCommitMessage = "";
592
+ workflowState.state.lastPushBranch = "";
342
593
  await workflowState.save();
343
594
 
344
595
  return {
345
596
  content: [
346
597
  {
347
598
  type: "text",
348
- text: `โœ… Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n\n๐ŸŽ‰ You're ready to commit!\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\n7. โณ Mark as complete with 'complete_task'\n\nRemember: git add . && git commit && git push`,
599
+ 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`,
349
600
  },
350
601
  ],
351
602
  };
@@ -356,6 +607,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
356
607
  const checks = [
357
608
  { name: "Task started", done: status.currentPhase !== "idle" },
358
609
  { name: "Feature/bug fixed", done: status.bugFixed },
610
+ { name: "Tests created", done: status.testsCreated },
359
611
  { name: "Tests created and passed", done: status.testsPassed },
360
612
  { name: "Documentation created", done: status.documentationCreated },
361
613
  ];
@@ -365,12 +617,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
365
617
  .map((c) => `${c.done ? "โœ…" : "โŒ"} ${c.name}`)
366
618
  .join("\n");
367
619
 
620
+ workflowState.state.readyCheckCompleted = allDone;
621
+ if (allDone) {
622
+ workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
623
+ ? workflowState.state.released
624
+ ? "ready_to_complete"
625
+ : "release"
626
+ : "commit";
627
+ }
628
+ await workflowState.save();
629
+
368
630
  if (allDone) {
369
631
  return {
370
632
  content: [
371
633
  {
372
634
  type: "text",
373
- text: `๐ŸŽ‰ ALL CHECKS PASSED! You're ready to commit!\n\n${checkList}\n\nโœ… You can now:\n1. git add .\n2. git commit -m "your message"\n3. git push\n4. Run 'complete_task' to finish\n\nTask: ${status.taskDescription}`,
635
+ 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}`,
374
636
  },
375
637
  ],
376
638
  };
@@ -386,6 +648,182 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
386
648
  }
387
649
  }
388
650
 
651
+ case "commit_and_push": {
652
+ if (!workflowState.state.readyToCommit) {
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text",
657
+ text: "โš ๏ธ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.",
658
+ },
659
+ ],
660
+ };
661
+ }
662
+
663
+ if (!workflowState.state.readyCheckCompleted) {
664
+ return {
665
+ content: [
666
+ {
667
+ type: "text",
668
+ text: "โš ๏ธ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!",
669
+ },
670
+ ],
671
+ };
672
+ }
673
+
674
+ const commitMessage =
675
+ typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
676
+
677
+ if (!commitMessage) {
678
+ return {
679
+ content: [
680
+ {
681
+ type: "text",
682
+ text: "โš ๏ธ Please provide a non-empty 'commitMessage' when using 'commit_and_push'.",
683
+ },
684
+ ],
685
+ };
686
+ }
687
+
688
+ if (!(await hasWorkingChanges())) {
689
+ return {
690
+ content: [
691
+ {
692
+ type: "text",
693
+ text: "โš ๏ธ No changes detected. Make sure you have modifications to commit before running 'commit_and_push'.",
694
+ },
695
+ ],
696
+ };
697
+ }
698
+
699
+ try {
700
+ await exec("git add .");
701
+ } catch (error) {
702
+ return {
703
+ content: [
704
+ {
705
+ type: "text",
706
+ text: `โŒ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`,
707
+ },
708
+ ],
709
+ };
710
+ }
711
+
712
+ try {
713
+ await exec(`git commit -m ${shellEscape(commitMessage)}`);
714
+ } catch (error) {
715
+ const output = error.stderr || error.stdout || error.message || "Unknown error";
716
+ if (output.includes("nothing to commit")) {
717
+ return {
718
+ content: [
719
+ {
720
+ type: "text",
721
+ text: "โš ๏ธ Nothing to commit. Make additional changes before running 'commit_and_push'.",
722
+ },
723
+ ],
724
+ };
725
+ }
726
+
727
+ return {
728
+ content: [
729
+ {
730
+ type: "text",
731
+ text: `โŒ git commit failed:\n\n${output}`,
732
+ },
733
+ ],
734
+ };
735
+ }
736
+
737
+ const requestedBranch =
738
+ typeof args.branch === "string" ? args.branch.trim() : "";
739
+ const currentBranch = await getCurrentBranch();
740
+ const pushBranch = requestedBranch || currentBranch;
741
+ const pushCommand = pushBranch
742
+ ? `git push origin ${shellEscape(pushBranch)}`
743
+ : "git push";
744
+
745
+ try {
746
+ await exec(pushCommand);
747
+ } catch (error) {
748
+ return {
749
+ content: [
750
+ {
751
+ type: "text",
752
+ text: `โŒ git push failed:\n\n${error.stderr || error.stdout || error.message}`,
753
+ },
754
+ ],
755
+ };
756
+ }
757
+
758
+ workflowState.state.commitAndPushCompleted = true;
759
+ workflowState.state.lastCommitMessage = commitMessage;
760
+ workflowState.state.lastPushBranch = pushBranch || currentBranch || "";
761
+ workflowState.state.currentPhase = workflowState.state.released
762
+ ? "ready_to_complete"
763
+ : "release";
764
+ await workflowState.save();
765
+
766
+ return {
767
+ content: [
768
+ {
769
+ type: "text",
770
+ text: `โœ… Commit & push completed!\n\nCommit message: ${commitMessage}\nPushed to: ${pushBranch || currentBranch || "(default upstream)"}\n\nNext: Run 'perform_release' to record the release.`,
771
+ },
772
+ ],
773
+ };
774
+ }
775
+
776
+ case "perform_release": {
777
+ if (workflowState.state.currentPhase === "idle") {
778
+ return {
779
+ content: [
780
+ {
781
+ type: "text",
782
+ text: "โš ๏ธ Please start a task first using 'start_task'!",
783
+ },
784
+ ],
785
+ };
786
+ }
787
+
788
+ if (!workflowState.state.commitAndPushCompleted) {
789
+ return {
790
+ content: [
791
+ {
792
+ type: "text",
793
+ text: "โš ๏ธ Please run 'commit_and_push' after the ready check before recording a release!",
794
+ },
795
+ ],
796
+ };
797
+ }
798
+
799
+ if (!workflowState.state.readyCheckCompleted) {
800
+ return {
801
+ content: [
802
+ {
803
+ type: "text",
804
+ text: "โš ๏ธ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!",
805
+ },
806
+ ],
807
+ };
808
+ }
809
+
810
+ workflowState.state.released = true;
811
+ workflowState.state.releaseCommand = args.command;
812
+ workflowState.state.releaseNotes =
813
+ typeof args.notes === "string" ? args.notes : "";
814
+ workflowState.state.currentPhase = "ready_to_complete";
815
+ await workflowState.save();
816
+
817
+ return {
818
+ content: [
819
+ {
820
+ type: "text",
821
+ text: `๐Ÿš€ Release recorded!\n\nCommand: ${args.command}\n${workflowState.state.releaseNotes ? `Notes: ${workflowState.state.releaseNotes}\n` : ""}\nโœ… Next: Run 'complete_task' to wrap up.`,
822
+ },
823
+ ],
824
+ };
825
+ }
826
+
389
827
  case "complete_task": {
390
828
  if (!workflowState.state.readyToCommit) {
391
829
  return {
@@ -398,6 +836,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
398
836
  };
399
837
  }
400
838
 
839
+ if (!workflowState.state.readyCheckCompleted) {
840
+ return {
841
+ content: [
842
+ {
843
+ type: "text",
844
+ text: "โš ๏ธ Please run 'check_ready_to_commit' and ensure all checks pass before completing the task!",
845
+ },
846
+ ],
847
+ };
848
+ }
849
+
850
+ if (!workflowState.state.commitAndPushCompleted) {
851
+ return {
852
+ content: [
853
+ {
854
+ type: "text",
855
+ text: "โš ๏ธ Commit and push not completed! Run 'commit_and_push' first.",
856
+ },
857
+ ],
858
+ };
859
+ }
860
+
861
+ if (!workflowState.state.released) {
862
+ return {
863
+ content: [
864
+ {
865
+ type: "text",
866
+ text: "โš ๏ธ Release not recorded! Run 'perform_release' after committing and pushing.",
867
+ },
868
+ ],
869
+ };
870
+ }
871
+
401
872
  if (await hasStagedChanges()) {
402
873
  return {
403
874
  content: [
@@ -416,6 +887,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
416
887
  testCommand: workflowState.state.testCommand,
417
888
  documentationType: workflowState.state.documentationType,
418
889
  commitMessage: args.commitMessage,
890
+ releaseCommand: workflowState.state.releaseCommand,
891
+ releaseNotes: workflowState.state.releaseNotes,
419
892
  });
420
893
 
421
894
  const completedTask = workflowState.state.taskDescription;
@@ -451,14 +924,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
451
924
  coding: "๐Ÿ”จ Currently coding",
452
925
  testing: "๐Ÿงช Ready for testing",
453
926
  documentation: "๐Ÿ“ Ready for documentation",
454
- ready: "โœ… Ready to commit",
927
+ ready: "โœ… Ready for verification",
928
+ commit: "๐Ÿ“ฆ Ready to commit & push",
929
+ release: "๐Ÿš€ Ready to release",
930
+ ready_to_complete: "โœ… Release completed",
455
931
  };
456
932
 
457
933
  return {
458
934
  content: [
459
935
  {
460
936
  type: "text",
461
- text: `๐Ÿ“‹ Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "โœ…" : "โณ"} Feature/bug fixed\n${status.testsPassed ? "โœ…" : "โณ"} Tests passed\n${status.documentationCreated ? "โœ…" : "โณ"} Documentation created\n${status.readyToCommit ? "โœ…" : "โณ"} Ready to commit\n\nNext: ${getNextStep(status)}`,
937
+ text: `๐Ÿ“‹ Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "โœ…" : "โณ"} Feature/bug fixed\n${status.testsCreated ? "โœ…" : "โณ"} Tests created\n${status.testsPassed ? "โœ…" : "โณ"} Tests passed\n${status.documentationCreated ? "โœ…" : "โณ"} Documentation created\n${status.readyCheckCompleted ? "โœ…" : "โณ"} Ready check completed\n${status.commitAndPushCompleted ? "โœ…" : "โณ"} Commit & push completed\n${status.released ? "โœ…" : "โณ"} Release recorded\n\nNext: ${getNextStep(status)}`,
462
938
  },
463
939
  ],
464
940
  };
@@ -496,18 +972,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
496
972
  };
497
973
  }
498
974
 
499
- default:
500
- throw new Error(`Unknown tool: ${name}`);
975
+ default:
976
+ throw new Error(`Unknown tool: ${name}`);
977
+ }
978
+ } catch (error) {
979
+ console.error("Tool handler error:", error);
980
+ return {
981
+ content: [
982
+ {
983
+ type: "text",
984
+ text: "โš ๏ธ Internal server error while processing the tool request. Please try again.",
985
+ },
986
+ ],
987
+ };
501
988
  }
502
989
  });
503
990
 
504
991
  // Helper function
505
992
  export function getNextStep(status) {
506
993
  if (!status.bugFixed) return "Mark feature/bug as fixed";
507
- if (!status.testsPassed) return "Create and run tests";
994
+ if (!status.testsCreated) return "Create tests";
995
+ if (!status.testsPassed) return "Run tests";
508
996
  if (!status.documentationCreated) return "Create documentation";
509
- if (!status.readyToCommit) return "Check if ready to commit";
510
- return "Commit and push!";
997
+ if (!status.readyCheckCompleted) return "Run 'check_ready_to_commit'";
998
+ if (!status.commitAndPushCompleted) return "Run 'commit_and_push'";
999
+ if (!status.released) return "Run 'perform_release'";
1000
+ return "Complete the task";
511
1001
  }
512
1002
 
513
1003
  // Define prompts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -23,6 +23,14 @@ test("WorkflowState persists data across save/load", async () => {
23
23
  state.state.currentPhase = "testing";
24
24
  state.state.taskDescription = "Test persistence";
25
25
  state.state.bugFixed = true;
26
+ state.state.testsCreated = true;
27
+ state.state.readyCheckCompleted = true;
28
+ state.state.released = true;
29
+ state.state.releaseCommand = "npm run release";
30
+ state.state.releaseNotes = "Automated release";
31
+ state.state.commitAndPushCompleted = true;
32
+ state.state.lastCommitMessage = "test: persistence";
33
+ state.state.lastPushBranch = "main";
26
34
  await state.save();
27
35
 
28
36
  const reloaded = new WorkflowState(state.stateFile);
@@ -31,21 +39,37 @@ test("WorkflowState persists data across save/load", async () => {
31
39
  assert.equal(reloaded.state.currentPhase, "testing");
32
40
  assert.equal(reloaded.state.taskDescription, "Test persistence");
33
41
  assert.equal(reloaded.state.bugFixed, true);
42
+ assert.equal(reloaded.state.testsCreated, true);
43
+ assert.equal(reloaded.state.readyCheckCompleted, true);
44
+ assert.equal(reloaded.state.released, true);
45
+ assert.equal(reloaded.state.releaseCommand, "npm run release");
46
+ assert.equal(reloaded.state.releaseNotes, "Automated release");
47
+ assert.equal(reloaded.state.commitAndPushCompleted, true);
48
+ assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
49
+ assert.equal(reloaded.state.lastPushBranch, "main");
34
50
  });
35
51
  });
36
52
 
37
53
  test("WorkflowState reset keeps history but clears progress", async () => {
38
54
  await withWorkflowState(async (state) => {
39
55
  state.state.currentPhase = "ready";
56
+ state.state.testsCreated = true;
40
57
  state.state.testsPassed = true;
41
58
  state.state.documentationCreated = true;
59
+ state.state.readyCheckCompleted = true;
60
+ state.state.released = true;
61
+ state.state.commitAndPushCompleted = true;
42
62
  state.addToHistory({ taskDescription: "Sample task" });
43
63
 
44
64
  state.reset();
45
65
 
46
66
  assert.equal(state.state.currentPhase, "idle");
67
+ assert.equal(state.state.testsCreated, false);
47
68
  assert.equal(state.state.testsPassed, false);
48
69
  assert.equal(state.state.documentationCreated, false);
70
+ assert.equal(state.state.readyCheckCompleted, false);
71
+ assert.equal(state.state.released, false);
72
+ assert.equal(state.state.commitAndPushCompleted, false);
49
73
  assert.equal(state.state.history.length, 1);
50
74
  assert.equal(state.state.history[0].taskDescription, "Sample task");
51
75
  });
@@ -54,9 +78,12 @@ test("WorkflowState reset keeps history but clears progress", async () => {
54
78
  test("getNextStep guides workflow progression", () => {
55
79
  const baseState = {
56
80
  bugFixed: false,
81
+ testsCreated: false,
57
82
  testsPassed: false,
58
83
  documentationCreated: false,
59
- readyToCommit: false,
84
+ readyCheckCompleted: false,
85
+ commitAndPushCompleted: false,
86
+ released: false,
60
87
  };
61
88
 
62
89
  assert.equal(
@@ -67,21 +94,27 @@ test("getNextStep guides workflow progression", () => {
67
94
  const afterFix = { ...baseState, bugFixed: true };
68
95
  assert.equal(
69
96
  getNextStep(afterFix),
70
- "Create and run tests"
97
+ "Create tests"
71
98
  );
72
99
 
73
- const afterTests = { ...afterFix, testsPassed: true };
100
+ const afterTestsCreated = { ...afterFix, testsCreated: true };
101
+ assert.equal(getNextStep(afterTestsCreated), "Run tests");
102
+
103
+ const afterTests = { ...afterTestsCreated, testsPassed: true };
74
104
  assert.equal(
75
105
  getNextStep(afterTests),
76
106
  "Create documentation"
77
107
  );
78
108
 
79
109
  const afterDocs = { ...afterTests, documentationCreated: true };
80
- assert.equal(
81
- getNextStep(afterDocs),
82
- "Check if ready to commit"
83
- );
110
+ assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
111
+
112
+ const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
113
+ assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
114
+
115
+ const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
116
+ assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
84
117
 
85
- const ready = { ...afterDocs, readyToCommit: true };
86
- assert.equal(getNextStep(ready), "Commit and push!");
118
+ const afterRelease = { ...afterCommit, released: true };
119
+ assert.equal(getNextStep(afterRelease), "Complete the task");
87
120
  });