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.
- package/.claude/settings.json +9 -0
- package/.claude-plugin/plugin.json +5 -6
- package/.mcp.json +19 -0
- package/{AGENTS.md → agents/AGENTS.md} +3 -3
- package/agents/claude.md +50 -0
- package/agents/coder.md +80 -0
- package/agents/planner.md +354 -0
- package/agents/reviewer.md +384 -0
- package/commands/analyze.md +15 -0
- package/commands/blast-radius.md +14 -0
- package/commands/map-architecture.md +10 -0
- package/commands/map-services.md +3 -0
- package/commands/watch-dna.md +9 -0
- package/hooks/hooks.json +17 -0
- package/hooks/knowledge-drift.sh +48 -0
- package/mcp/dist/parsers/api.js +30 -0
- package/mcp/dist/parsers/backend-node.js +26 -0
- package/mcp/dist/parsers/config.js +74 -0
- package/mcp/dist/parsers/database.js +40 -0
- package/mcp/dist/parsers/domain.js +21 -0
- package/mcp/dist/parsers/events.js +36 -0
- package/mcp/dist/parsers/frontend.js +34 -0
- package/mcp/dist/parsers/infra.js +42 -0
- package/mcp/dist/parsers/patterns.js +49 -0
- package/mcp/dist/parsers/security.js +16 -0
- package/{index.js → mcp/dist/server.js} +408 -4
- package/package.json +19 -15
- package/scripts/sfn-pr-check.js +108 -0
- package/scripts/sfn-pr-check.ts +121 -0
- package/scripts/utils.js +29 -0
- package/scripts/utils.ts +31 -0
- package/.secufusion-project-spec.json +0 -1068
- /package/commands/{code.md → sfn-code.md} +0 -0
- /package/commands/{init.md → sfn-init.md} +0 -0
- /package/commands/{plan.md → sfn-plan.md} +0 -0
- /package/commands/{review.md → sfn-review.md} +0 -0
|
@@ -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 =
|
|
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
|
|
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
|
|
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")), "
|
|
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": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SecuFusion MCP server - developer workflow tooling with guardrails",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "mcp/dist/server.js",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
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": "
|
|
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 &&
|
|
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
|
|
27
|
-
"dev": "tsc && node
|
|
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
|
+
});
|