x-readiness-mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -1,169 +1,107 @@
1
1
  // mcp/server.js
2
- // X-Readiness MCP Server
3
- //
4
- // Tools:
5
- // 1) planner → Generate checklist + persist plan file
6
- // 2) execute_checklist → Load plan file + execute checks ONLY after explicit user approval code
7
- //
8
- // Why approval code?
9
- // - Copilot/agents may auto-call tools without asking.
10
- // - A user-provided approvalCode forces explicit human permission (copy/paste).
11
-
12
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
4
  import * as z from "zod";
15
-
16
- import fs from "node:fs/promises";
17
- import path from "node:path";
18
5
  import crypto from "node:crypto";
19
6
 
20
- import { generateChecklist } from "./tools/planning.js";
21
- import { executionTool } from "./tools/execution.js";
22
-
23
- const server = new McpServer(
24
- { name: "x-readiness-mcp", version: "0.3.0" },
25
- { capabilities: { tools: {} } }
26
- );
27
-
28
- // -----------------------------
29
- // Plan persistence helpers
30
- // -----------------------------
31
- const PLAN_DIR = path.resolve(process.cwd(), ".xreadiness", "plan");
32
-
33
- async function ensurePlanDir() {
34
- await fs.mkdir(PLAN_DIR, { recursive: true });
35
- }
36
-
37
- async function savePlanToDisk(plan) {
38
- await ensurePlanDir();
39
-
40
- const planId =
41
- (typeof plan?.checklist_id === "string" && plan.checklist_id.trim()) ||
42
- `plan_${crypto.randomUUID()}`;
43
-
44
- // Sanitize filename for Windows (replace colons with hyphens)
45
- const sanitizedPlanId = planId.replace(/:/g, "-");
46
-
47
- const planPath = path.join(PLAN_DIR, `${sanitizedPlanId}.json`);
48
- await fs.writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
49
-
50
- return { planId, planPath };
51
- }
52
-
53
- async function loadPlanFromDisk(planPath) {
54
- const raw = await fs.readFile(planPath, "utf8");
55
- return JSON.parse(raw);
56
- }
57
-
58
- function countRulesFromPlan(plan) {
59
- if (Array.isArray(plan?.features)) {
60
- return plan.features.reduce((acc, f) => acc + (f?.rules?.length ?? 0), 0);
61
- }
62
- if (Array.isArray(plan?.rules)) return plan.rules.length;
63
- return 0;
64
- }
7
+ import { planTool } from "./tools/planning.js";
8
+ import { executeTool } from "./tools/execution.js";
9
+ import { autoFixTool } from "./tools/autofix.js";
65
10
 
66
11
  // -----------------------------
67
- // Explicit permission mechanism
12
+ // Approval gate (in-memory)
68
13
  // -----------------------------
69
- // In-memory approvals keyed by (repoPath + planPath). Resets on server restart.
70
14
  const approvals = new Map(); // key -> { code, createdAt }
71
- const APPROVAL_TTL_MS = 10 * 60 * 1000; // 10 minutes
72
-
73
- function approvalKey(repoPath, planPath) {
74
- return `${repoPath}||${planPath}`;
75
- }
15
+ const APPROVAL_TTL_MS = 10 * 60 * 1000;
76
16
 
77
17
  function newApprovalCode() {
78
- // 6-digit numeric code (easy to copy/paste)
79
18
  return String(Math.floor(100000 + Math.random() * 900000));
80
19
  }
81
-
82
- function getOrCreateApproval(repoPath, planPath) {
83
- const key = approvalKey(repoPath, planPath);
20
+ function approvalKey(kind, repoPath, ref) {
21
+ return `${kind}||${repoPath}||${ref}`;
22
+ }
23
+ function getOrCreateApproval(kind, repoPath, ref) {
24
+ const key = approvalKey(kind, repoPath, ref);
84
25
  const existing = approvals.get(key);
85
26
  const now = Date.now();
86
-
87
- if (existing && now - existing.createdAt <= APPROVAL_TTL_MS) {
88
- return { key, ...existing };
89
- }
90
-
91
- const code = newApprovalCode();
92
- const createdAt = now;
93
- approvals.set(key, { code, createdAt });
94
- return { key, code, createdAt };
95
- }
96
-
97
- function clearApproval(repoPath, planPath) {
98
- approvals.delete(approvalKey(repoPath, planPath));
27
+ if (existing && now - existing.createdAt <= APPROVAL_TTL_MS) return existing;
28
+ const rec = { code: newApprovalCode(), createdAt: now };
29
+ approvals.set(key, rec);
30
+ return rec;
99
31
  }
100
-
101
- function isApprovalValid(repoPath, planPath, providedCode) {
102
- const key = approvalKey(repoPath, planPath);
32
+ function isApprovalValid(kind, repoPath, ref, code) {
33
+ const key = approvalKey(kind, repoPath, ref);
103
34
  const rec = approvals.get(key);
104
35
  if (!rec) return false;
105
36
  if (Date.now() - rec.createdAt > APPROVAL_TTL_MS) {
106
37
  approvals.delete(key);
107
38
  return false;
108
39
  }
109
- return String(providedCode) === String(rec.code);
40
+ return String(code) === String(rec.code);
41
+ }
42
+ function clearApproval(kind, repoPath, ref) {
43
+ approvals.delete(approvalKey(kind, repoPath, ref));
110
44
  }
111
45
 
112
- // ============================================================================
113
- // TOOL 1: planner (Planning Tool)
114
- // ============================================================================
46
+ const server = new McpServer(
47
+ { name: "x-readiness-mcp", version: "0.4.0" },
48
+ { capabilities: { tools: {} } }
49
+ );
50
+
51
+ // -----------------------------
52
+ // TOOL: plan
53
+ // -----------------------------
115
54
  server.registerTool(
116
- "planner",
55
+ "plan",
117
56
  {
118
57
  description:
119
- "Planning Tool - Generate X-API readiness checklist and store it in mcp/plan as a JSON plan file. Returns plan_id and plan_path.",
58
+ "Planning Tool: Generate a high-level X-Readiness plan with rule checklist based on scope and developer intent. Creates plan files in mcp/plan folder. This is Step 1 - after generating the plan, you'll need to get approval before executing it.",
120
59
  inputSchema: z.object({
60
+ repoPath: z.string().describe("Path to repo/workspace root to analyze"),
121
61
  scope: z
62
+ .enum(["x_api_readiness", "pagination_ready", "security_ready", "error_handling_ready", "versioning_ready"])
63
+ .describe("Which readiness scope to plan for"),
64
+ developerPrompt: z
122
65
  .string()
123
- .describe(
124
- "Readiness scope: 'x_api_readiness', 'pagination_ready', 'security_ready', etc."
125
- ),
126
- format: z
127
- .enum(["json", "markdown", "both"])
128
- .optional()
129
- .describe("Output format (default: 'json')"),
130
- intent: z
131
- .string()
132
- .optional()
133
- .describe("Optional intent/context string to store with the plan")
66
+ .describe("Developer intent: what to focus on, specific requirements, or keywords (e.g., 'pagination', 'error handling')"),
67
+ outputFormat: z.enum(["markdown", "json", "both"]).optional().default("markdown")
134
68
  })
135
69
  },
136
- async ({ scope, format = "json", intent }) => {
137
- try {
138
- const checklist = await generateChecklist(scope, format);
139
-
140
- if (intent) checklist.intent = intent;
141
-
142
- const { planId, planPath } = await savePlanToDisk(checklist);
143
-
144
- const featuresCount = Array.isArray(checklist?.features)
145
- ? checklist.features.length
146
- : 0;
147
-
148
- const rulesCount = countRulesFromPlan(checklist);
70
+ async (input) => {
71
+ const result = await planTool(input);
72
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
73
+ }
74
+ );
149
75
 
76
+ // -----------------------------
77
+ // TOOL: execute (gated)
78
+ // -----------------------------
79
+ server.registerTool(
80
+ "execute",
81
+ {
82
+ description:
83
+ "Execution Tool: Execute a saved plan against source code, checking each rule systematically. Generates detailed violation report with file/line references. Step 2 - REQUIRES APPROVAL. You will receive an approval code that must be provided to proceed with execution.",
84
+ inputSchema: z.object({
85
+ repoPath: z.string().describe("Path to repo/workspace root"),
86
+ planPath: z.string().describe("Path to the plan JSON file (from plan tool output)"),
87
+ approvalCode: z.string().optional().describe("6-digit approval code (will be provided when first called without code)")
88
+ })
89
+ },
90
+ async ({ repoPath, planPath, approvalCode }) => {
91
+ if (!approvalCode || !isApprovalValid("execute", repoPath, planPath, approvalCode)) {
92
+ const { code } = getOrCreateApproval("execute", repoPath, planPath);
150
93
  return {
151
94
  content: [
152
95
  {
153
96
  type: "text",
154
97
  text: JSON.stringify(
155
98
  {
156
- plan_id: planId,
157
- plan_path: planPath,
158
- summary: {
159
- scope: checklist.scope ?? scope,
160
- template_version: checklist.template_version,
161
- template_last_updated: checklist.template_last_updated,
162
- features: featuresCount,
163
- rules: rulesCount
164
- },
165
- next_step:
166
- "To execute, call execute_checklist with repoPath, planPath, and the approvalCode (you will be prompted for it)."
99
+ status: "CONFIRMATION_REQUIRED",
100
+ message:
101
+ "⚠️ APPROVAL REQUIRED: Execution will scan your source code and check against all rules in the plan.\n\nTo proceed, call the 'execute' tool again with the same parameters and include the approvalCode below.",
102
+ approvalCode: code,
103
+ expires_in_minutes: Math.floor(APPROVAL_TTL_MS / 60000),
104
+ next_action: "Re-run execute tool with approvalCode parameter"
167
105
  },
168
106
  null,
169
107
  2
@@ -171,114 +109,66 @@ server.registerTool(
171
109
  }
172
110
  ]
173
111
  };
174
- } catch (error) {
175
- return {
176
- content: [
177
- {
178
- type: "text",
179
- text: `Planning Error: ${error?.message ?? String(error)}\n${
180
- error?.stack ?? ""
181
- }`
182
- }
183
- ],
184
- isError: true
185
- };
186
112
  }
113
+
114
+ clearApproval("execute", repoPath, planPath);
115
+ const result = await executeTool({ repoPath, planPath });
116
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
187
117
  }
188
118
  );
189
119
 
190
- // ============================================================================
191
- // TOOL 2: execute_checklist (Execution Tool)
192
- // ============================================================================
193
- // - Loads plan JSON from disk (planPath)
194
- // - Requires approvalCode to proceed
195
- // - If approvalCode missing/wrong, returns CONFIRMATION_REQUIRED (no execution)
196
- // ============================================================================
120
+ // -----------------------------
121
+ // TOOL: auto_fix (gated)
122
+ // -----------------------------
197
123
  server.registerTool(
198
- "execute_checklist",
124
+ "auto_fix",
199
125
  {
200
126
  description:
201
- "Execution Tool - Loads a stored plan from mcp/plan and checks a repository. Requires explicit approvalCode to execute.",
127
+ "Auto-fix Tool: Generate fixes for violations found in the execution report. Step 3 - REQUIRES APPROVAL. Can propose fixes or apply them directly.",
202
128
  inputSchema: z.object({
203
- repoPath: z
204
- .string()
205
- .describe("Path to the repository or source code to check"),
206
- planPath: z
207
- .string()
208
- .describe("Path to the saved plan JSON file (from planner output)"),
209
- approvalCode: z
210
- .string()
211
- .optional()
212
- .describe(
213
- "User approval code required to run. If omitted, the tool returns a code and stops."
214
- )
129
+ repoPath: z.string().describe("Path to repo/workspace root"),
130
+ runId: z.string().describe("run_id from execute output"),
131
+ mode: z.enum(["propose", "apply"]).default("propose").describe("'propose' shows fixes without applying, 'apply' applies fixes to source code"),
132
+ approvalCode: z.string().optional().describe("6-digit approval code (will be provided when first called without code)")
215
133
  })
216
134
  },
217
- async ({ repoPath, planPath, approvalCode }) => {
218
- try {
219
- // Permission gate: do not execute unless approvalCode matches
220
- if (!approvalCode || !isApprovalValid(repoPath, planPath, approvalCode)) {
221
- const { code } = getOrCreateApproval(repoPath, planPath);
222
-
223
- return {
224
- content: [
225
- {
226
- type: "text",
227
- text: JSON.stringify(
228
- {
229
- status: "CONFIRMATION_REQUIRED",
230
- message:
231
- "Execution not started. Please confirm you want to run analysis by re-running execute_checklist with the approvalCode below.",
232
- approvalCode: code,
233
- expires_in_minutes: Math.floor(APPROVAL_TTL_MS / 60000),
234
- next_call_example: {
235
- tool: "execute_checklist",
236
- input: { repoPath, planPath, approvalCode: code }
237
- }
238
- },
239
- null,
240
- 2
241
- )
242
- }
243
- ]
244
- };
245
- }
246
-
247
- // If we’re here, permission is confirmed
248
- clearApproval(repoPath, planPath);
249
-
250
- const checklist = await loadPlanFromDisk(planPath);
251
- const results = await executionTool(repoPath, checklist);
252
-
253
- // Optional: make "all skipped" clearer so it isn't interpreted as 0% failure
254
- const rs = results?.readiness_summary;
255
- if (rs && rs.total_rules_checked === 0 && (rs.rules_skipped ?? 0) > 0) {
256
- rs.status = "NOT_EVALUATED";
257
- rs.message =
258
- "No rules were evaluated (all skipped). Score is not meaningful without detectable API signals.";
259
- rs.score = "N/A";
260
- }
261
-
262
- return {
263
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
264
- };
265
- } catch (error) {
135
+ async ({ repoPath, runId, mode, approvalCode }) => {
136
+ const ref = `${runId}||${mode}`;
137
+ if (!approvalCode || !isApprovalValid("auto_fix", repoPath, ref, approvalCode)) {
138
+ const { code } = getOrCreateApproval("auto_fix", repoPath, ref);
266
139
  return {
267
140
  content: [
268
141
  {
269
142
  type: "text",
270
- text: `Execution Error: ${error?.message ?? String(error)}`
143
+ text: JSON.stringify(
144
+ {
145
+ status: "CONFIRMATION_REQUIRED",
146
+ message:
147
+ "⚠️ APPROVAL REQUIRED: Auto-fix will analyze violations and " +
148
+ (mode === "apply" ? "MODIFY your source code files." : "propose changes to your source code.") +
149
+ "\n\nTo proceed, call the 'auto_fix' tool again with the same parameters and include the approvalCode below.",
150
+ approvalCode: code,
151
+ expires_in_minutes: Math.floor(APPROVAL_TTL_MS / 60000),
152
+ next_action: "Re-run auto_fix tool with approvalCode parameter"
153
+ },
154
+ null,
155
+ 2
156
+ )
271
157
  }
272
- ],
273
- isError: true
158
+ ]
274
159
  };
275
160
  }
161
+
162
+ clearApproval("auto_fix", repoPath, ref);
163
+ const result = await autoFixTool({ repoPath, runId, mode });
164
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
276
165
  }
277
166
  );
278
167
 
279
- // ============================================================================
168
+ // -----------------------------
280
169
  // START SERVER
281
- // ============================================================================
170
+ // -----------------------------
282
171
  console.error("x-readiness-mcp: starting (stdio)...");
283
172
  const transport = new StdioServerTransport();
284
173
  await server.connect(transport);
174
+
@@ -0,0 +1,151 @@
1
+ // mcp/tools/autofix.js
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ function reportsPath(repoPath, runId) {
10
+ return path.resolve(repoPath, ".xreadiness", "reports", `${runId}.json`);
11
+ }
12
+ function patchesDir(repoPath) {
13
+ return path.resolve(repoPath, ".xreadiness", "patches");
14
+ }
15
+ async function ensureDir(p) {
16
+ await fs.mkdir(p, { recursive: true });
17
+ }
18
+
19
+ async function gitIsClean(repoPath) {
20
+ try {
21
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain"], { cwd: repoPath });
22
+ return stdout.trim().length === 0;
23
+ } catch {
24
+ // If git isn't available, be conservative
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function getGitDiff(repoPath) {
30
+ try {
31
+ const { stdout } = await execFileAsync("git", ["diff", "--stat"], { cwd: repoPath });
32
+ const { stdout: diffContent } = await execFileAsync("git", ["diff"], { cwd: repoPath });
33
+ return { stat: stdout, diff: diffContent };
34
+ } catch (err) {
35
+ return { stat: "Error getting git diff", diff: err.message };
36
+ }
37
+ }
38
+
39
+ function buildPatchFromViolations(report) {
40
+ // For v1, only generate a patch if violations include a ready-to-apply patch chunk.
41
+ // You can extend your rule checkers to attach suggestedFixUnifiedDiff per violation.
42
+ const diffs = report.violations
43
+ .map(v => v.suggested_patch)
44
+ .filter(Boolean);
45
+
46
+ return diffs.join("\n");
47
+ }
48
+
49
+ /**
50
+ * Auto-fix tool with 2-stage approval:
51
+ * - mode="suggest": Shows proposed patches/diffs without applying
52
+ * - mode="apply": Applies patches to source code (requires clean git state)
53
+ */
54
+ export async function autoFixTool({ repoPath, runId, mode = "suggest" }) {
55
+ const reportFile = reportsPath(repoPath, runId);
56
+
57
+ let reportData;
58
+ try {
59
+ const raw = await fs.readFile(reportFile, "utf8");
60
+ reportData = JSON.parse(raw);
61
+ } catch (err) {
62
+ return {
63
+ status: "ERROR",
64
+ message: `Failed to load report file: ${reportFile}. Error: ${err.message}`
65
+ };
66
+ }
67
+
68
+ const patch = buildPatchFromViolations(reportData);
69
+ if (!patch) {
70
+ return {
71
+ status: "NO_AUTOFIX_AVAILABLE",
72
+ message: "No violations contain auto-fix patches yet. Implement suggested_patch in rule checkers first.",
73
+ violations_count: reportData.violations?.length || 0,
74
+ note: "Auto-fix requires rule checkers to provide suggested_patch field for each violation"
75
+ };
76
+ }
77
+
78
+ await ensureDir(patchesDir(repoPath));
79
+ const patchPath = path.join(patchesDir(repoPath), `${runId}.patch`);
80
+ await fs.writeFile(patchPath, patch, "utf8");
81
+
82
+ // SUGGEST mode: just show the proposed changes
83
+ if (mode === "suggest") {
84
+ const violationsWithFixes = reportData.violations.filter(v => v.suggested_patch);
85
+ const filesImpacted = [...new Set(violationsWithFixes.map(v => v.file_path))];
86
+
87
+ return {
88
+ status: "PATCH_PROPOSED",
89
+ mode: "suggest",
90
+ patch_path: patchPath,
91
+ patch_preview: patch.substring(0, 2000) + (patch.length > 2000 ? "\n... (truncated)" : ""),
92
+ files_impacted: filesImpacted,
93
+ violations_with_fixes: violationsWithFixes.length,
94
+ total_violations: reportData.violations?.length || 0,
95
+ applied: false,
96
+ message: `✅ Auto-fix analysis complete!\n\n📊 Summary:\n- ${violationsWithFixes.length} violations have suggested fixes\n- ${filesImpacted.length} files will be modified\n\n📄 Patch saved to: ${patchPath}\n\n⚠️ NEXT STEP:\n- Review the patch file carefully\n- To apply fixes, call 'auto_fix' tool again with mode='apply'\n- Application requires approval code and clean git state`,
97
+ risk_notes: [
98
+ "Patches are auto-generated and may need manual review",
99
+ "Always commit your changes before applying auto-fixes",
100
+ "Test thoroughly after applying fixes"
101
+ ],
102
+ next_action: "Review patch, then call auto_fix with mode='apply' and approvalCode"
103
+ };
104
+ }
105
+
106
+ // APPLY mode: actually modify files
107
+ const clean = await gitIsClean(repoPath);
108
+ if (!clean) {
109
+ return {
110
+ status: "REQUIRES_CLEAN_GIT",
111
+ message: "⚠️ Git repository has uncommitted changes.\n\nBefore applying auto-fixes:\n1. Commit your current changes: git commit -am 'your message'\n2. Or stash changes: git stash\n3. Then call auto_fix again with mode='apply'",
112
+ patch_path: patchPath,
113
+ applied: false,
114
+ instruction: "Clean your git working directory before applying patches"
115
+ };
116
+ }
117
+
118
+ // Apply patch via git apply
119
+ try {
120
+ await execFileAsync("git", ["apply", patchPath], { cwd: repoPath });
121
+ } catch (err) {
122
+ return {
123
+ status: "PATCH_FAILED",
124
+ message: `Failed to apply patch: ${err.message}`,
125
+ patch_path: patchPath,
126
+ applied: false,
127
+ suggestion: "Review the patch file manually and apply fixes by hand, or check for conflicts"
128
+ };
129
+ }
130
+
131
+ // Get git diff summary after applying
132
+ const { stat, diff } = await getGitDiff(repoPath);
133
+ const modifiedFiles = stat.split("\n").filter(line => line.trim()).slice(0, -1); // Remove summary line
134
+
135
+ return {
136
+ status: "PATCH_APPLIED",
137
+ mode: "apply",
138
+ patch_path: patchPath,
139
+ applied: true,
140
+ files_modified: modifiedFiles,
141
+ git_diff_stat: stat,
142
+ git_diff_preview: diff.substring(0, 3000) + (diff.length > 3000 ? "\n... (truncated, use git diff for full output)" : ""),
143
+ message: `✅ Auto-fixes applied successfully!\n\n📊 Changes:\n${stat}\n\n⚠️ IMPORTANT:\n1. Review the changes: git diff\n2. Test your application thoroughly\n3. Commit if satisfied: git commit -am 'Applied X-Readiness auto-fixes'\n4. Or revert if needed: git checkout .`,
144
+ next_steps: [
145
+ "Review git diff output",
146
+ "Run tests to verify fixes",
147
+ "Commit changes if satisfied",
148
+ "Re-run execution to verify compliance"
149
+ ]
150
+ };
151
+ }