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 +3 -0
- package/package.json +27 -0
- package/server.js +284 -0
- package/templates/master_template.json +524 -0
- package/templates/master_template.md +374 -0
- package/tools/execution.js +852 -0
- package/tools/planning.js +343 -0
- package/x-readiness-mcp-0.3.0.tgz +0 -0
package/bin/cli.js
ADDED
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);
|