secufusion-mcp 1.2.1 → 1.2.3

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 (4) hide show
  1. package/AGENTS.md +9 -10
  2. package/README.md +13 -0
  3. package/index.js +115 -286
  4. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ You are an elite Senior Developer and Architect working on the SecuFusion worksp
9
9
 
10
10
  **DYNAMIC ENFORCEMENT**: You must dynamically adhere to these rules at all times. Whether you are starting a fresh task, resuming an interrupted session, or answering a mid-task prompt, you must strictly respect this sequence and never skip ahead.
11
11
 
12
- 1. **Rule 0 (Pre-requisite to all rules) — Load Project DNA First**: Before ANY other action, call `manage_project_spec(action: "read")` then `manage_project_spec(action: "get_golden_rules")`. This is Phase 00. Without the project DNA loaded, you are not allowed to reason, classify, plan, or code. Period.
12
+ 1. **Rule 0 (Pre-requisite to all rules) — Load Project DNA First**: Before ANY other action, call `prime_session(work_item_id: <id>)`. This is Phase 00. Without the project DNA loaded, you are not allowed to reason, classify, plan, or code. Period.
13
13
  2. **Rule 1 - ReAct (Reason, Observe, Act) First**: After DNA is loaded, you MUST deeply reason about the problem statement. Apply the ReAct framework: analyze the problem, observe context, and formulate a high-level solution hypothesis.
14
14
  3. **Rule 2 - Classify Second**: Only after you have reasoned through the problem statement, you MUST call the `classify_task` tool. This will formally categorize the task and lock in architectural boundaries.
15
15
  4. **Rule 3 - STRICT YIELD (Stop and Wait)**: Immediately after classifying the task, you MUST YIELD YOUR TURN. **DO NOT CHAIN TOOL CALLS.** Output the classification, ask the user for the "green signal," and STOP.
@@ -29,15 +29,11 @@ You are an elite Senior Developer and Architect working on the SecuFusion worksp
29
29
 
30
30
  ```
31
31
  STEP 1 — ALWAYS, unconditionally:
32
- → call manage_project_spec(action: "read")
33
- This loads ALL microservices, ports, repos, Kafka topics, auth flow,
34
- table ownership, Keycloak config, and inter-service calls into context.
32
+ → call prime_session(work_item_id: <active_id>)
33
+ This loads ONLY the relevant microservices, ports, repos, Kafka topics,
34
+ table ownership, golden rules, and task progress into your context using Thin Indexes.
35
35
 
36
- STEP 2 — ALWAYS, unconditionally:
37
- → call manage_project_spec(action: "get_golden_rules")
38
- This loads all non-negotiable architectural guardrails.
39
-
40
- STEP 3 — if you are working on a specific service:
36
+ STEP 2 — if you need more details about a specific service not returned by prime_session:
41
37
  → call manage_project_spec(action: "get_service", service_name: <that service>)
42
38
  ```
43
39
 
@@ -72,7 +68,7 @@ There are NO circumstances under which Phase 00 can be skipped, abbreviated, or
72
68
  ❌ Write any code
73
69
  ❌ Proceed to Phase 0.5 or any other phase
74
70
 
75
- The ONLY tool calls permitted before Phase 00 completes are the `manage_project_spec` calls listed above.
71
+ The ONLY tool calls permitted before Phase 00 completes are `prime_session` and `manage_project_spec`.
76
72
 
77
73
  ---
78
74
 
@@ -351,6 +347,9 @@ STEP 3: if implementing anything similar to a past feature:
351
347
  call get_pattern_from_task(work_item_id: <matching id>)
352
348
  — extract reusable patterns
353
349
 
350
+ STEP 3.5: call skill_recommend(query: <task description>)
351
+ — ALWAYS. Retrieve domain-specific skills and patterns from the skill catalog.
352
+
354
353
  STEP 4: call manage_task(action: "initialize",
355
354
  work_item_id: ...,
356
355
  title: ...,
package/README.md CHANGED
@@ -25,6 +25,19 @@
25
25
  | `run_pre_pr_checks_with_reviewer_agent` | **Phase 4** — PR Handoff | **NEW** — Unified 3-tier PR gate. Runs mechanical checks, AI file reviews, and context-aware task evaluation in a single pass. |
26
26
  | `get_secufusion_rules` | **Setup** | Returns the `AGENTS.md` rules for AI clients that don't natively support MCP Resources |
27
27
  | `classify_task` | **Phase 0.5** — Task Classification | **NEW** — Deep multi-pass analysis engine. Classifies any task as `BACKEND_ONLY`, `FRONTEND_ONLY`, `FULL_STACK`, or `EXTENSION_ONLY` based on root cause (where the fix lives), not surface symptoms. Must be the first tool called on any task. |
28
+ | `prime_session` | **Phase 0** — Session Start | **NEW** — Hyper-efficient session startup. Combines Phase 00 (spec) and Phase 0 (task) into one call using Thin Indexes to optimize context tokens. |
29
+ | `skill_recommend` | **Phase 1** — Planning | **NEW** — Dynamically recommends and retrieves domain-specific coding skills from `skills-catalog.json`. |
30
+
31
+ ---
32
+
33
+ ## 🚀 What's New: MLMCPS Architecture Alignment
34
+
35
+ The SecuFusion MCP has been refactored to align with the advanced MLMCPS framework principles, bringing massive efficiency and UX improvements:
36
+
37
+ 1. **Persona-Driven Slash Commands**: The monolithic agent prompt has been split. You can now use `/sfn_spec`, `/sfn_code`, and `/sfn_review` in your AI chat to instantly summon the Planner, Coder, or Reviewer persona, ensuring razor-sharp focus per phase.
38
+ 2. **Subprocess PR Checks**: Tier 1 mechanical checks (`TENANT_ISOLATION`, `N_PLUS_ONE`, etc.) are now extracted into a standalone CLI script (`scripts/sfn-pr-check.js`), allowing them to be run by the AI *or* natively within your CI/CD pipelines.
39
+ 3. **Thin Index Token Discipline**: Context bloat is gone. Tools like `manage_task(read)` and `prime_session` now return lightweight "Thin Indexes"—compact Markdown summaries with absolute file paths—so the AI only reads the full JSON via `view_file` when truly necessary.
40
+ 4. **Dynamic Skill Registry**: A new `skill_recommend` tool allows the AI to dynamically discover domain-specific architectural skills without bloating the base prompt.
28
41
 
29
42
  ---
30
43
 
package/index.js CHANGED
@@ -524,24 +524,32 @@ function generateNewTaskFolder(workItemId, title, tasksRoot) {
524
524
  let numericId = workItemId.replace(/\D/g, ''); // Extract just the digits
525
525
  if (!numericId)
526
526
  numericId = workItemId; // fallback
527
- let taskTypeStr = "task";
527
+ let taskTypeStr = "Task";
528
528
  // Try to load the classification to get the true task_type
529
529
  const classifPath = path.join(tasksRoot, '..', 'classifications', `${workItemId}.json`);
530
530
  const classif = readJson(classifPath);
531
531
  if (classif && typeof classif.task_type === 'string') {
532
532
  const rawType = classif.task_type.toLowerCase();
533
533
  if (rawType === 'user_story')
534
- taskTypeStr = 'story';
534
+ taskTypeStr = 'US';
535
+ else if (rawType === 'bug')
536
+ taskTypeStr = 'Bug';
535
537
  else
536
- taskTypeStr = rawType;
538
+ taskTypeStr = rawType.charAt(0).toUpperCase() + rawType.slice(1);
537
539
  }
538
540
  else {
539
541
  // Fallback: guess from the workItemId string itself if passed as "BUG-1173"
540
542
  const prefixMatch = workItemId.match(/^([a-zA-Z]+)-/);
541
543
  if (prefixMatch) {
542
- taskTypeStr = prefixMatch[1].toLowerCase();
543
- if (taskTypeStr === 'wi')
544
- taskTypeStr = 'task'; // Generic WI to task
544
+ const lowerPrefix = prefixMatch[1].toLowerCase();
545
+ if (lowerPrefix === 'wi')
546
+ taskTypeStr = 'Task';
547
+ else if (lowerPrefix === 'bug')
548
+ taskTypeStr = 'Bug';
549
+ else if (lowerPrefix === 'us')
550
+ taskTypeStr = 'US';
551
+ else
552
+ taskTypeStr = lowerPrefix.charAt(0).toUpperCase() + lowerPrefix.slice(1);
545
553
  }
546
554
  }
547
555
  return path.join(tasksRoot, `${taskTypeStr}-${numericId}-${slug}`);
@@ -630,8 +638,19 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
630
638
  return appendTelemetry({ isError: true, content: [{ type: "text", text: `ERROR: Task WI-${work_item_id} not found. Call action=initialize first.\n\nExpected folder: ${dir}` }] }, inputChars);
631
639
  }
632
640
  if (action === "read") {
633
- const full = { spec: readJson(specPath), progress: readJson(progressPath), decisions: readJson(decisionsPath), files_touched: readJson(filesPath), scenarios: readJson(scenariosPath) };
634
- return appendTelemetry({ content: [{ type: "text", text: JSON.stringify(full, null, 2) }] }, inputChars);
641
+ const specObj = readJson(specPath) || {};
642
+ const decisions = readJson(decisionsPath) || [];
643
+ const files = readJson(filesPath) || [];
644
+ const scenarios = readJson(scenariosPath) || [];
645
+ let out = `## Task ${work_item_id} (Thin Index)\n\n`;
646
+ out += `**Title:** ${specObj.title || 'unknown'}\n\n`;
647
+ out += `To optimize context, full task JSON is not returned. Use \`view_file\` on the following absolute paths if you need details:\n`;
648
+ out += `- **Spec**: ${specPath}\n`;
649
+ out += `- **Progress**: ${progressPath}\n`;
650
+ out += `- **Decisions** (${decisions.length}): ${decisionsPath}\n`;
651
+ out += `- **Files Touched** (${files.length}): ${filesPath}\n`;
652
+ out += `- **Scenarios** (${scenarios.length}): ${scenariosPath}\n`;
653
+ return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
635
654
  }
636
655
  if (action === "read_summary") {
637
656
  const spec = readJson(specPath) || {};
@@ -2128,7 +2147,13 @@ server.tool("prime_session", "Hyper-efficient session startup — combines Phase
2128
2147
  if (pending.length > 0) {
2129
2148
  out += `**Pending ACs:**\n${pending.map(ac => ` - [ ] ${ac}`).join("\n")}\n`;
2130
2149
  }
2131
- out += "\n";
2150
+ out += "\n### Artifact Pointers\n";
2151
+ out += `Use \`view_file\` on these absolute paths to read full task JSON data:\n`;
2152
+ out += `- **Spec**: ${path.join(taskDir, "spec.json")}\n`;
2153
+ out += `- **Progress**: ${path.join(taskDir, "progress.json")}\n`;
2154
+ out += `- **Decisions**: ${path.join(taskDir, "decisions.json")}\n`;
2155
+ out += `- **Files Touched**: ${path.join(taskDir, "files-touched.json")}\n`;
2156
+ out += `- **Scenarios**: ${path.join(taskDir, "scenarios.json")}\n\n`;
2132
2157
  }
2133
2158
  else {
2134
2159
  out += `⚠️ No active task found for ${work_item_id}. Call manage_task(initialize) to create it.\n\n`;
@@ -2790,262 +2815,44 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2790
2815
  exists: fs.existsSync(abs),
2791
2816
  };
2792
2817
  });
2793
- const mechanical = [];
2818
+ let mechanical = [];
2794
2819
  const passResults = [];
2795
2820
  const reviewComments = [];
2796
2821
  let aiCost = 0;
2797
2822
  // ══════════════════════════════════════════════════════════════════════
2798
- // TIER 1 — MECHANICAL CHECKS (P1–P5)
2823
+ // TIER 1 — MECHANICAL CHECKS (P1–P5) Via Subprocess
2799
2824
  // ══════════════════════════════════════════════════════════════════════
2800
- // ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
2801
- const p1 = [];
2802
- const p1Suppressed = [];
2803
- if (!skip_checks.includes("tenant_isolation")) {
2804
- for (const f of fileRecords.filter(f => f.isRepo)) {
2805
- const queryAnnotationLines = [];
2806
- f.lines.forEach((line, idx) => {
2807
- if (/@Query\s*\(/i.test(line))
2808
- queryAnnotationLines.push(idx);
2809
- });
2810
- f.lines.forEach((line, idx) => {
2811
- const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
2812
- if (dm && !dm[1].toLowerCase().includes("tenant")) {
2813
- let suppressionReason = "";
2814
- const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === dm[0] || s.method.endsWith(`.${dm[0]}`)));
2815
- if (explicitSup)
2816
- suppressionReason = explicitSup.reason;
2817
- else {
2818
- for (let i = Math.max(0, idx - 5); i < idx; i++) {
2819
- const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2820
- if (match)
2821
- suppressionReason = match[1];
2822
- }
2823
- }
2824
- if (suppressionReason) {
2825
- p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
2826
- }
2827
- else {
2828
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2829
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
2830
- suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
2831
- }
2832
- }
2833
- });
2834
- queryAnnotationLines.forEach(ai => {
2835
- const block = f.lines.slice(ai, ai + 5).join(" ");
2836
- if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
2837
- let suppressionReason = "";
2838
- let queryMethodName = "";
2839
- const methodDeclMatch = block.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
2840
- if (methodDeclMatch)
2841
- queryMethodName = methodDeclMatch[1];
2842
- if (queryMethodName) {
2843
- const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
2844
- if (explicitSup)
2845
- suppressionReason = explicitSup.reason;
2846
- }
2847
- if (!suppressionReason) {
2848
- for (let i = Math.max(0, ai - 5); i < ai; i++) {
2849
- const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2850
- if (match)
2851
- suppressionReason = match[1];
2852
- }
2853
- }
2854
- if (suppressionReason) {
2855
- p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
2856
- }
2857
- else {
2858
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
2859
- message: `@Query at line ${ai + 1} has no tenantId filter`,
2860
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
2861
- }
2862
- }
2863
- });
2864
- }
2825
+ let tier1Passed = false;
2826
+ try {
2827
+ const scriptPath = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "scripts/sfn-pr-check.js");
2828
+ const execResult = require('child_process').execFileSync('node', [scriptPath, '--task', work_item_id, '--workspace', wsRoot], { encoding: 'utf-8' });
2829
+ const jsonStr = execResult.trim().split('\n').pop() || "{}";
2830
+ const parsed = JSON.parse(jsonStr);
2831
+ mechanical = parsed.mechanical || [];
2832
+ tier1Passed = parsed.pass === true;
2865
2833
  }
2866
- mechanical.push(...p1);
2867
- passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
2868
- // Auto-log suppressions to decisions.json
2869
- if (p1Suppressed.length > 0) {
2870
- const tasksRoot = resolve(TASKS_DIR);
2871
- const dir = resolveTaskFolder(work_item_id, tasksRoot);
2872
- if (dir) {
2873
- const decisionsPath = path.join(dir, "decisions.json");
2874
- const decisions = readJson(decisionsPath) || [];
2875
- let modified = false;
2876
- for (const sup of p1Suppressed) {
2877
- const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
2878
- if (!decisions.some(d => d.decision === desc)) {
2879
- decisions.push({
2880
- id: decisions.length + 1,
2881
- decision: desc,
2882
- rationale: sup.reason,
2883
- logged_at: new Date().toISOString()
2884
- });
2885
- modified = true;
2886
- }
2834
+ catch (e) {
2835
+ if (e.stdout) {
2836
+ const jsonStr = e.stdout.trim().split('\n').pop() || "{}";
2837
+ try {
2838
+ const parsed = JSON.parse(jsonStr);
2839
+ mechanical = parsed.mechanical || [];
2887
2840
  }
2888
- if (modified)
2889
- writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
2890
- }
2891
- }
2892
- // ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
2893
- const p2 = [];
2894
- if (!skip_checks.includes("n_plus_one")) {
2895
- for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
2896
- let depth = 0, inLoop = false, loopDepth = -1;
2897
- f.lines.forEach((line, idx) => {
2898
- const o = (line.match(/\{/g) || []).length;
2899
- const c = (line.match(/\}/g) || []).length;
2900
- if (/\b(for|forEach|stream\(\)|\.map\(|\.forEach\(|while\s*\()/i.test(line) && !inLoop) {
2901
- inLoop = true;
2902
- loopDepth = depth;
2903
- }
2904
- if (inLoop) {
2905
- const rm = /\.(findById|findByIdAndTenantId|findAll|findAllBy|save|saveAll|delete|deleteById|existsById|count)\s*\(/.exec(line);
2906
- if (rm)
2907
- p2.push({ check: "N_PLUS_ONE", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2908
- message: `Repository call \`${rm[0]}\` inside a loop`,
2909
- suggestion: "Extract IDs first, then use findAllById() / saveAll() outside the loop" });
2910
- }
2911
- depth += o - c;
2912
- if (inLoop && depth <= loopDepth) {
2913
- inLoop = false;
2914
- loopDepth = -1;
2915
- }
2916
- });
2841
+ catch (e2) { }
2917
2842
  }
2843
+ tier1Passed = false;
2918
2844
  }
2919
- mechanical.push(...p2);
2845
+ const p1 = mechanical.filter(m => m.check === "TENANT_ISOLATION");
2846
+ passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
2847
+ const p2 = mechanical.filter(m => m.check === "N_PLUS_ONE");
2920
2848
  passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
2921
- // ── P3: MISSING_INDEX ─────────────────────────────────────────────────
2922
- const p3 = [];
2923
- if (!skip_checks.includes("missing_index")) {
2924
- const queryCols = [];
2925
- for (const f of fileRecords.filter(f => f.isRepo)) {
2926
- f.lines.forEach((line, idx) => {
2927
- const dm = line.match(/(?:find|delete|count|exists)\w+By([\w]+)/);
2928
- if (dm) {
2929
- for (const p of dm[1].replace(/And|Or/g, ",").split(",")) {
2930
- const col = p.charAt(0).toLowerCase() + p.slice(1);
2931
- if (!["tenantId", "id"].includes(col))
2932
- queryCols.push({ col, file: f.file_path, line: idx + 1 });
2933
- }
2934
- }
2935
- const wm = line.match(/WHERE\s+\w+\.(\w+)\s*=/gi);
2936
- if (wm)
2937
- for (const w of wm) {
2938
- const cm = w.match(/\.(\w+)\s*=/i);
2939
- if (cm && !["tenantId", "id", "tenant_id"].includes(cm[1]))
2940
- queryCols.push({ col: cm[1], file: f.file_path, line: idx + 1 });
2941
- }
2942
- });
2943
- }
2944
- const migrationContent = fileRecords.filter(f => f.isMigration).map(f => f.content.toLowerCase()).join("\n");
2945
- if (fileRecords.some(f => f.isMigration)) {
2946
- for (const { col, file, line } of queryCols) {
2947
- const snake = col.replace(/([A-Z])/g, "_$1").toLowerCase();
2948
- if (!new RegExp(`create.*index.*(${snake}|${col.toLowerCase()})`, "i").test(migrationContent)) {
2949
- p3.push({ check: "MISSING_INDEX", severity: "WARNING", file, line,
2950
- message: `Query on column \`${col}\` has no CREATE INDEX in migration`,
2951
- suggestion: `Add: CREATE INDEX idx_<table>_${snake} ON <table>(${snake});` });
2952
- }
2953
- }
2954
- }
2955
- }
2956
- mechanical.push(...p3);
2849
+ const p3 = mechanical.filter(m => m.check === "MISSING_INDEX");
2957
2850
  passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
2958
- // ── P4: KAFKA_SYNC ────────────────────────────────────────────────────
2959
- const p4 = [];
2960
- if (!skip_checks.includes("kafka_sync")) {
2961
- for (const f of fileRecords.filter(f => f.isKafka)) {
2962
- let inListener = false, lDepth = 0;
2963
- f.lines.forEach((line, idx) => {
2964
- if (/@KafkaListener/.test(line)) {
2965
- inListener = true;
2966
- lDepth = 0;
2967
- }
2968
- if (inListener) {
2969
- lDepth += (line.match(/\{/g) || []).length;
2970
- lDepth -= (line.match(/\}/g) || []).length;
2971
- const sm = /\.(save|saveAll|findById|findAll|delete|deleteById|restTemplate|webClient|getForObject|postForObject|exchange)\s*\(/.exec(line);
2972
- if (sm) {
2973
- const isAsync = /@Async|CompletableFuture|executorService|threadPool/i.test(f.lines.slice(Math.max(0, idx - 3), idx + 1).join(" "));
2974
- if (!isAsync)
2975
- p4.push({ check: "KAFKA_SYNC", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2976
- message: `Sync call \`${sm[0]}\` inside @KafkaListener — blocks consumer thread`,
2977
- suggestion: "Offload to @Async method or a dedicated ExecutorService" });
2978
- }
2979
- if (lDepth <= 0 && lDepth !== 0)
2980
- inListener = false;
2981
- }
2982
- });
2983
- }
2984
- }
2985
- mechanical.push(...p4);
2851
+ const p4 = mechanical.filter(m => m.check === "KAFKA_SYNC");
2986
2852
  passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
2987
- // ── P5: EARLY_RETURN ──────────────────────────────────────────────────
2988
- // Algorithm: collect all return lines per method via flush-on-close.
2989
- // Every return except the last in a method body is an early return.
2990
- const p5 = [];
2991
- if (!skip_checks.includes("early_return")) {
2992
- for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
2993
- let depth = 0, methodDepth = -1, methodStart = -1, inMethod = false, mName = "";
2994
- let mReturns = [], mBody = [];
2995
- const flush = () => {
2996
- if (mReturns.length > 1) {
2997
- for (const ri of mReturns.slice(0, -1)) {
2998
- p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
2999
- message: `Early return in \`${mName}()\` — violates single-exit rule`,
3000
- suggestion: "Declare result at top, use nested if blocks, single return at method end" });
3001
- }
3002
- }
3003
- else if (mReturns.length === 1) {
3004
- const ri = mReturns[0];
3005
- const after = f.lines.slice(ri + 1).map((l, o) => ({ l: l.trim(), ai: ri + 1 + o })).filter(({ l }) => l.length > 0 && l !== "}" && l !== "};" && !l.startsWith("//"));
3006
- if (after.length > 0 && mBody.length > 0 && after[0].ai <= mBody[mBody.length - 1]) {
3007
- p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
3008
- message: `Early return in \`${mName}()\` — violates single-exit rule`,
3009
- suggestion: "Declare result at top, use nested if blocks, single return at method end" });
3010
- }
3011
- }
3012
- mReturns = [];
3013
- mBody = [];
3014
- };
3015
- f.lines.forEach((line, idx) => {
3016
- const trimmed = line.trim();
3017
- const o = (line.match(/\{/g) || []).length;
3018
- const c = (line.match(/\}/g) || []).length;
3019
- const jm = line.match(/(?:public|private|protected|static)\s+\S+\s+(\w+)\s*\(/);
3020
- const tm = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|async\s+(\w+)\s*\()/);
3021
- if ((f.isJava ? jm : tm) && !inMethod) {
3022
- inMethod = true;
3023
- methodDepth = depth;
3024
- methodStart = idx;
3025
- mReturns = [];
3026
- mBody = [];
3027
- mName = jm?.[1] || tm?.[1] || tm?.[2] || tm?.[3] || "unknown";
3028
- }
3029
- depth += o - c;
3030
- if (inMethod && idx > methodStart) {
3031
- if (trimmed.length > 0)
3032
- mBody.push(idx);
3033
- if (/\breturn\b/.test(line) && !/^\s*\/\//.test(line))
3034
- mReturns.push(idx);
3035
- if (depth <= methodDepth) {
3036
- flush();
3037
- inMethod = false;
3038
- methodDepth = -1;
3039
- }
3040
- }
3041
- });
3042
- }
3043
- }
3044
- mechanical.push(...p5);
2853
+ const p5 = mechanical.filter(m => m.check === "EARLY_RETURN");
3045
2854
  passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
3046
- // ── Tier 1 gate ────────────────────────────────────────────────────────
3047
2855
  const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
3048
- const tier1Passed = blockingMechanical.length === 0;
3049
2856
  // ══════════════════════════════════════════════════════════════════════
3050
2857
  // TIER 2 — AI FILE REVIEWER (Pass 6)
3051
2858
  // Reads each changed file, applies architectural heuristics.
@@ -4696,48 +4503,70 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
4696
4503
  }
4697
4504
  return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
4698
4505
  });
4699
- // ─── Resource & Prompt for AGENTS.md ──────────────────────────────────────────
4700
- const AGENTS_MD_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "AGENTS.md");
4701
- server.resource("secufusion_rules", "secufusion://rules", async (uri) => {
4702
- const content = readFileSafe(AGENTS_MD_PATH);
4703
- if (!content) {
4704
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
4705
- }
4506
+ // ─── Resource & Prompt for Personas ──────────────────────────────────────────
4507
+ const AGENTS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "../.agents");
4508
+ const PLANNER_MD_PATH = path.join(AGENTS_DIR, "planner.md");
4509
+ const CODER_MD_PATH = path.join(AGENTS_DIR, "coder.md");
4510
+ const REVIEWER_MD_PATH = path.join(AGENTS_DIR, "reviewer.md");
4511
+ const SKILLS_CATALOG_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "skills-catalog.json");
4512
+ server.prompt("secufusion_planner", "Starts a conversation with the SecuFusion planner workflow rules loaded (Phase 00 - Phase 1).", async () => {
4513
+ const content = readFileSafe(PLANNER_MD_PATH);
4514
+ if (!content)
4515
+ throw new Error(`Rules file not found at ${PLANNER_MD_PATH}`);
4706
4516
  return {
4707
- contents: [{
4708
- uri: uri.href,
4709
- text: content
4710
- }]
4517
+ messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Planner. Here are your rules:\n\n${content}` } }]
4711
4518
  };
4712
4519
  });
4713
- server.prompt("secufusion_developer", "Starts a conversation with the SecuFusion developer workflow rules loaded.", async () => {
4714
- const content = readFileSafe(AGENTS_MD_PATH);
4715
- if (!content) {
4716
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
4717
- }
4520
+ server.prompt("secufusion_coder", "Starts a conversation with the SecuFusion coder workflow rules loaded (Phase 2 - Phase 3).", async () => {
4521
+ const content = readFileSafe(CODER_MD_PATH);
4522
+ if (!content)
4523
+ throw new Error(`Rules file not found at ${CODER_MD_PATH}`);
4718
4524
  return {
4719
- messages: [
4720
- {
4721
- role: "user",
4722
- content: {
4723
- type: "text",
4724
- text: `You are now the SecuFusion Senior Architect. Here are your rules:\n\n${content}`
4725
- }
4726
- }
4727
- ]
4525
+ messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Coder. Here are your rules:\n\n${content}` } }]
4728
4526
  };
4729
4527
  });
4730
- // ─────────────────────────────────────────────
4731
- // Start transport
4732
- // ─────────────────────────────────────────────
4733
- server.tool("get_secufusion_rules", "Fetches the mandatory AGENTS.md workflow rules for the SecuFusion project. Call this before starting any work.", {}, async () => {
4734
- const content = readFileSafe(AGENTS_MD_PATH);
4735
- if (!content) {
4736
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
4528
+ server.prompt("secufusion_reviewer", "Starts a conversation with the SecuFusion reviewer workflow rules loaded (Phase 4 - Phase 5).", async () => {
4529
+ const content = readFileSafe(REVIEWER_MD_PATH);
4530
+ if (!content)
4531
+ throw new Error(`Rules file not found at ${REVIEWER_MD_PATH}`);
4532
+ return {
4533
+ messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Reviewer. Here are your rules:\n\n${content}` } }]
4534
+ };
4535
+ });
4536
+ // ─── Slash Commands (Aliases for Personas / Workflows) ───────────────────────
4537
+ server.prompt("sfn_spec", "Slash command to start Phase 0.5 (Classification) and Phase 1 (Planning)", async () => {
4538
+ const content = readFileSafe(PLANNER_MD_PATH);
4539
+ return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_spec]\nYou are in Spec/Planning mode. Execute Phase 00, Phase 0.5, and Phase 1 rules:\n\n${content}` } }] };
4540
+ });
4541
+ server.prompt("sfn_code", "Slash command to start Phase 2 (Execution) and Phase 3 (Course Correction)", async () => {
4542
+ const content = readFileSafe(CODER_MD_PATH);
4543
+ return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_code]\nYou are in Coder mode. Execute Phase 2 and Phase 3 rules:\n\n${content}` } }] };
4544
+ });
4545
+ server.prompt("sfn_review", "Slash command to start Phase 4 (PR Checks) and Phase 5 (Retrospective)", async () => {
4546
+ const content = readFileSafe(REVIEWER_MD_PATH);
4547
+ return { messages: [{ role: "user", content: { type: "text", text: `[Command: /sfn_review]\nYou are in Review mode. Execute Phase 4 and Phase 5 rules:\n\n${content}` } }] };
4548
+ });
4549
+ // ─── Tools ─────────────────────────────────────────────
4550
+ server.tool("skill_recommend", "Searches the skill catalog for matching domain skills based on keywords.", {
4551
+ keywords: z.array(z.string()).describe("Keywords to search for (e.g., 'kafka', 'react')")
4552
+ }, async ({ keywords }) => {
4553
+ const content = readFileSafe(SKILLS_CATALOG_PATH);
4554
+ if (!content)
4555
+ return appendTelemetry({ content: [{ type: "text", text: "No skills catalog found." }] }, 0);
4556
+ try {
4557
+ const catalog = JSON.parse(content);
4558
+ const matches = catalog.skills.filter((skill) => keywords.some(kw => skill.tags.includes(kw.toLowerCase())));
4559
+ if (matches.length === 0)
4560
+ return appendTelemetry({ content: [{ type: "text", text: "No matching skills found." }] }, content.length);
4561
+ let out = "Found the following relevant skills:\n";
4562
+ matches.forEach((s) => {
4563
+ out += `- **${s.name}**: ${s.description}\n *Read file:* \`${s.path}\`\n`;
4564
+ });
4565
+ return appendTelemetry({ content: [{ type: "text", text: out }] }, content.length);
4566
+ }
4567
+ catch (e) {
4568
+ return appendTelemetry({ content: [{ type: "text", text: "Error parsing skills catalog." }] }, 0);
4737
4569
  }
4738
- return appendTelemetry({
4739
- content: [{ type: "text", text: content }]
4740
- }, 0);
4741
4570
  });
4742
4571
  const transport = new StdioServerTransport();
4743
4572
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",