secufusion-mcp 1.2.7 → 2.0.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.
@@ -20,15 +20,31 @@ import { z } from "zod";
20
20
  import fs from "fs";
21
21
  import path from "path";
22
22
  import { execSync } from "child_process";
23
+ import { parseDomainModels } from "./parsers/domain.js";
24
+ import { parseApiEndpoints } from "./parsers/api.js";
25
+ import { parseEventTopics } from "./parsers/events.js";
26
+ import { inferPatterns } from "./parsers/patterns.js";
27
+ import { parseFrontend } from "./parsers/frontend.js";
28
+ import { parseNodeApis } from "./parsers/backend-node.js";
29
+ import { parseInfra } from "./parsers/infra.js";
30
+ import { parseDatabase } from "./parsers/database.js";
31
+ import { parseSecurity } from "./parsers/security.js";
32
+ import { parseConfig, parseExternalEnv } from "./parsers/config.js";
33
+ import chokidar from "chokidar";
23
34
  // ─────────────────────────────────────────────
24
35
  // Constants / helpers
25
36
  // ─────────────────────────────────────────────
26
37
  const STATE_FILE = ".secufusion-state.json";
27
38
  const REJECTED_FILE = ".rejected-patterns.json";
28
39
  const TOKEN_FILE = ".secufusion-tokens.json";
40
+ const argv = process.argv.slice(2);
41
+ const rootFlag = argv.indexOf('--root');
42
+ const ROOT = rootFlag !== -1
43
+ ? path.resolve(argv[rootFlag + 1] ?? '.')
44
+ : process.cwd();
29
45
  /** Find the workspace root by walking up from cwd to find a project marker. */
30
46
  function getWorkspaceRoot() {
31
- let currentDir = process.cwd();
47
+ let currentDir = ROOT;
32
48
  const rootMarkers = [".git", "package.json", "pom.xml", ".secufusion-project-spec.json", ".agents"];
33
49
  for (let i = 0; i < 10; i++) {
34
50
  for (const marker of rootMarkers) {
@@ -41,7 +57,7 @@ function getWorkspaceRoot() {
41
57
  break; // Reached filesystem root
42
58
  currentDir = parent;
43
59
  }
44
- return process.cwd(); // Fallback to current directory
60
+ return ROOT; // Fallback to ROOT directory
45
61
  }
46
62
  /** Resolve a path relative to workspace root (where the MCP server is invoked). */
47
63
  function resolve(file) {
@@ -2708,7 +2724,8 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2708
2724
  }, async ({ work_item_id, skip_checks = [], suppressions = [] }) => {
2709
2725
  try {
2710
2726
  const cp = require('child_process');
2711
- const scriptPath = require('path').resolve(__dirname, 'scripts/sfn-pr-check.js');
2727
+ const __dirname = require('url').fileURLToPath(new URL('.', import.meta.url));
2728
+ const scriptPath = require('path').resolve(__dirname, '../../scripts/sfn-pr-check.js');
2712
2729
  const args = JSON.stringify({ work_item_id, skip_checks, suppressions });
2713
2730
  const output = cp.execFileSync(process.execPath, [scriptPath, args], { encoding: 'utf-8' });
2714
2731
  return { content: [{ type: 'text', text: output }] };
@@ -3937,7 +3954,7 @@ server.tool("reviewer_agent", "Context-aware architectural review agent — call
3937
3954
  return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
3938
3955
  });
3939
3956
  // ─── Resource & Prompt for Personas ──────────────────────────────────────────
3940
- const AGENTS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "../.agents");
3957
+ const AGENTS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "../../agents");
3941
3958
  const PLANNER_MD_PATH = path.join(AGENTS_DIR, "planner.md");
3942
3959
  const CODER_MD_PATH = path.join(AGENTS_DIR, "coder.md");
3943
3960
  const REVIEWER_MD_PATH = path.join(AGENTS_DIR, "reviewer.md");
@@ -4001,5 +4018,392 @@ server.tool("skill_recommend", "Searches the skill catalog for matching domain s
4001
4018
  return appendTelemetry({ content: [{ type: "text", text: "Error parsing skills catalog." }] }, 0);
4002
4019
  }
4003
4020
  });
4021
+ let watcher = null;
4022
+ function getEcosystemRoot(workspaceRoot) {
4023
+ const parentDir = path.dirname(workspaceRoot);
4024
+ if (fs.existsSync(parentDir)) {
4025
+ const parentChildren = fs.readdirSync(parentDir);
4026
+ const sfnRepos = parentChildren.filter((c) => c.startsWith("sfn-") || c === "secufusion");
4027
+ if (sfnRepos.length > 1) {
4028
+ return parentDir;
4029
+ }
4030
+ }
4031
+ return workspaceRoot;
4032
+ }
4033
+ const getDnaPath = (ecosystemRoot) => path.join(ecosystemRoot, ".secufusion", "dna.json");
4034
+ function ensureDnaFile(ecosystemRoot) {
4035
+ const dnaPath = getDnaPath(ecosystemRoot);
4036
+ const dir = path.dirname(dnaPath);
4037
+ if (!fs.existsSync(dir)) {
4038
+ fs.mkdirSync(dir, { recursive: true });
4039
+ }
4040
+ if (!fs.existsSync(dnaPath)) {
4041
+ fs.writeFileSync(dnaPath, JSON.stringify({
4042
+ domains: [],
4043
+ apis: [],
4044
+ events: [],
4045
+ node_apis: [],
4046
+ frontend: [],
4047
+ infra: [],
4048
+ databases: [],
4049
+ security: [],
4050
+ configurations: [],
4051
+ inferred_patterns: []
4052
+ }, null, 2));
4053
+ }
4054
+ return dnaPath;
4055
+ }
4056
+ function readDna(ecosystemRoot) {
4057
+ ensureDnaFile(ecosystemRoot);
4058
+ return JSON.parse(fs.readFileSync(getDnaPath(ecosystemRoot), "utf-8"));
4059
+ }
4060
+ function writeDna(ecosystemRoot, dna) {
4061
+ fs.writeFileSync(getDnaPath(ecosystemRoot), JSON.stringify(dna, null, 2));
4062
+ }
4063
+ const VALID_EXTENSIONS = ['.java', '.ts', '.js', '.tsx', '.jsx', '.vue', '.sql', '.prisma', '.tf', '.yaml', '.yml', '.env', '.env.example', '.env.local', '.properties'];
4064
+ function getAllSourceFiles(dirPath, repoName, arrayOfFiles = []) {
4065
+ if (!fs.existsSync(dirPath))
4066
+ return arrayOfFiles;
4067
+ const files = fs.readdirSync(dirPath);
4068
+ files.forEach(function (file) {
4069
+ const fullPath = path.join(dirPath, file);
4070
+ if (fs.statSync(fullPath).isDirectory()) {
4071
+ if (file !== "node_modules" && file !== "target" && file !== ".git" && file !== "build") {
4072
+ arrayOfFiles = getAllSourceFiles(fullPath, repoName, arrayOfFiles);
4073
+ }
4074
+ }
4075
+ else {
4076
+ if (VALID_EXTENSIONS.some(ext => file.endsWith(ext)) || file === 'docker-compose.yml') {
4077
+ arrayOfFiles.push({ path: fullPath, repo: repoName });
4078
+ }
4079
+ }
4080
+ });
4081
+ return arrayOfFiles;
4082
+ }
4083
+ function getPolyRepoSourceFiles(ecosystemRoot) {
4084
+ let allFiles = [];
4085
+ const children = fs.readdirSync(ecosystemRoot);
4086
+ for (const child of children) {
4087
+ const childPath = path.join(ecosystemRoot, child);
4088
+ if (fs.statSync(childPath).isDirectory() && (child.startsWith("sfn-") || child === "secufusion")) {
4089
+ allFiles = allFiles.concat(getAllSourceFiles(childPath, child));
4090
+ }
4091
+ else if (fs.statSync(childPath).isDirectory() && ecosystemRoot === childPath) {
4092
+ allFiles = allFiles.concat(getAllSourceFiles(childPath, path.basename(childPath)));
4093
+ }
4094
+ }
4095
+ return allFiles;
4096
+ }
4097
+ function performFullExtraction(ecosystemRoot) {
4098
+ const sourceFiles = getPolyRepoSourceFiles(ecosystemRoot);
4099
+ const models = [];
4100
+ const apis = [];
4101
+ const events = [];
4102
+ const nodeApis = [];
4103
+ const frontend = [];
4104
+ const infra = [];
4105
+ const databases = [];
4106
+ const security = [];
4107
+ const configurations = [];
4108
+ for (const fileObj of sourceFiles) {
4109
+ const content = fs.readFileSync(fileObj.path, 'utf-8');
4110
+ if (fileObj.path.endsWith('.java')) {
4111
+ models.push(...parseDomainModels(fileObj.path, content, fileObj.repo));
4112
+ apis.push(...parseApiEndpoints(fileObj.path, content, fileObj.repo));
4113
+ events.push(...parseEventTopics(fileObj.path, content, fileObj.repo));
4114
+ }
4115
+ nodeApis.push(...parseNodeApis(fileObj.path, content, fileObj.repo));
4116
+ frontend.push(...parseFrontend(fileObj.path, content, fileObj.repo));
4117
+ infra.push(...parseInfra(fileObj.path, content, fileObj.repo));
4118
+ databases.push(...parseDatabase(fileObj.path, content, fileObj.repo));
4119
+ security.push(...parseSecurity(fileObj.path, content, fileObj.repo));
4120
+ configurations.push(...parseConfig(fileObj.path, content, fileObj.repo));
4121
+ }
4122
+ const dna = readDna(ecosystemRoot);
4123
+ dna.domains = models;
4124
+ dna.apis = apis;
4125
+ dna.events = events;
4126
+ dna.node_apis = nodeApis;
4127
+ dna.frontend = frontend;
4128
+ dna.infra = infra;
4129
+ dna.databases = databases;
4130
+ dna.security = security;
4131
+ dna.configurations = configurations;
4132
+ // Apply Pattern Inference
4133
+ dna.inferred_patterns = inferPatterns(dna);
4134
+ writeDna(ecosystemRoot, dna);
4135
+ return {
4136
+ modelsCount: models.length,
4137
+ apisCount: apis.length + nodeApis.length,
4138
+ eventsCount: events.length,
4139
+ frontendCount: frontend.length,
4140
+ infraCount: infra.length,
4141
+ databasesCount: databases.length,
4142
+ securityCount: security.length,
4143
+ configurationsCount: configurations.length,
4144
+ inferredCount: dna.inferred_patterns.length
4145
+ };
4146
+ }
4147
+ server.tool("scan_repository_stack", "Analyzes the ecosystem root to identify the framework and infrastructure", {
4148
+ workspace_root: z.string().describe("Root directory of the workspace")
4149
+ }, async ({ workspace_root }) => {
4150
+ try {
4151
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4152
+ ensureDnaFile(ecosystemRoot);
4153
+ const files = fs.readdirSync(ecosystemRoot);
4154
+ let stack = [];
4155
+ if (files.includes("package.json"))
4156
+ stack.push("Node.js");
4157
+ if (files.includes("pom.xml") || files.includes("build.gradle"))
4158
+ stack.push("Java/Spring");
4159
+ if (files.includes("docker-compose.yml"))
4160
+ stack.push("Docker");
4161
+ let warningMsg = "";
4162
+ // Look for .secufusion-project-spec.json in ecosystemRoot or workspaceRoot
4163
+ const specPathEcosystem = path.join(ecosystemRoot, ".secufusion-project-spec.json");
4164
+ const specPathWorkspace = path.join(workspace_root, ".secufusion-project-spec.json");
4165
+ const specPath = fs.existsSync(specPathWorkspace) ? specPathWorkspace : (fs.existsSync(specPathEcosystem) ? specPathEcosystem : null);
4166
+ if (specPath) {
4167
+ try {
4168
+ const specData = JSON.parse(fs.readFileSync(specPath, "utf-8"));
4169
+ if (specData) {
4170
+ let expectedServices = [];
4171
+ if (specData.microservices) {
4172
+ expectedServices = expectedServices.concat(Object.keys(specData.microservices));
4173
+ }
4174
+ if (specData.frontend && specData.frontend.repo) {
4175
+ expectedServices.push(specData.frontend.repo);
4176
+ }
4177
+ if (specData.chrome_extension && specData.chrome_extension.repo) {
4178
+ expectedServices.push(specData.chrome_extension.repo);
4179
+ }
4180
+ const physicalDirs = files.filter(f => fs.statSync(path.join(ecosystemRoot, f)).isDirectory());
4181
+ const missingServices = expectedServices.filter(svc => !physicalDirs.includes(svc));
4182
+ if (missingServices.length > 0) {
4183
+ warningMsg = `\n\n[WARNING] According to .secufusion-project-spec.json, the following mandatory repositories (microservices/frontend/ext) are missing from your local workspace: ${missingServices.join(", ")}. Please clone them to ensure a complete DNA map.`;
4184
+ }
4185
+ }
4186
+ }
4187
+ catch (e) {
4188
+ warningMsg = `\n\n[WARNING] Failed to parse .secufusion-project-spec.json: ${e.message}`;
4189
+ }
4190
+ }
4191
+ return {
4192
+ content: [{ type: "text", text: `Scanned ecosystem at ${ecosystemRoot}. Detected stack: ${stack.join(", ")}${warningMsg}` }]
4193
+ };
4194
+ }
4195
+ catch (e) {
4196
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4197
+ }
4198
+ });
4199
+ server.tool("extract_domain_models", "Parses files across the ecosystem to build the Domain Encyclopedia and checks Golden Rules", {
4200
+ workspace_root: z.string().describe("Root directory of the workspace"),
4201
+ }, async ({ workspace_root }) => {
4202
+ try {
4203
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4204
+ const stats = performFullExtraction(ecosystemRoot);
4205
+ return {
4206
+ content: [{ type: "text", text: `Extracted ${stats.modelsCount} domain models. Derived ${stats.inferredCount} architectural patterns.` }]
4207
+ };
4208
+ }
4209
+ catch (e) {
4210
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4211
+ }
4212
+ });
4213
+ server.tool("extract_api_endpoints", "Scans for REST/GraphQL endpoints across the ecosystem", {
4214
+ workspace_root: z.string().describe("Root directory of the workspace")
4215
+ }, async ({ workspace_root }) => {
4216
+ try {
4217
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4218
+ const stats = performFullExtraction(ecosystemRoot);
4219
+ return {
4220
+ content: [{ type: "text", text: `Extracted ${stats.apisCount} API controllers. Derived ${stats.inferredCount} architectural patterns.` }]
4221
+ };
4222
+ }
4223
+ catch (e) {
4224
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4225
+ }
4226
+ });
4227
+ server.tool("extract_event_topics", "Scans for event listener/producer usage across the ecosystem", {
4228
+ workspace_root: z.string().describe("Root directory of the workspace")
4229
+ }, async ({ workspace_root }) => {
4230
+ try {
4231
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4232
+ const stats = performFullExtraction(ecosystemRoot);
4233
+ return {
4234
+ content: [{ type: "text", text: `Extracted ${stats.eventsCount} event classes. Derived ${stats.inferredCount} architectural patterns.` }]
4235
+ };
4236
+ }
4237
+ catch (e) {
4238
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4239
+ }
4240
+ });
4241
+ server.tool("query_knowledge_graph", "Queries the relationships in the central dna.json", {
4242
+ workspace_root: z.string().describe("Root directory of the workspace"),
4243
+ query: z.string().describe("What to query")
4244
+ }, async ({ workspace_root, query }) => {
4245
+ try {
4246
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4247
+ const dnaPath = getDnaPath(ecosystemRoot);
4248
+ if (!fs.existsSync(dnaPath)) {
4249
+ return { content: [{ type: "text", text: `No DNA found at ${dnaPath}.` }] };
4250
+ }
4251
+ const dna = JSON.parse(fs.readFileSync(dnaPath, "utf-8"));
4252
+ const result = JSON.stringify(dna, null, 2);
4253
+ return {
4254
+ content: [{ type: "text", text: `Ecosystem Knowledge Graph Data:\n${result}` }]
4255
+ };
4256
+ }
4257
+ catch (e) {
4258
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4259
+ }
4260
+ });
4261
+ server.tool("calculate_blast_radius", "Calculates the impact of changing a specific component, entity, or API", {
4262
+ workspace_root: z.string().describe("Root directory of the workspace"),
4263
+ component_name: z.string().describe("Name of the component, entity, or API to analyze")
4264
+ }, async ({ workspace_root, component_name }) => {
4265
+ try {
4266
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4267
+ const dnaPath = getDnaPath(ecosystemRoot);
4268
+ if (!fs.existsSync(dnaPath)) {
4269
+ return { content: [{ type: "text", text: `No DNA found at ${dnaPath}.` }] };
4270
+ }
4271
+ const dna = JSON.parse(fs.readFileSync(dnaPath, "utf-8"));
4272
+ // Simple search across all arrays to find dependencies
4273
+ const impacts = [];
4274
+ for (const key of Object.keys(dna)) {
4275
+ if (Array.isArray(dna[key])) {
4276
+ for (const item of dna[key]) {
4277
+ const itemStr = JSON.stringify(item);
4278
+ if (itemStr.includes(component_name)) {
4279
+ impacts.push(`- Found in ${key}: ${item.controllerName || item.componentName || item.tableName || item.name || item.filePath || item.key}`);
4280
+ }
4281
+ }
4282
+ }
4283
+ }
4284
+ return {
4285
+ content: [{ type: "text", text: `Blast Radius for '${component_name}':\n${impacts.join('\n')}` }]
4286
+ };
4287
+ }
4288
+ catch (e) {
4289
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4290
+ }
4291
+ });
4292
+ server.tool("load_external_env_file", "Loads an unmasked external environment file to override masked values in the knowledge graph", {
4293
+ workspace_root: z.string().describe("Root directory of the workspace"),
4294
+ absolute_file_path: z.string().describe("Absolute path to the unmasked .env or .properties file")
4295
+ }, async ({ workspace_root, absolute_file_path }) => {
4296
+ try {
4297
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4298
+ if (!fs.existsSync(absolute_file_path)) {
4299
+ return { content: [{ type: "text", text: `File not found: ${absolute_file_path}` }], isError: true };
4300
+ }
4301
+ const content = fs.readFileSync(absolute_file_path, "utf-8");
4302
+ const realValues = parseExternalEnv(content);
4303
+ const dnaPath = getDnaPath(ecosystemRoot);
4304
+ if (!fs.existsSync(dnaPath)) {
4305
+ return { content: [{ type: "text", text: `No DNA found at ${dnaPath}.` }], isError: true };
4306
+ }
4307
+ const dna = JSON.parse(fs.readFileSync(dnaPath, "utf-8"));
4308
+ let updatedCount = 0;
4309
+ if (dna.configurations) {
4310
+ for (const config of dna.configurations) {
4311
+ for (const entry of config.entries) {
4312
+ if (entry.requiresRealValues && realValues[entry.key]) {
4313
+ entry.value = realValues[entry.key];
4314
+ entry.requiresRealValues = false;
4315
+ updatedCount++;
4316
+ }
4317
+ }
4318
+ }
4319
+ }
4320
+ writeDna(ecosystemRoot, dna);
4321
+ return {
4322
+ content: [{ type: "text", text: `Successfully loaded ${updatedCount} real configuration values into the Knowledge Graph.` }]
4323
+ };
4324
+ }
4325
+ catch (e) {
4326
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
4327
+ }
4328
+ });
4329
+ server.tool("start_dna_watcher", "Starts a continuous file watcher to automatically sync the DNA when files are modified", {
4330
+ workspace_root: z.string().describe("Root directory of the workspace")
4331
+ }, async ({ workspace_root }) => {
4332
+ try {
4333
+ if (watcher) {
4334
+ return { content: [{ type: "text", text: `DNA Watcher is already running.` }] };
4335
+ }
4336
+ const ecosystemRoot = getEcosystemRoot(workspace_root);
4337
+ watcher = chokidar.watch(ecosystemRoot, {
4338
+ ignored: /(^|[\/\\])\..|node_modules|target|build/,
4339
+ persistent: true
4340
+ });
4341
+ watcher.on('change', (path) => {
4342
+ if (VALID_EXTENSIONS.some(ext => path.endsWith(ext))) {
4343
+ console.log(`[DNA Watcher] Detected change in ${path}. Re-syncing DNA...`);
4344
+ performFullExtraction(ecosystemRoot);
4345
+ }
4346
+ });
4347
+ return {
4348
+ content: [{ type: "text", text: `Successfully started DNA File Watcher on ${ecosystemRoot}. The knowledge graph will now update automatically in the background.` }]
4349
+ };
4350
+ }
4351
+ catch (e) {
4352
+ return { content: [{ type: "text", text: `Error starting watcher: ${e.message}` }], isError: true };
4353
+ }
4354
+ });
4355
+ // ─── MCP Prompts (appear as slash commands in Claude IDE picker) ──────────────
4356
+ server.prompt("sfn-init", "Prime the SecuFusion ecosystem — scan the entire codebase, map the architectural DNA (services, models, APIs, events), and write it to .secufusion/dna.json. Run this once when you first open a new project.", {
4357
+ workspace_root: z.string().describe("Absolute path to the project root. Leave blank to use the current working directory.")
4358
+ }, ({ workspace_root }) => ({
4359
+ messages: [{
4360
+ role: "user",
4361
+ content: {
4362
+ type: "text",
4363
+ text: `You are executing **sfn:init** — the SecuFusion DNA bootstrap command.
4364
+
4365
+ Workspace root: ${workspace_root || process.cwd()}
4366
+
4367
+ **This is a learn-first command. Do NOT write implementation code. Study THIS project and produce real findings.**
4368
+
4369
+ ## Phase 1 — SCAN (run all 4 tool calls concurrently in a single message)
4370
+ 1. Call \`scan_repository_stack\` — identify language, framework, microservices, build/test commands. Flag monorepos.
4371
+ 2. Call \`extract_domain_models\` — map all domain entities, fields, and relationships.
4372
+ 3. Call \`extract_api_endpoints\` — map all REST/GraphQL endpoints, methods, and owning services.
4373
+ 4. Call \`extract_event_topics\` — map all Kafka/event-bus topics, producers, and consumers.
4374
+
4375
+ ## Phase 2 — WRITE DNA
4376
+ - Write combined findings to \`.secufusion/dna.json\` at the workspace root.
4377
+ - Update the \`<MEMORY>\` block in \`agents/claude.md\`: set \`DNA_LOADED = true\`, \`LAST_INIT\` = now, and \`ARCHITECTURE_SUMMARY\` = a 2-3 sentence human-readable summary.
4378
+
4379
+ ## Phase 3 — REPORT
4380
+ - Count: services, domain models, API endpoints, event topics found.
4381
+ - Flag anything marked \`(inferred)\` that the human should verify.
4382
+ - Confirm \`.secufusion/dna.json\` was written successfully.
4383
+
4384
+ **Next step:** Run \`/sfn-plan\` with a ticket ID to start your first task.`
4385
+ }
4386
+ }]
4387
+ }));
4388
+ server.prompt("sfn-watch", "Enable continuous background DNA learning — starts a file watcher that automatically re-syncs .secufusion/dna.json whenever source files change.", {
4389
+ workspace_root: z.string().describe("Absolute path to the project root to watch.")
4390
+ }, ({ workspace_root }) => ({
4391
+ messages: [{
4392
+ role: "user",
4393
+ content: {
4394
+ type: "text",
4395
+ text: `You are executing **sfn:watch** — the SecuFusion continuous DNA watcher.
4396
+
4397
+ Workspace root: ${workspace_root || process.cwd()}
4398
+
4399
+ Call \`start_dna_watcher\` with workspace_root = "${workspace_root || process.cwd()}".
4400
+
4401
+ This will start a background file watcher that automatically re-scans and updates \`.secufusion/dna.json\` whenever source files are modified, keeping the architectural knowledge graph fresh without manual re-runs of \`/sfn-init\`.
4402
+
4403
+ Confirm to the user when the watcher is active and which directory is being watched.`
4404
+ }
4405
+ }]
4406
+ }));
4407
+ // ─────────────────────────────────────────────────────────────────────────────
4004
4408
  const transport = new StdioServerTransport();
4005
4409
  await server.connect(transport);
package/package.json CHANGED
@@ -1,30 +1,33 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.2.7",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
- "main": "index.js",
6
+ "main": "mcp/dist/server.js",
7
7
  "files": [
8
- "index.js",
9
- "index.d.ts",
10
- "README.md",
11
- ".secufusion-project-spec.json",
12
- "AGENTS.md",
13
- "commands/",
14
- ".claude-plugin/"
8
+ ".claude-plugin",
9
+ ".mcp.json",
10
+ ".claude",
11
+ "agents",
12
+ "commands",
13
+ "hooks",
14
+ "mcp/dist",
15
+ "scripts",
16
+ "README.md"
15
17
  ],
16
18
  "bin": {
17
- "secufusion-mcp": "index.js"
19
+ "secufusion-mcp": "mcp/dist/server.js"
18
20
  },
19
21
  "engines": {
20
22
  "node": ">=18.0.0"
21
23
  },
22
24
  "scripts": {
23
- "build": "tsc",
24
- "prepublishOnly": "copy ..\\README.md README.md && copy ..\\.agents\\AGENTS.md AGENTS.md && npm run build",
25
+ "build": "tsc --project mcp/tsconfig.json",
26
+ "prepublishOnly": "copy ..\\README.md README.md && npm run build",
25
27
  "postpublish": "del README.md",
26
- "start": "node index.js",
27
- "dev": "tsc && node index.js"
28
+ "start": "node mcp/dist/server.js",
29
+ "dev": "tsc --project mcp/tsconfig.json && node mcp/dist/server.js",
30
+ "postinstall": "node -e \"try { require('fs').chmodSync('hooks/knowledge-drift.sh', 0o755) } catch(e) {}\""
28
31
  },
29
32
  "keywords": [
30
33
  "mcp",
@@ -35,7 +38,8 @@
35
38
  "license": "ISC",
36
39
  "dependencies": {
37
40
  "@modelcontextprotocol/sdk": "^1.0.0",
38
- "zod": "^3.23.8"
41
+ "zod": "^3.23.8",
42
+ "chokidar": "^5.0.0"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/node": "^22.0.0",
@@ -0,0 +1,108 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { resolve, readFileSafe, getWorkspaceRoot } from './utils.js';
4
+ function parseArgs() {
5
+ try {
6
+ return JSON.parse(process.argv[2] || '{}');
7
+ }
8
+ catch {
9
+ return { work_item_id: 'UNKNOWN', skip_checks: [], suppressions: [] };
10
+ }
11
+ }
12
+ async function runTier1Checks(workItemId, skipChecks, suppressions) {
13
+ const wsRoot = getWorkspaceRoot();
14
+ // Find task folder
15
+ const tasksBase = resolve(".secufusion/tasks");
16
+ let taskDir = null;
17
+ if (fs.existsSync(tasksBase)) {
18
+ const folders = fs.readdirSync(tasksBase);
19
+ const match = folders.find(f => f.startsWith(workItemId));
20
+ if (match)
21
+ taskDir = path.join(tasksBase, match);
22
+ }
23
+ if (!taskDir) {
24
+ return `❌ CHANGES_REQUESTED: Task folder not found for ${workItemId}`;
25
+ }
26
+ // Read files-touched.json
27
+ let filesTouched = [];
28
+ try {
29
+ const ftContent = readFileSafe(path.join(taskDir, 'files-touched.json'));
30
+ if (ftContent)
31
+ filesTouched = JSON.parse(ftContent);
32
+ }
33
+ catch { }
34
+ let out = `## run_pre_pr_checks_with_reviewer_agent — ${workItemId}\n\n`;
35
+ out += `Found ${filesTouched.length} modified files.\n\n`;
36
+ let allPassed = true;
37
+ let failedChecks = [];
38
+ for (const file of filesTouched) {
39
+ const fullPath = path.resolve(wsRoot, file);
40
+ const content = readFileSafe(fullPath);
41
+ if (!content)
42
+ continue;
43
+ const isJava = file.endsWith('.java');
44
+ const isTs = file.endsWith('.ts');
45
+ const isSql = file.endsWith('.sql');
46
+ // P1 TENANT_ISOLATION
47
+ if (!skipChecks.includes('tenant_isolation') && isJava && file.includes('Repository')) {
48
+ const hasTenantFilter = /tenantid|tenant_id|\.tenant/i.test(content);
49
+ if (!hasTenantFilter) {
50
+ const suppressed = suppressions.find(s => s.check === 'TENANT_ISOLATION');
51
+ if (!suppressed) {
52
+ allPassed = false;
53
+ failedChecks.push(`❌ P1 TENANT_ISOLATION: ${file} lacks tenant filtering.`);
54
+ }
55
+ }
56
+ }
57
+ // P2 N_PLUS_ONE
58
+ if (!skipChecks.includes('n_plus_one') && (isJava || isTs)) {
59
+ // Very naive check for loop containing repository/dao calls
60
+ if (/(for|while)\s*\(.*\)[\s\S]*?(repository\.|dao\.)/i.test(content)) {
61
+ allPassed = false;
62
+ failedChecks.push(`❌ P2 N_PLUS_ONE: Potential DB call inside loop in ${file}.`);
63
+ }
64
+ }
65
+ // P3 MISSING_INDEX
66
+ if (!skipChecks.includes('missing_index') && isSql) {
67
+ if (/create table|alter table.*add column/i.test(content) && !/create index/i.test(content)) {
68
+ allPassed = false;
69
+ failedChecks.push(`❌ P3 MISSING_INDEX: Schema modification without an index in ${file}.`);
70
+ }
71
+ }
72
+ // P4 KAFKA_SYNC
73
+ if (!skipChecks.includes('kafka_sync') && isJava && content.includes('@KafkaListener')) {
74
+ if (/resttemplate|webclient|httpclient/i.test(content)) {
75
+ allPassed = false;
76
+ failedChecks.push(`❌ P4 KAFKA_SYNC: Synchronous HTTP call inside @KafkaListener in ${file}.`);
77
+ }
78
+ }
79
+ // P5 EARLY_RETURN
80
+ if (!skipChecks.includes('early_return') && (isJava || isTs)) {
81
+ // Count number of return statements. If > 1 in a method block, flag it (naive implementation)
82
+ const returns = (content.match(/\breturn\b/g) || []).length;
83
+ if (returns > 1 && !file.includes('Test')) {
84
+ allPassed = false;
85
+ failedChecks.push(`❌ P5 EARLY_RETURN: Multiple exit points detected in ${file}.`);
86
+ }
87
+ }
88
+ }
89
+ if (!allPassed) {
90
+ out += `### Tier 1: Mechanical Checks Failed\n`;
91
+ out += failedChecks.join('\n') + '\n\n';
92
+ out += `Verdict: ❌ CHANGES_REQUESTED\n`;
93
+ }
94
+ else {
95
+ out += `### Tier 1: Mechanical Checks Passed\n`;
96
+ out += `All files passed zero-tolerance P1-P5 validation.\n\n`;
97
+ if (!skipChecks.includes('ai_review') || !skipChecks.includes('context_review')) {
98
+ out += `### Tier 2 & 3: AI Reviewer\n`;
99
+ out += `(AI logic would execute here using Claude context)\n\n`;
100
+ }
101
+ out += `Verdict: ✅ APPROVED\n`;
102
+ }
103
+ return out;
104
+ }
105
+ const args = parseArgs();
106
+ runTier1Checks(args.work_item_id, args.skip_checks, args.suppressions).then(out => {
107
+ console.log(out);
108
+ });