@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,188 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { WorkflowState } from "../workflow-state.js";
8
+ import { handleToolCall } from "../tools/handlers.js";
9
+ import { normalizeRequestArgs, shellEscape } from "../utils.js";
10
+ import {
11
+ createCommitMessageParts,
12
+ determineReleaseTypeFromCommit,
13
+ } from "../commit-helpers.js";
14
+
15
+ async function withWorkflowState(callback) {
16
+ const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-handlers-"));
17
+ const stateFile = path.join(tempDir, "state.json");
18
+ const state = new WorkflowState(stateFile);
19
+ await state.load();
20
+
21
+ try {
22
+ await callback(state);
23
+ } finally {
24
+ await rm(tempDir, { recursive: true, force: true });
25
+ }
26
+ }
27
+
28
+ const utils = {
29
+ shellEscape,
30
+ determineReleaseTypeFromCommit,
31
+ createCommitMessageParts,
32
+ };
33
+
34
+ function createRequest(name, args) {
35
+ return {
36
+ params: {
37
+ name,
38
+ arguments: args,
39
+ },
40
+ };
41
+ }
42
+
43
+ test("commit_and_push commits changes and pushes", async () => {
44
+ await withWorkflowState(async (workflowState) => {
45
+ workflowState.state.readyToCommit = true;
46
+ workflowState.state.readyCheckCompleted = true;
47
+ workflowState.state.testsSkipped = false;
48
+ workflowState.state.testsCreated = true;
49
+ workflowState.state.testsPassed = true;
50
+ workflowState.state.currentPhase = "commit";
51
+ await workflowState.save();
52
+
53
+ const commands = [];
54
+ const execStub = async (command) => {
55
+ commands.push(command);
56
+ return { stdout: "" };
57
+ };
58
+
59
+ const git = {
60
+ hasWorkingChanges: async () => true,
61
+ hasTestChanges: async () => true,
62
+ getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
63
+ getCurrentBranch: async () => "main",
64
+ getLastCommitMessage: async () => "",
65
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
66
+ };
67
+
68
+ const response = await handleToolCall({
69
+ request: createRequest("commit_and_push", { commitMessage: "" }),
70
+ normalizeRequestArgs,
71
+ workflowState,
72
+ exec: execStub,
73
+ git,
74
+ utils,
75
+ });
76
+
77
+ const pushCommand = commands.find((command) => command.startsWith("git push"));
78
+ assert.ok(pushCommand, "git push should be executed during commit");
79
+ assert.ok(
80
+ pushCommand.startsWith("git push origin"),
81
+ "push command should target origin"
82
+ );
83
+ assert.equal(
84
+ commands.some((command) => command.startsWith("git commit")),
85
+ true,
86
+ "git commit should be executed"
87
+ );
88
+ assert.ok(
89
+ response.content[0].text.includes("Commit and push completed!"),
90
+ "response should confirm commit and push"
91
+ );
92
+ assert.equal(workflowState.state.commitAndPushCompleted, true);
93
+ assert.equal(workflowState.state.lastCommitMessage.length > 0, true);
94
+ assert.equal(workflowState.state.currentPhase, "release");
95
+ });
96
+ });
97
+
98
+ test("continue_workflow warns when workflow is idle", async () => {
99
+ await withWorkflowState(async (workflowState) => {
100
+ const git = {
101
+ hasWorkingChanges: async () => false,
102
+ hasTestChanges: async () => false,
103
+ getStagedChanges: async () => [],
104
+ getCurrentBranch: async () => "main",
105
+ getLastCommitMessage: async () => "",
106
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
107
+ };
108
+
109
+ const response = await handleToolCall({
110
+ request: createRequest("continue_workflow", {}),
111
+ normalizeRequestArgs,
112
+ workflowState,
113
+ exec: async () => ({ stdout: "" }),
114
+ git,
115
+ utils,
116
+ });
117
+
118
+ assert.ok(
119
+ response.content[0].text.includes("No active workflow"),
120
+ "Idle workflow should prompt user to start a task"
121
+ );
122
+ });
123
+ });
124
+
125
+ test("perform_release runs release command before pushing with tags", async () => {
126
+ await withWorkflowState(async (workflowState) => {
127
+ workflowState.state.currentPhase = "release";
128
+ workflowState.state.commitAndPushCompleted = true;
129
+ workflowState.state.readyCheckCompleted = true;
130
+ workflowState.state.lastPushBranch = "feature/split";
131
+ workflowState.state.lastCommitMessage = "fix: address issue";
132
+ await workflowState.save();
133
+
134
+ const commands = [];
135
+ const execStub = async (command) => {
136
+ commands.push(command);
137
+ if (command === "git status --porcelain") {
138
+ return { stdout: "" };
139
+ }
140
+
141
+ return { stdout: "" };
142
+ };
143
+
144
+ const git = {
145
+ hasWorkingChanges: async () => false,
146
+ hasTestChanges: async () => true,
147
+ getStagedChanges: async () => [],
148
+ getCurrentBranch: async () => "main",
149
+ getLastCommitMessage: async () => "",
150
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
151
+ };
152
+
153
+ const response = await handleToolCall({
154
+ request: createRequest("perform_release", { command: "npm run release" }),
155
+ normalizeRequestArgs,
156
+ workflowState,
157
+ exec: execStub,
158
+ git,
159
+ utils,
160
+ });
161
+
162
+ const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
163
+ assert.ok(releaseCommand, "release command should be executed");
164
+ assert.ok(
165
+ releaseCommand.includes("-- --release-as patch"),
166
+ "release command should append release type"
167
+ );
168
+
169
+ const pushCommand = commands.find((command) => command.startsWith("git push"));
170
+ assert.ok(pushCommand, "git push with tags should be executed after release");
171
+ assert.ok(
172
+ pushCommand.startsWith("git push --follow-tags"),
173
+ "push command should include --follow-tags"
174
+ );
175
+ assert.ok(
176
+ pushCommand.includes("feature/split"),
177
+ "push command should target last recorded branch"
178
+ );
179
+
180
+ assert.equal(workflowState.state.released, true);
181
+ assert.equal(workflowState.state.currentPhase, "ready_to_complete");
182
+ assert.ok(
183
+ response.content[0].text.includes("No workflow history yet") ||
184
+ response.content[0].text.includes("Workflow History"),
185
+ "response should describe workflow history or absence thereof"
186
+ );
187
+ });
188
+ });
@@ -0,0 +1,211 @@
1
+ export function getToolList() {
2
+ return [
3
+ {
4
+ name: "start_task",
5
+ description:
6
+ "Start a new coding task. This is the first step - be conscious about what you're coding.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ description: {
11
+ type: "string",
12
+ description: "Clear description of what you're going to code",
13
+ },
14
+ type: {
15
+ type: "string",
16
+ enum: ["feature", "bugfix", "refactor", "other"],
17
+ description: "Type of task",
18
+ },
19
+ },
20
+ required: ["description", "type"],
21
+ },
22
+ },
23
+ {
24
+ name: "mark_bug_fixed",
25
+ description:
26
+ "Mark that the bug/feature is fixed. Reminder: Now you MUST create tests!",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ summary: {
31
+ type: "string",
32
+ description: "Brief summary of what was fixed/implemented",
33
+ },
34
+ },
35
+ required: ["summary"],
36
+ },
37
+ },
38
+ {
39
+ name: "create_tests",
40
+ description: "Mark that tests covering the change have been created.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {},
44
+ },
45
+ },
46
+ {
47
+ name: "skip_tests",
48
+ description:
49
+ "Record that tests were intentionally skipped, with a justification.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ reason: {
54
+ type: "string",
55
+ description: "Why tests cannot be created or executed",
56
+ },
57
+ },
58
+ required: ["reason"],
59
+ },
60
+ },
61
+ {
62
+ name: "run_tests",
63
+ description:
64
+ "Record test results. NEVER commit if tests fail! Only proceed if all tests are green.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ passed: {
69
+ type: "boolean",
70
+ description: "Did all tests pass?",
71
+ },
72
+ testCommand: {
73
+ type: "string",
74
+ description: "The test command that was run",
75
+ },
76
+ details: {
77
+ type: "string",
78
+ description: "Test results details",
79
+ },
80
+ },
81
+ required: ["passed", "testCommand"],
82
+ },
83
+ },
84
+ {
85
+ name: "create_documentation",
86
+ description:
87
+ "Mark that documentation has been created/updated. This is required before committing.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ documentationType: {
92
+ type: "string",
93
+ enum: ["README", "inline-comments", "API-docs", "changelog", "other"],
94
+ description: "Type of documentation created",
95
+ },
96
+ summary: {
97
+ type: "string",
98
+ description: "What was documented",
99
+ },
100
+ },
101
+ required: ["documentationType", "summary"],
102
+ },
103
+ },
104
+ {
105
+ name: "check_ready_to_commit",
106
+ description:
107
+ "Check if all workflow steps are completed and you're ready to commit & push.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {},
111
+ },
112
+ },
113
+ {
114
+ name: "commit_and_push",
115
+ description:
116
+ "Automatically run git add, commit, and push once the ready check passes.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ commitMessage: {
121
+ type: "string",
122
+ description: "Commit message to use",
123
+ },
124
+ branch: {
125
+ type: "string",
126
+ description: "Optional branch name to push to (defaults to current branch)",
127
+ },
128
+ },
129
+ required: [],
130
+ },
131
+ },
132
+ {
133
+ name: "perform_release",
134
+ description:
135
+ "Record release details after committing and pushing your changes.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ command: {
140
+ type: "string",
141
+ description: "Release command that was executed",
142
+ },
143
+ notes: {
144
+ type: "string",
145
+ description: "Optional release notes",
146
+ },
147
+ },
148
+ required: ["command"],
149
+ },
150
+ },
151
+ {
152
+ name: "complete_task",
153
+ description:
154
+ "Mark the task as complete after successful commit & push. Resets workflow for next task.",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ commitMessage: {
159
+ type: "string",
160
+ description: "The commit message used",
161
+ },
162
+ },
163
+ required: ["commitMessage"],
164
+ },
165
+ },
166
+ {
167
+ name: "drop_task",
168
+ description:
169
+ "Abandon the current task and reset the workflow without completing it.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ reason: {
174
+ type: "string",
175
+ description: "Optional explanation for dropping the task",
176
+ },
177
+ },
178
+ },
179
+ },
180
+ {
181
+ name: "get_workflow_status",
182
+ description: "Get current workflow status and what needs to be done next.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {},
186
+ },
187
+ },
188
+ {
189
+ name: "view_history",
190
+ description: "View workflow history of completed tasks.",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {
194
+ limit: {
195
+ type: "number",
196
+ description: "Number of recent tasks to show (default: 10)",
197
+ },
198
+ },
199
+ },
200
+ },
201
+ {
202
+ name: "continue_workflow",
203
+ description:
204
+ "Get guidance on the next workflow step. Automatically performs the ready check when appropriate.",
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {},
208
+ },
209
+ },
210
+ ];
211
+ }