secufusion-mcp 1.2.2 → 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 +101 -280
  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
@@ -638,8 +638,19 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
638
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);
639
639
  }
640
640
  if (action === "read") {
641
- const full = { spec: readJson(specPath), progress: readJson(progressPath), decisions: readJson(decisionsPath), files_touched: readJson(filesPath), scenarios: readJson(scenariosPath) };
642
- 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);
643
654
  }
644
655
  if (action === "read_summary") {
645
656
  const spec = readJson(specPath) || {};
@@ -2136,7 +2147,13 @@ server.tool("prime_session", "Hyper-efficient session startup — combines Phase
2136
2147
  if (pending.length > 0) {
2137
2148
  out += `**Pending ACs:**\n${pending.map(ac => ` - [ ] ${ac}`).join("\n")}\n`;
2138
2149
  }
2139
- 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`;
2140
2157
  }
2141
2158
  else {
2142
2159
  out += `⚠️ No active task found for ${work_item_id}. Call manage_task(initialize) to create it.\n\n`;
@@ -2798,262 +2815,44 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2798
2815
  exists: fs.existsSync(abs),
2799
2816
  };
2800
2817
  });
2801
- const mechanical = [];
2818
+ let mechanical = [];
2802
2819
  const passResults = [];
2803
2820
  const reviewComments = [];
2804
2821
  let aiCost = 0;
2805
2822
  // ══════════════════════════════════════════════════════════════════════
2806
- // TIER 1 — MECHANICAL CHECKS (P1–P5)
2823
+ // TIER 1 — MECHANICAL CHECKS (P1–P5) Via Subprocess
2807
2824
  // ══════════════════════════════════════════════════════════════════════
2808
- // ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
2809
- const p1 = [];
2810
- const p1Suppressed = [];
2811
- if (!skip_checks.includes("tenant_isolation")) {
2812
- for (const f of fileRecords.filter(f => f.isRepo)) {
2813
- const queryAnnotationLines = [];
2814
- f.lines.forEach((line, idx) => {
2815
- if (/@Query\s*\(/i.test(line))
2816
- queryAnnotationLines.push(idx);
2817
- });
2818
- f.lines.forEach((line, idx) => {
2819
- const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
2820
- if (dm && !dm[1].toLowerCase().includes("tenant")) {
2821
- let suppressionReason = "";
2822
- const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === dm[0] || s.method.endsWith(`.${dm[0]}`)));
2823
- if (explicitSup)
2824
- suppressionReason = explicitSup.reason;
2825
- else {
2826
- for (let i = Math.max(0, idx - 5); i < idx; i++) {
2827
- const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2828
- if (match)
2829
- suppressionReason = match[1];
2830
- }
2831
- }
2832
- if (suppressionReason) {
2833
- p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
2834
- }
2835
- else {
2836
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2837
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
2838
- suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
2839
- }
2840
- }
2841
- });
2842
- queryAnnotationLines.forEach(ai => {
2843
- const block = f.lines.slice(ai, ai + 5).join(" ");
2844
- if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
2845
- let suppressionReason = "";
2846
- let queryMethodName = "";
2847
- const methodDeclMatch = block.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
2848
- if (methodDeclMatch)
2849
- queryMethodName = methodDeclMatch[1];
2850
- if (queryMethodName) {
2851
- const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
2852
- if (explicitSup)
2853
- suppressionReason = explicitSup.reason;
2854
- }
2855
- if (!suppressionReason) {
2856
- for (let i = Math.max(0, ai - 5); i < ai; i++) {
2857
- const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2858
- if (match)
2859
- suppressionReason = match[1];
2860
- }
2861
- }
2862
- if (suppressionReason) {
2863
- p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
2864
- }
2865
- else {
2866
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
2867
- message: `@Query at line ${ai + 1} has no tenantId filter`,
2868
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
2869
- }
2870
- }
2871
- });
2872
- }
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;
2873
2833
  }
2874
- mechanical.push(...p1);
2875
- passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
2876
- // Auto-log suppressions to decisions.json
2877
- if (p1Suppressed.length > 0) {
2878
- const tasksRoot = resolve(TASKS_DIR);
2879
- const dir = resolveTaskFolder(work_item_id, tasksRoot);
2880
- if (dir) {
2881
- const decisionsPath = path.join(dir, "decisions.json");
2882
- const decisions = readJson(decisionsPath) || [];
2883
- let modified = false;
2884
- for (const sup of p1Suppressed) {
2885
- const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
2886
- if (!decisions.some(d => d.decision === desc)) {
2887
- decisions.push({
2888
- id: decisions.length + 1,
2889
- decision: desc,
2890
- rationale: sup.reason,
2891
- logged_at: new Date().toISOString()
2892
- });
2893
- modified = true;
2894
- }
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 || [];
2895
2840
  }
2896
- if (modified)
2897
- writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
2898
- }
2899
- }
2900
- // ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
2901
- const p2 = [];
2902
- if (!skip_checks.includes("n_plus_one")) {
2903
- for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
2904
- let depth = 0, inLoop = false, loopDepth = -1;
2905
- f.lines.forEach((line, idx) => {
2906
- const o = (line.match(/\{/g) || []).length;
2907
- const c = (line.match(/\}/g) || []).length;
2908
- if (/\b(for|forEach|stream\(\)|\.map\(|\.forEach\(|while\s*\()/i.test(line) && !inLoop) {
2909
- inLoop = true;
2910
- loopDepth = depth;
2911
- }
2912
- if (inLoop) {
2913
- const rm = /\.(findById|findByIdAndTenantId|findAll|findAllBy|save|saveAll|delete|deleteById|existsById|count)\s*\(/.exec(line);
2914
- if (rm)
2915
- p2.push({ check: "N_PLUS_ONE", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2916
- message: `Repository call \`${rm[0]}\` inside a loop`,
2917
- suggestion: "Extract IDs first, then use findAllById() / saveAll() outside the loop" });
2918
- }
2919
- depth += o - c;
2920
- if (inLoop && depth <= loopDepth) {
2921
- inLoop = false;
2922
- loopDepth = -1;
2923
- }
2924
- });
2841
+ catch (e2) { }
2925
2842
  }
2843
+ tier1Passed = false;
2926
2844
  }
2927
- 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");
2928
2848
  passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
2929
- // ── P3: MISSING_INDEX ─────────────────────────────────────────────────
2930
- const p3 = [];
2931
- if (!skip_checks.includes("missing_index")) {
2932
- const queryCols = [];
2933
- for (const f of fileRecords.filter(f => f.isRepo)) {
2934
- f.lines.forEach((line, idx) => {
2935
- const dm = line.match(/(?:find|delete|count|exists)\w+By([\w]+)/);
2936
- if (dm) {
2937
- for (const p of dm[1].replace(/And|Or/g, ",").split(",")) {
2938
- const col = p.charAt(0).toLowerCase() + p.slice(1);
2939
- if (!["tenantId", "id"].includes(col))
2940
- queryCols.push({ col, file: f.file_path, line: idx + 1 });
2941
- }
2942
- }
2943
- const wm = line.match(/WHERE\s+\w+\.(\w+)\s*=/gi);
2944
- if (wm)
2945
- for (const w of wm) {
2946
- const cm = w.match(/\.(\w+)\s*=/i);
2947
- if (cm && !["tenantId", "id", "tenant_id"].includes(cm[1]))
2948
- queryCols.push({ col: cm[1], file: f.file_path, line: idx + 1 });
2949
- }
2950
- });
2951
- }
2952
- const migrationContent = fileRecords.filter(f => f.isMigration).map(f => f.content.toLowerCase()).join("\n");
2953
- if (fileRecords.some(f => f.isMigration)) {
2954
- for (const { col, file, line } of queryCols) {
2955
- const snake = col.replace(/([A-Z])/g, "_$1").toLowerCase();
2956
- if (!new RegExp(`create.*index.*(${snake}|${col.toLowerCase()})`, "i").test(migrationContent)) {
2957
- p3.push({ check: "MISSING_INDEX", severity: "WARNING", file, line,
2958
- message: `Query on column \`${col}\` has no CREATE INDEX in migration`,
2959
- suggestion: `Add: CREATE INDEX idx_<table>_${snake} ON <table>(${snake});` });
2960
- }
2961
- }
2962
- }
2963
- }
2964
- mechanical.push(...p3);
2849
+ const p3 = mechanical.filter(m => m.check === "MISSING_INDEX");
2965
2850
  passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
2966
- // ── P4: KAFKA_SYNC ────────────────────────────────────────────────────
2967
- const p4 = [];
2968
- if (!skip_checks.includes("kafka_sync")) {
2969
- for (const f of fileRecords.filter(f => f.isKafka)) {
2970
- let inListener = false, lDepth = 0;
2971
- f.lines.forEach((line, idx) => {
2972
- if (/@KafkaListener/.test(line)) {
2973
- inListener = true;
2974
- lDepth = 0;
2975
- }
2976
- if (inListener) {
2977
- lDepth += (line.match(/\{/g) || []).length;
2978
- lDepth -= (line.match(/\}/g) || []).length;
2979
- const sm = /\.(save|saveAll|findById|findAll|delete|deleteById|restTemplate|webClient|getForObject|postForObject|exchange)\s*\(/.exec(line);
2980
- if (sm) {
2981
- const isAsync = /@Async|CompletableFuture|executorService|threadPool/i.test(f.lines.slice(Math.max(0, idx - 3), idx + 1).join(" "));
2982
- if (!isAsync)
2983
- p4.push({ check: "KAFKA_SYNC", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2984
- message: `Sync call \`${sm[0]}\` inside @KafkaListener — blocks consumer thread`,
2985
- suggestion: "Offload to @Async method or a dedicated ExecutorService" });
2986
- }
2987
- if (lDepth <= 0 && lDepth !== 0)
2988
- inListener = false;
2989
- }
2990
- });
2991
- }
2992
- }
2993
- mechanical.push(...p4);
2851
+ const p4 = mechanical.filter(m => m.check === "KAFKA_SYNC");
2994
2852
  passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
2995
- // ── P5: EARLY_RETURN ──────────────────────────────────────────────────
2996
- // Algorithm: collect all return lines per method via flush-on-close.
2997
- // Every return except the last in a method body is an early return.
2998
- const p5 = [];
2999
- if (!skip_checks.includes("early_return")) {
3000
- for (const f of fileRecords.filter(f => f.isJava || f.isTS)) {
3001
- let depth = 0, methodDepth = -1, methodStart = -1, inMethod = false, mName = "";
3002
- let mReturns = [], mBody = [];
3003
- const flush = () => {
3004
- if (mReturns.length > 1) {
3005
- for (const ri of mReturns.slice(0, -1)) {
3006
- p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
3007
- message: `Early return in \`${mName}()\` — violates single-exit rule`,
3008
- suggestion: "Declare result at top, use nested if blocks, single return at method end" });
3009
- }
3010
- }
3011
- else if (mReturns.length === 1) {
3012
- const ri = mReturns[0];
3013
- 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("//"));
3014
- if (after.length > 0 && mBody.length > 0 && after[0].ai <= mBody[mBody.length - 1]) {
3015
- p5.push({ check: "EARLY_RETURN", severity: "BLOCKING", file: f.file_path, line: ri + 1,
3016
- message: `Early return in \`${mName}()\` — violates single-exit rule`,
3017
- suggestion: "Declare result at top, use nested if blocks, single return at method end" });
3018
- }
3019
- }
3020
- mReturns = [];
3021
- mBody = [];
3022
- };
3023
- f.lines.forEach((line, idx) => {
3024
- const trimmed = line.trim();
3025
- const o = (line.match(/\{/g) || []).length;
3026
- const c = (line.match(/\}/g) || []).length;
3027
- const jm = line.match(/(?:public|private|protected|static)\s+\S+\s+(\w+)\s*\(/);
3028
- const tm = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|async\s+(\w+)\s*\()/);
3029
- if ((f.isJava ? jm : tm) && !inMethod) {
3030
- inMethod = true;
3031
- methodDepth = depth;
3032
- methodStart = idx;
3033
- mReturns = [];
3034
- mBody = [];
3035
- mName = jm?.[1] || tm?.[1] || tm?.[2] || tm?.[3] || "unknown";
3036
- }
3037
- depth += o - c;
3038
- if (inMethod && idx > methodStart) {
3039
- if (trimmed.length > 0)
3040
- mBody.push(idx);
3041
- if (/\breturn\b/.test(line) && !/^\s*\/\//.test(line))
3042
- mReturns.push(idx);
3043
- if (depth <= methodDepth) {
3044
- flush();
3045
- inMethod = false;
3046
- methodDepth = -1;
3047
- }
3048
- }
3049
- });
3050
- }
3051
- }
3052
- mechanical.push(...p5);
2853
+ const p5 = mechanical.filter(m => m.check === "EARLY_RETURN");
3053
2854
  passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
3054
- // ── Tier 1 gate ────────────────────────────────────────────────────────
3055
2855
  const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
3056
- const tier1Passed = blockingMechanical.length === 0;
3057
2856
  // ══════════════════════════════════════════════════════════════════════
3058
2857
  // TIER 2 — AI FILE REVIEWER (Pass 6)
3059
2858
  // Reads each changed file, applies architectural heuristics.
@@ -4704,48 +4503,70 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
4704
4503
  }
4705
4504
  return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
4706
4505
  });
4707
- // ─── Resource & Prompt for AGENTS.md ──────────────────────────────────────────
4708
- const AGENTS_MD_PATH = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "AGENTS.md");
4709
- server.resource("secufusion_rules", "secufusion://rules", async (uri) => {
4710
- const content = readFileSafe(AGENTS_MD_PATH);
4711
- if (!content) {
4712
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
4713
- }
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}`);
4714
4516
  return {
4715
- contents: [{
4716
- uri: uri.href,
4717
- text: content
4718
- }]
4517
+ messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Planner. Here are your rules:\n\n${content}` } }]
4719
4518
  };
4720
4519
  });
4721
- server.prompt("secufusion_developer", "Starts a conversation with the SecuFusion developer workflow rules loaded.", async () => {
4722
- const content = readFileSafe(AGENTS_MD_PATH);
4723
- if (!content) {
4724
- throw new Error(`Rules file not found at ${AGENTS_MD_PATH}`);
4725
- }
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}`);
4726
4524
  return {
4727
- messages: [
4728
- {
4729
- role: "user",
4730
- content: {
4731
- type: "text",
4732
- text: `You are now the SecuFusion Senior Architect. Here are your rules:\n\n${content}`
4733
- }
4734
- }
4735
- ]
4525
+ messages: [{ role: "user", content: { type: "text", text: `You are now the SecuFusion Coder. Here are your rules:\n\n${content}` } }]
4736
4526
  };
4737
4527
  });
4738
- // ─────────────────────────────────────────────
4739
- // Start transport
4740
- // ─────────────────────────────────────────────
4741
- server.tool("get_secufusion_rules", "Fetches the mandatory AGENTS.md workflow rules for the SecuFusion project. Call this before starting any work.", {}, async () => {
4742
- const content = readFileSafe(AGENTS_MD_PATH);
4743
- if (!content) {
4744
- 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);
4745
4569
  }
4746
- return appendTelemetry({
4747
- content: [{ type: "text", text: content }]
4748
- }, 0);
4749
4570
  });
4750
4571
  const transport = new StdioServerTransport();
4751
4572
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.2.2",
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",