secufusion-mcp 1.0.37 → 1.0.38

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/AGENTS.md +2 -18
  2. package/index.js +0 -396
  3. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -436,25 +436,13 @@ STEP 3: do NOT repeat the rejected pattern — ever
436
436
  ## Phase 4 — PR Handoff
437
437
  (TRIGGER: developer says "prepare PR", "run checks", or "ready to merge")
438
438
 
439
- ### MANDATORY sequence — do not raise a PR until all steps pass with zero errors
439
+ ### MANDATORY sequence
440
440
 
441
441
  ```
442
442
  STEP 1: call manage_task(action: "complete", work_item_id: <id>)
443
443
  — marks status complete, auto-generates pr-summary.md
444
444
 
445
- STEP 2: call run_pre_pr_checks(work_item_id: <id>)
446
- — runs: spec checkbox check, AST linting, tenant isolation scan,
447
- hardcoded URL scan, Flyway migration coverage
448
-
449
- STEP 3: if run_pre_pr_checks returns ANY error:
450
- → navigate to the failing microservice
451
- → fix the error NATIVELY in source code
452
- → call run_pre_pr_checks again
453
- → repeat until ZERO errors — no exceptions
454
-
455
- STEP 4: raise PR only when run_pre_pr_checks reports zero errors
456
-
457
- STEP 5: call generate_ado_comments(work_item_id, layman_summary, technical_deep_dive)
445
+ STEP 2: call generate_ado_comments(work_item_id, layman_summary, technical_deep_dive)
458
446
  — Use this tool to output two distinct comments for the developer to paste into the Azure DevOps (ADO) board:
459
447
  1. **Layman Summary**: A short, fundamental, non-technical explanation of the business value and what was resolved.
460
448
  2. **Technical Deep-Dive**: A comprehensive, highly technical breakdown of the architectural changes, core optimizations, and exact implementation details.
@@ -463,10 +451,6 @@ STEP 5: call generate_ado_comments(work_item_id, layman_summary, technical_deep_
463
451
  ### Hard enforcement
464
452
 
465
453
  ❌ Do NOT raise a PR while `pending_acs` is non-empty
466
- ❌ Do NOT suppress linter warnings to pass the gate — fix them natively
467
- ❌ Do NOT skip `run_pre_pr_checks` and assume the workspace is clean
468
- ❌ Do NOT raise a PR if tenant isolation violations are present — security breach
469
- ❌ Do NOT raise a PR if Flyway coverage is missing for a modified `@Entity`
470
454
 
471
455
  ## Phase 5 — Retrospective
472
456
  (TRIGGER: after manage_task action=complete is called
package/index.js CHANGED
@@ -112,207 +112,6 @@ function appendTelemetry(response, inputChars) {
112
112
  return response;
113
113
  }
114
114
  // ─────────────────────────────────────────────
115
- // run_pre_pr_checks helpers
116
- // ─────────────────────────────────────────────
117
- const CONSOLE_LOG_REGEX = [
118
- { regex: /console\.log\s*\(/g, label: "console.log()", ext: [".ts", ".tsx", ".js", ".jsx"] },
119
- { regex: /System\.out\.println\s*\(/g, label: "System.out.println()", ext: [".java"] },
120
- ];
121
- const HARDCODED_URL_PATTERN = /(?:https?:\/\/)?(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?|(?:uat|prod|staging|preprod)\.[a-z0-9.-]+\.[a-z]{2,}/gi;
122
- function walkDir(dir, exts) {
123
- let results = [];
124
- try {
125
- const list = fs.readdirSync(dir);
126
- for (const file of list) {
127
- if (file === "node_modules" || file === ".git" || file === "dist" || file === "target")
128
- continue;
129
- const filePath = path.join(dir, file);
130
- const stat = fs.statSync(filePath);
131
- if (stat && stat.isDirectory()) {
132
- results = results.concat(walkDir(filePath, exts));
133
- }
134
- else {
135
- if (exts.some(ext => file.endsWith(ext))) {
136
- results.push(filePath);
137
- }
138
- }
139
- }
140
- }
141
- catch (e) {
142
- /* ignore */
143
- }
144
- return results;
145
- }
146
- // ── AST-level linting via ESLint (preferred over regex for JS/TS) ─────────────
147
- function runEslint(rootDir) {
148
- // Check if ESLint is configured in the project
149
- const eslintConfigs = [".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml",
150
- ".eslintrc.yaml", "eslint.config.js", "eslint.config.mjs"];
151
- const hasEslintConfig = eslintConfigs.some((f) => fs.existsSync(path.join(rootDir, f)));
152
- const hasPkgEslint = (() => {
153
- const pkg = readFileSafe(path.join(rootDir, "package.json"));
154
- if (!pkg)
155
- return false;
156
- try {
157
- return !!(JSON.parse(pkg).eslintConfig);
158
- }
159
- catch {
160
- return false;
161
- }
162
- })();
163
- if (!hasEslintConfig && !hasPkgEslint)
164
- return { violations: [], available: false };
165
- try {
166
- execSync("npx eslint . --format json --max-warnings=0 --no-error-on-unmatched-pattern", { cwd: rootDir, stdio: "pipe" });
167
- return { violations: [], available: true };
168
- }
169
- catch (e) {
170
- const raw = e.stdout?.toString() ?? "";
171
- try {
172
- const results = JSON.parse(raw);
173
- const violations = [];
174
- for (const file of results) {
175
- for (const msg of file.messages) {
176
- if (msg.severity > 0) {
177
- violations.push(`[ESLint] ${path.relative(rootDir, file.filePath)}:${msg.line} — ${msg.message}${msg.ruleId ? ` (${msg.ruleId})` : ""}`);
178
- }
179
- }
180
- }
181
- return { violations, available: true };
182
- }
183
- catch {
184
- // ESLint output not JSON — return raw stderr summary
185
- const stderr = e.stderr?.toString() ?? "";
186
- return { violations: stderr ? [`[ESLint] ${stderr.slice(0, 400)}`] : [], available: true };
187
- }
188
- }
189
- }
190
- // ── Maven + Checkstyle for Java/Spring projects ───────────────────────────────
191
- function runCheckstyle(rootDir) {
192
- const hasPom = fs.existsSync(path.join(rootDir, "pom.xml"));
193
- if (!hasPom)
194
- return { violations: [], available: false };
195
- try {
196
- execSync("mvn checkstyle:check -q --no-transfer-progress", { cwd: rootDir, stdio: "pipe" });
197
- return { violations: [], available: true };
198
- }
199
- catch (e) {
200
- const output = ((e.stdout?.toString() ?? "") + (e.stderr?.toString() ?? "")).slice(0, 1200);
201
- return {
202
- violations: [`[Checkstyle/Maven] Build failed:\n${output}`],
203
- available: true,
204
- };
205
- }
206
- }
207
- // ── Regex fallback — used only when no linter is configured ──────────────────
208
- function checkConsoleLogsRegex(rootDir) {
209
- const violations = [];
210
- for (const { regex, label, ext } of CONSOLE_LOG_REGEX) {
211
- const files = walkDir(rootDir, ext);
212
- for (const file of files) {
213
- const content = readFileSafe(file);
214
- if (!content)
215
- continue;
216
- const lines = content.split("\n");
217
- lines.forEach((line, idx) => {
218
- if (regex.test(line)) {
219
- violations.push(`${label} found at ${path.relative(rootDir, file)}:${idx + 1}`);
220
- }
221
- regex.lastIndex = 0;
222
- });
223
- }
224
- }
225
- return violations;
226
- }
227
- function checkHardcodedUrls(rootDir) {
228
- const violations = [];
229
- const exts = [".ts", ".tsx", ".js", ".jsx", ".java", ".properties", ".yml", ".yaml"];
230
- const files = walkDir(rootDir, exts);
231
- for (const file of files) {
232
- const content = readFileSafe(file);
233
- if (!content)
234
- continue;
235
- const matches = content.match(HARDCODED_URL_PATTERN);
236
- if (matches) {
237
- violations.push(`Hardcoded URL/IP in ${path.relative(rootDir, file)}: ${[...new Set(matches)].join(", ")}`);
238
- }
239
- }
240
- return violations;
241
- }
242
- // ── Workspace / Microservice Discovery ───────────────────────────────────────
243
- function findProjectRoots(dir, depth = 0) {
244
- if (depth > 3)
245
- return []; // Limit depth to avoid scanning massive trees
246
- if (!fs.existsSync(dir))
247
- return [];
248
- const entries = fs.readdirSync(dir, { withFileTypes: true });
249
- const hasPackageJson = entries.some(e => e.name === "package.json");
250
- const hasPomXml = entries.some(e => e.name === "pom.xml");
251
- const roots = [];
252
- if (hasPackageJson || hasPomXml) {
253
- roots.push(dir);
254
- }
255
- for (const entry of entries) {
256
- if (entry.isDirectory()) {
257
- if (["node_modules", ".git", "dist", "build", "target", ".idea"].includes(entry.name))
258
- continue;
259
- roots.push(...findProjectRoots(path.join(dir, entry.name), depth + 1));
260
- }
261
- }
262
- return [...new Set(roots)];
263
- }
264
- // ── Automated Tenant Isolation Scanner ────────────────────────────────────────
265
- function checkTenantIsolation(rootDir) {
266
- const violations = [];
267
- const files = walkDir(rootDir, [".java"]).filter(f => f.endsWith("Repository.java"));
268
- const queryMethodRegex = /\b(?:find|get|read|query|search|stream|count|exists|delete|remove)(?:All)?By[A-Z0-9]/;
269
- const atQueryRegex = /@Query\s*\(/;
270
- for (const file of files) {
271
- const content = readFileSafe(file);
272
- if (!content)
273
- continue;
274
- const lines = content.split("\n");
275
- for (let i = 0; i < lines.length; i++) {
276
- const line = lines[i];
277
- if (queryMethodRegex.test(line) || atQueryRegex.test(line)) {
278
- // Look at current line and the next to handle simple wrapping
279
- const combinedContext = line + (lines[i + 1] || "");
280
- if (!/tenant/i.test(combinedContext)) {
281
- violations.push(`Missing tenant isolation in ${path.relative(rootDir, file)}:${i + 1} — query lacks 'tenantId':\n` +
282
- ` ${line.trim()}`);
283
- }
284
- }
285
- }
286
- }
287
- return violations;
288
- }
289
- function checkFlywayMigrations(rootDir) {
290
- const warnings = [];
291
- const javaFiles = walkDir(rootDir, [".java"]);
292
- const entityFiles = javaFiles.filter((f) => {
293
- const content = readFileSafe(f);
294
- return content && /@Entity\b/.test(content);
295
- });
296
- if (entityFiles.length === 0)
297
- return warnings;
298
- const flywayDirs = [
299
- path.join(rootDir, "src", "main", "resources", "db", "migration"),
300
- path.join(rootDir, "src", "main", "resources", "db", "migrations"),
301
- path.join(rootDir, "resources", "db", "migration"),
302
- ];
303
- const flywayDir = flywayDirs.find((d) => fs.existsSync(d));
304
- if (!flywayDir) {
305
- warnings.push(`Found ${entityFiles.length} JPA @Entity file(s) but no Flyway migration directory detected. ` +
306
- `Expected at: src/main/resources/db/migration/`);
307
- return warnings;
308
- }
309
- const migrationFiles = fs.readdirSync(flywayDir).filter((f) => /^V\d+__.*\.sql$/i.test(f));
310
- if (migrationFiles.length === 0) {
311
- warnings.push(`Found ${entityFiles.length} JPA @Entity file(s) but no Flyway SQL migrations exist in ${flywayDir}`);
312
- }
313
- return warnings;
314
- }
315
- // ─────────────────────────────────────────────
316
115
  // MCP Server bootstrap
317
116
  // ─────────────────────────────────────────────
318
117
  const server = new McpServer({
@@ -500,201 +299,6 @@ server.tool("log_rejected_pattern", "Record a coding pattern that was rejected b
500
299
  ],
501
300
  }, inputChars);
502
301
  });
503
- // ─── Tool 3: run_pre_pr_checks ───────────────────────────────────────────────
504
- server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks across the workspace. " +
505
- "Automatically discovers modified microservices and runs native linters, database script checks, and strict tenant-isolation scanners. " +
506
- "Validates: spec checkboxes, hardcoded IPs/URLs, tenantId in Repositories, and Flyway migration coverage for JPA entities. " +
507
- "Linter/isolation errors must be fixed natively — do not suppress warnings to pass the gate. " +
508
- "MUST pass with zero errors before a PR is raised.", {
509
- work_item_id: z
510
- .string()
511
- .describe("Azure DevOps work item ID to include in the PR summary."),
512
- root_dir: z
513
- .string()
514
- .optional()
515
- .describe("Root directory to scan (defaults to cwd). Use when the source repo is in a sub-folder."),
516
- skip_checks: z
517
- .array(z.enum([
518
- "spec_boxes",
519
- "console_logs",
520
- "tenant_isolation",
521
- "hardcoded_urls",
522
- "flyway_migrations",
523
- ]))
524
- .optional()
525
- .describe("Explicitly skip specific checks. Use sparingly — document the reason in your PR description."),
526
- }, async ({ work_item_id, root_dir, skip_checks = [] }) => {
527
- const inputChars = JSON.stringify({ work_item_id, root_dir, skip_checks }).length;
528
- const scanRoot = root_dir ? path.resolve(root_dir) : getWorkspaceRoot();
529
- const errors = [];
530
- const warnings = [];
531
- const passed = [];
532
- // ── 1. Branch state check ─────────────────────────────────────────────
533
- if (!skip_checks.includes("spec_boxes")) {
534
- const branch = getCurrentBranch(scanRoot);
535
- const statePath = path.resolve(scanRoot, STATE_FILE);
536
- let branchState = null;
537
- if (fs.existsSync(statePath)) {
538
- try {
539
- const stateData = JSON.parse(readFileSafe(statePath) || "{}");
540
- branchState = stateData[branch];
541
- }
542
- catch { }
543
- }
544
- if (!branchState) {
545
- errors.push(`❌ [STATE] No branch state found for '${branch}' in \`${STATE_FILE}\`. ` +
546
- `Call \`manage_branch_state\` with action='initialize' before raising a PR.`);
547
- }
548
- else if (branchState.pending_acs && branchState.pending_acs.length > 0) {
549
- errors.push(`❌ [STATE] ${branchState.pending_acs.length} pending AC(s) remain for branch '${branch}':\n` +
550
- branchState.pending_acs.map((u) => ` • ${u}`).join("\n"));
551
- }
552
- else {
553
- passed.push(`✅ [STATE] Branch state for '${branch}' is clear (no pending ACs).`);
554
- }
555
- }
556
- else {
557
- warnings.push("⚠️ [STATE] Branch state check SKIPPED (manually bypassed).");
558
- }
559
- // ── 2. AST-level linting (Workspace Discovery → ESLint/Checkstyle → regex fallback)
560
- if (!skip_checks.includes("console_logs")) {
561
- let projectRoots = findProjectRoots(scanRoot);
562
- if (projectRoots.length === 0)
563
- projectRoots.push(scanRoot);
564
- let anyLinterAvailable = false;
565
- const allLintViolations = [];
566
- const usedLinters = new Set();
567
- for (const pRoot of projectRoots) {
568
- const eslint = runEslint(pRoot);
569
- const checkstyle = runCheckstyle(pRoot);
570
- if (eslint.available) {
571
- anyLinterAvailable = true;
572
- usedLinters.add("ESLint");
573
- allLintViolations.push(...eslint.violations);
574
- }
575
- if (checkstyle.available) {
576
- anyLinterAvailable = true;
577
- usedLinters.add("Checkstyle");
578
- allLintViolations.push(...checkstyle.violations);
579
- }
580
- }
581
- if (anyLinterAvailable) {
582
- // AST linters found and ran — they are the source of truth
583
- if (allLintViolations.length > 0) {
584
- errors.push(`❌ [LINTING] ${allLintViolations.length} linter error(s) across workspace — fix natively in their respective microservices:\n` +
585
- allLintViolations.map((v) => ` • ${v}`).join("\n"));
586
- }
587
- else {
588
- passed.push(`✅ [LINTING] AST-level linting passed across workspace (${Array.from(usedLinters).join(" + ")}).`);
589
- }
590
- }
591
- else {
592
- // No linters configured — fall back to regex scan with a warning
593
- const regexViolations = checkConsoleLogsRegex(scanRoot);
594
- if (regexViolations.length > 0) {
595
- errors.push(`❌ [LOGGING] ${regexViolations.length} prohibited log statement(s) found via regex fallback\n` +
596
- ` (No ESLint/Checkstyle config detected in any workspace project — configure a linter for AST-level accuracy):\n` +
597
- regexViolations.map((v) => ` • ${v}`).join("\n"));
598
- }
599
- else {
600
- warnings.push("⚠️ [LOGGING] No ESLint/Checkstyle config found in workspace — used regex fallback. " +
601
- "Consider adding a linter for AST-level hygiene.");
602
- }
603
- }
604
- }
605
- else {
606
- warnings.push("⚠️ [LINTING] Linter check SKIPPED (manually bypassed).");
607
- }
608
- // ── 3. Tenant Isolation Scanner ──────────────────────────────────────────
609
- if (!skip_checks.includes("tenant_isolation")) {
610
- const tenantViolations = checkTenantIsolation(scanRoot);
611
- if (tenantViolations.length > 0) {
612
- errors.push(`❌ [SECURITY] Tenant Isolation Failed: Found ${tenantViolations.length} query method(s) in *Repository.java without tenant scoping:\n` +
613
- tenantViolations.map((v) => ` • ${v}`).join("\n"));
614
- }
615
- else {
616
- passed.push("✅ [SECURITY] Tenant isolation active: All database queries correctly scoped with tenantId.");
617
- }
618
- }
619
- else {
620
- warnings.push("⚠️ [SECURITY] Tenant isolation check SKIPPED (manually bypassed).");
621
- }
622
- // ── 4. Hardcoded URL check (regex) ───────────────────────────────────────
623
- if (!skip_checks.includes("hardcoded_urls")) {
624
- const urlViolations = checkHardcodedUrls(scanRoot);
625
- if (urlViolations.length > 0) {
626
- errors.push(`❌ [SECURITY] Found ${urlViolations.length} hardcoded URL/IP(s):\n` +
627
- urlViolations.map((v) => ` • ${v}`).join("\n"));
628
- }
629
- else {
630
- passed.push("✅ [SECURITY] No hardcoded UAT/Prod IPs or environment URLs found.");
631
- }
632
- }
633
- else {
634
- warnings.push("⚠️ [SECURITY] Hardcoded URL check SKIPPED (manually bypassed).");
635
- }
636
- // ── 4. Flyway migration check ──────────────────────────────────────────
637
- if (!skip_checks.includes("flyway_migrations")) {
638
- const flywayWarnings = checkFlywayMigrations(scanRoot);
639
- if (flywayWarnings.length > 0) {
640
- errors.push(...flywayWarnings.map((w) => `❌ [FLYWAY] ${w}`));
641
- }
642
- else {
643
- passed.push("✅ [FLYWAY] Flyway migration coverage looks good.");
644
- }
645
- }
646
- else {
647
- warnings.push("⚠️ [FLYWAY] Flyway migration check SKIPPED (manually bypassed).");
648
- }
649
- // ── Surface rejected patterns as a reminder ───────────────────────────
650
- const rejectedContent = readFileSafe(resolve(REJECTED_FILE));
651
- let rejectedReminder = "";
652
- if (rejectedContent) {
653
- try {
654
- const rejected = JSON.parse(rejectedContent);
655
- if (rejected.length > 0) {
656
- rejectedReminder =
657
- `\n\n---\n### 🚫 Rejected Patterns Reminder (${rejected.length} on record)\n` +
658
- rejected.map((r) => `- [${r.category}] ${r.pattern}`).join("\n");
659
- }
660
- }
661
- catch {
662
- /* ignore parse errors */
663
- }
664
- }
665
- // ── Build final report ─────────────────────────────────────────────────
666
- const hasErrors = errors.length > 0;
667
- const timestamp = new Date().toISOString();
668
- let report;
669
- if (hasErrors) {
670
- report =
671
- `# ❌ Pre-PR Checks FAILED — Work Item #${work_item_id}\n\n` +
672
- `**Scanned directory:** \`${scanRoot}\`\n` +
673
- `**Timestamp:** ${timestamp}\n\n` +
674
- `## Errors (must fix before PR)\n\n${errors.join("\n\n")}\n\n` +
675
- (warnings.length > 0 ? `## Warnings\n\n${warnings.join("\n")}\n\n` : "") +
676
- (passed.length > 0 ? `## Passed\n\n${passed.join("\n")}\n` : "") +
677
- rejectedReminder;
678
- }
679
- else {
680
- report =
681
- `# ✅ Pre-PR Checks PASSED — Work Item #${work_item_id}\n\n` +
682
- `**Scanned directory:** \`${scanRoot}\`\n` +
683
- `**Timestamp:** ${timestamp}\n\n` +
684
- `## All Checks Passed\n\n${passed.join("\n")}\n\n` +
685
- (warnings.length > 0 ? `## Warnings\n\n${warnings.join("\n")}\n\n` : "") +
686
- `## PR Summary\n\n` +
687
- `- **Work Item:** [#${work_item_id}](https://dev.azure.com/secufusion/_workitems/edit/${work_item_id})\n` +
688
- `- **Spec:** All acceptance criteria verified ✅\n` +
689
- `- **Guardrails:** tenantId scoping, no debug logs, no hardcoded URLs, Flyway covered ✅\n` +
690
- `- **Ready to raise PR** 🚀\n` +
691
- rejectedReminder;
692
- }
693
- return appendTelemetry({
694
- content: [{ type: "text", text: report }],
695
- isError: hasErrors,
696
- }, inputChars);
697
- });
698
302
  // ─── Tool 4: manage_project_spec ─────────────────────────────────────────────
699
303
  server.tool("manage_project_spec", "Manages the .secufusion-project-spec.json file — the permanent project memory containing all service ports, repos, domains, infrastructure config, coding patterns, auth flow, and golden rules. Every session must read this before writing any code.", {
700
304
  action: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",