x-readiness-mcp 0.3.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/bin/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import "../server.js";
3
+
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "x-readiness-mcp",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Clean, deterministic X-Readiness verification using MCP with Planning and Execution tools",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "x-readiness-mcp": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node server.js",
12
+ "dev": "node --watch server.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "x-readiness",
18
+ "api-verification",
19
+ "rest-api",
20
+ "conformance"
21
+ ],
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.0.0",
24
+ "cheerio": "^1.2.0",
25
+ "glob": "^13.0.0"
26
+ }
27
+ }
package/server.js ADDED
@@ -0,0 +1,284 @@
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
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import * as z from "zod";
15
+
16
+ import fs from "node:fs/promises";
17
+ import path from "node:path";
18
+ import crypto from "node:crypto";
19
+
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
+ }
65
+
66
+ // -----------------------------
67
+ // Explicit permission mechanism
68
+ // -----------------------------
69
+ // In-memory approvals keyed by (repoPath + planPath). Resets on server restart.
70
+ 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
+ }
76
+
77
+ function newApprovalCode() {
78
+ // 6-digit numeric code (easy to copy/paste)
79
+ return String(Math.floor(100000 + Math.random() * 900000));
80
+ }
81
+
82
+ function getOrCreateApproval(repoPath, planPath) {
83
+ const key = approvalKey(repoPath, planPath);
84
+ const existing = approvals.get(key);
85
+ 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));
99
+ }
100
+
101
+ function isApprovalValid(repoPath, planPath, providedCode) {
102
+ const key = approvalKey(repoPath, planPath);
103
+ const rec = approvals.get(key);
104
+ if (!rec) return false;
105
+ if (Date.now() - rec.createdAt > APPROVAL_TTL_MS) {
106
+ approvals.delete(key);
107
+ return false;
108
+ }
109
+ return String(providedCode) === String(rec.code);
110
+ }
111
+
112
+ // ============================================================================
113
+ // TOOL 1: planner (Planning Tool)
114
+ // ============================================================================
115
+ server.registerTool(
116
+ "planner",
117
+ {
118
+ 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.",
120
+ inputSchema: z.object({
121
+ scope: z
122
+ .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")
134
+ })
135
+ },
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);
149
+
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: JSON.stringify(
155
+ {
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)."
167
+ },
168
+ null,
169
+ 2
170
+ )
171
+ }
172
+ ]
173
+ };
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
+ }
187
+ }
188
+ );
189
+
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
+ // ============================================================================
197
+ server.registerTool(
198
+ "execute_checklist",
199
+ {
200
+ description:
201
+ "Execution Tool - Loads a stored plan from mcp/plan and checks a repository. Requires explicit approvalCode to execute.",
202
+ 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
+ )
215
+ })
216
+ },
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) {
266
+ return {
267
+ content: [
268
+ {
269
+ type: "text",
270
+ text: `Execution Error: ${error?.message ?? String(error)}`
271
+ }
272
+ ],
273
+ isError: true
274
+ };
275
+ }
276
+ }
277
+ );
278
+
279
+ // ============================================================================
280
+ // START SERVER
281
+ // ============================================================================
282
+ console.error("x-readiness-mcp: starting (stdio)...");
283
+ const transport = new StdioServerTransport();
284
+ await server.connect(transport);