@rigour-labs/mcp 2.19.2 → 2.20.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.
Files changed (3) hide show
  1. package/dist/index.js +106 -0
  2. package/package.json +2 -2
  3. package/src/index.ts +111 -0
package/dist/index.js CHANGED
@@ -67,6 +67,35 @@ async function logStudioEvent(cwd, event) {
67
67
  // Silent fail - Studio logging is non-blocking and zero-telemetry
68
68
  }
69
69
  }
70
+ // Helper to parse diff and get modified lines per file
71
+ function parseDiff(diff) {
72
+ const lines = diff.split('\n');
73
+ const mapping = {};
74
+ let currentFile = "";
75
+ let currentLine = 0;
76
+ for (const line of lines) {
77
+ if (line.startsWith('+++ b/')) {
78
+ currentFile = line.slice(6);
79
+ mapping[currentFile] = new Set();
80
+ }
81
+ else if (line.startsWith('@@')) {
82
+ const match = line.match(/\+(\d+)/);
83
+ if (match) {
84
+ currentLine = parseInt(match[1], 10);
85
+ }
86
+ }
87
+ else if (line.startsWith('+') && !line.startsWith('+++')) {
88
+ if (currentFile) {
89
+ mapping[currentFile].add(currentLine);
90
+ }
91
+ currentLine++;
92
+ }
93
+ else if (!line.startsWith('-')) {
94
+ currentLine++;
95
+ }
96
+ }
97
+ return mapping;
98
+ }
70
99
  server.setRequestHandler(ListToolsRequestSchema, async () => {
71
100
  return {
72
101
  tools: [
@@ -430,6 +459,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
430
459
  },
431
460
  required: ["cwd", "handoffId", "agentId"],
432
461
  },
462
+ },
463
+ {
464
+ name: "rigour_review",
465
+ description: "Perform a high-fidelity code review on a pull request diff. Analyzes changed files using all active quality gates.",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {
469
+ cwd: {
470
+ type: "string",
471
+ description: "Absolute path to the project root.",
472
+ },
473
+ repository: {
474
+ type: "string",
475
+ description: "Full repository name (e.g., 'owner/repo').",
476
+ },
477
+ branch: {
478
+ type: "string",
479
+ description: "The branch containing the changes.",
480
+ },
481
+ diff: {
482
+ type: "string",
483
+ description: "The git diff content to analyze.",
484
+ },
485
+ files: {
486
+ type: "array",
487
+ items: { type: "string" },
488
+ description: "List of filenames that were changed.",
489
+ },
490
+ },
491
+ required: ["cwd", "diff"],
492
+ },
433
493
  }
434
494
  ],
435
495
  };
@@ -1154,6 +1214,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1154
1214
  };
1155
1215
  break;
1156
1216
  }
1217
+ case "rigour_review": {
1218
+ const { diff, files: changedFiles } = args;
1219
+ // 1. Map diff to line numbers for filtering
1220
+ const diffMapping = parseDiff(diff);
1221
+ const targetFiles = changedFiles || Object.keys(diffMapping);
1222
+ // 2. Run high-fidelity analysis on changed files
1223
+ const report = await runner.run(cwd, targetFiles);
1224
+ // 3. Filter failures to only those on changed lines (or global gate failures)
1225
+ const filteredFailures = report.failures.filter(failure => {
1226
+ // Global failures (no file associated) are always reported
1227
+ if (!failure.files || failure.files.length === 0)
1228
+ return true;
1229
+ return failure.files.some(file => {
1230
+ const fileModifiedLines = diffMapping[file];
1231
+ // If we can't find the file in the diff, assume it's a side-effect or skip
1232
+ if (!fileModifiedLines)
1233
+ return false;
1234
+ // If failure has line info, check if it's in modifiedLines
1235
+ if (failure.line !== undefined) {
1236
+ return fileModifiedLines.has(failure.line);
1237
+ }
1238
+ // Fallback: if file is changed and no specific line is given, report it
1239
+ return true;
1240
+ });
1241
+ });
1242
+ result = {
1243
+ content: [
1244
+ {
1245
+ type: "text",
1246
+ text: JSON.stringify({
1247
+ status: filteredFailures.length > 0 ? "FAIL" : "PASS",
1248
+ failures: filteredFailures.map(f => ({
1249
+ id: f.id,
1250
+ gate: f.title,
1251
+ severity: "FAIL", // Rigour failures are FAIL by default
1252
+ message: f.details,
1253
+ file: f.files?.[0] || "",
1254
+ line: f.line || 1,
1255
+ suggestion: f.hint
1256
+ }))
1257
+ }),
1258
+ },
1259
+ ],
1260
+ };
1261
+ break;
1262
+ }
1157
1263
  default:
1158
1264
  throw new Error(`Unknown tool: ${name}`);
1159
1265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/mcp",
3
- "version": "2.19.2",
3
+ "version": "2.20.0",
4
4
  "type": "module",
5
5
  "mcpName": "io.github.rigour-labs/rigour",
6
6
  "description": "Quality gates for AI-generated code. Forces AI agents to meet strict engineering standards with PASS/FAIL enforcement.",
@@ -20,7 +20,7 @@
20
20
  "execa": "^8.0.1",
21
21
  "fs-extra": "^11.2.0",
22
22
  "yaml": "^2.8.2",
23
- "@rigour-labs/core": "2.19.2"
23
+ "@rigour-labs/core": "2.20.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.0.3",
package/src/index.ts CHANGED
@@ -93,6 +93,34 @@ async function logStudioEvent(cwd: string, event: any) {
93
93
  }
94
94
  }
95
95
 
96
+ // Helper to parse diff and get modified lines per file
97
+ function parseDiff(diff: string): Record<string, Set<number>> {
98
+ const lines = diff.split('\n');
99
+ const mapping: Record<string, Set<number>> = {};
100
+ let currentFile = "";
101
+ let currentLine = 0;
102
+
103
+ for (const line of lines) {
104
+ if (line.startsWith('+++ b/')) {
105
+ currentFile = line.slice(6);
106
+ mapping[currentFile] = new Set();
107
+ } else if (line.startsWith('@@')) {
108
+ const match = line.match(/\+(\d+)/);
109
+ if (match) {
110
+ currentLine = parseInt(match[1], 10);
111
+ }
112
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
113
+ if (currentFile) {
114
+ mapping[currentFile].add(currentLine);
115
+ }
116
+ currentLine++;
117
+ } else if (!line.startsWith('-')) {
118
+ currentLine++;
119
+ }
120
+ }
121
+ return mapping;
122
+ }
123
+
96
124
  server.setRequestHandler(ListToolsRequestSchema, async () => {
97
125
  return {
98
126
  tools: [
@@ -456,6 +484,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
456
484
  },
457
485
  required: ["cwd", "handoffId", "agentId"],
458
486
  },
487
+ },
488
+ {
489
+ name: "rigour_review",
490
+ description: "Perform a high-fidelity code review on a pull request diff. Analyzes changed files using all active quality gates.",
491
+ inputSchema: {
492
+ type: "object",
493
+ properties: {
494
+ cwd: {
495
+ type: "string",
496
+ description: "Absolute path to the project root.",
497
+ },
498
+ repository: {
499
+ type: "string",
500
+ description: "Full repository name (e.g., 'owner/repo').",
501
+ },
502
+ branch: {
503
+ type: "string",
504
+ description: "The branch containing the changes.",
505
+ },
506
+ diff: {
507
+ type: "string",
508
+ description: "The git diff content to analyze.",
509
+ },
510
+ files: {
511
+ type: "array",
512
+ items: { type: "string" },
513
+ description: "List of filenames that were changed.",
514
+ },
515
+ },
516
+ required: ["cwd", "diff"],
517
+ },
459
518
  }
460
519
  ],
461
520
  };
@@ -1270,6 +1329,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1270
1329
  break;
1271
1330
  }
1272
1331
 
1332
+ case "rigour_review": {
1333
+ const { diff, files: changedFiles } = args as any;
1334
+
1335
+ // 1. Map diff to line numbers for filtering
1336
+ const diffMapping = parseDiff(diff);
1337
+ const targetFiles = changedFiles || Object.keys(diffMapping);
1338
+
1339
+ // 2. Run high-fidelity analysis on changed files
1340
+ const report = await runner.run(cwd, targetFiles);
1341
+
1342
+ // 3. Filter failures to only those on changed lines (or global gate failures)
1343
+ const filteredFailures = report.failures.filter(failure => {
1344
+ // Global failures (no file associated) are always reported
1345
+ if (!failure.files || failure.files.length === 0) return true;
1346
+
1347
+ return failure.files.some(file => {
1348
+ const fileModifiedLines = diffMapping[file];
1349
+ // If we can't find the file in the diff, assume it's a side-effect or skip
1350
+ if (!fileModifiedLines) return false;
1351
+
1352
+ // If failure has line info, check if it's in modifiedLines
1353
+ if (failure.line !== undefined) {
1354
+ return fileModifiedLines.has(failure.line);
1355
+ }
1356
+
1357
+ // Fallback: if file is changed and no specific line is given, report it
1358
+ return true;
1359
+ });
1360
+ });
1361
+
1362
+ result = {
1363
+ content: [
1364
+ {
1365
+ type: "text",
1366
+ text: JSON.stringify({
1367
+ status: filteredFailures.length > 0 ? "FAIL" : "PASS",
1368
+ failures: filteredFailures.map(f => ({
1369
+ id: f.id,
1370
+ gate: f.title,
1371
+ severity: "FAIL", // Rigour failures are FAIL by default
1372
+ message: f.details,
1373
+ file: f.files?.[0] || "",
1374
+ line: f.line || 1,
1375
+ suggestion: f.hint
1376
+ }))
1377
+ }),
1378
+ },
1379
+ ],
1380
+ };
1381
+ break;
1382
+ }
1383
+
1273
1384
  default:
1274
1385
  throw new Error(`Unknown tool: ${name}`);
1275
1386
  }