secufusion-mcp 1.0.8 → 1.0.10
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/README.md +4 -2
- package/index.js +77 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ Manages a structured JSON state file (`.secufusion-state.json`) keyed to the cur
|
|
|
115
115
|
| `pending_acs` | array | No | Array of strings for pending tasks (For `initialize`/`update`) |
|
|
116
116
|
| `completed_acs` | array | No | Array of completed AC strings (For `update`) |
|
|
117
117
|
| `next_step` | string | No | **CRITICAL for `update`**: A clear instruction on what to do next to allow instant resumption. |
|
|
118
|
-
| `reference_file_path` | string | No | Path to a reference file with standard coding patterns |
|
|
118
|
+
| `reference_file_path` | string | No | Path to a reference file with standard coding patterns (auto-compressed to save tokens) |
|
|
119
119
|
|
|
120
120
|
**Example — Starting a new task:**
|
|
121
121
|
```json
|
|
@@ -256,6 +256,7 @@ These rules are enforced automatically — the AI will never violate them:
|
|
|
256
256
|
✅ No hardcoded UAT/Prod IPs or environment URLs
|
|
257
257
|
✅ Every JPA @Entity change accompanied by a Flyway .sql migration
|
|
258
258
|
✅ State in .secufusion-state.json must be fully resolved before PR is raised
|
|
259
|
+
✅ Token Efficiency: Every tool response includes a 📊 Telemetry receipt tracking input/output tokens, cost, and the Session Total
|
|
259
260
|
```
|
|
260
261
|
|
|
261
262
|
---
|
|
@@ -266,8 +267,9 @@ These rules are enforced automatically — the AI will never violate them:
|
|
|
266
267
|
|---|---|
|
|
267
268
|
| `.secufusion-state.json` | Living task blueprint — tracks pending/completed ACs per branch |
|
|
268
269
|
| `.rejected-patterns.json` | Cumulative log of all rejected patterns across sessions |
|
|
270
|
+
| `.secufusion-tokens.json` | Persistent tracking of session-wide LLM token usage and cost |
|
|
269
271
|
|
|
270
|
-
> **Tip:** Commit
|
|
272
|
+
> **Tip:** Commit `.secufusion-state.json` and `.rejected-patterns.json` to your repo. Do **not** commit `.secufusion-tokens.json`.
|
|
271
273
|
|
|
272
274
|
---
|
|
273
275
|
|
package/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { execSync } from "child_process";
|
|
|
24
24
|
// ─────────────────────────────────────────────
|
|
25
25
|
const STATE_FILE = ".secufusion-state.json";
|
|
26
26
|
const REJECTED_FILE = ".rejected-patterns.json";
|
|
27
|
+
const TOKEN_FILE = ".secufusion-tokens.json";
|
|
27
28
|
/** Resolve a path relative to cwd (where the MCP server is invoked). */
|
|
28
29
|
function resolve(file) {
|
|
29
30
|
return path.resolve(process.cwd(), file);
|
|
@@ -49,6 +50,50 @@ function getCurrentBranch(cwd) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
// ─────────────────────────────────────────────
|
|
53
|
+
// Telemetry & Compression helpers
|
|
54
|
+
// ─────────────────────────────────────────────
|
|
55
|
+
function compressCode(code) {
|
|
56
|
+
return code
|
|
57
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
|
|
58
|
+
.replace(/\/\/.*$/gm, "") // Remove single-line comments
|
|
59
|
+
.replace(/^\s*[\r\n]/gm, "") // Remove empty lines
|
|
60
|
+
.replace(/[ \t]+$/gm, ""); // Remove trailing whitespace
|
|
61
|
+
}
|
|
62
|
+
function appendTelemetry(response, inputChars) {
|
|
63
|
+
const inputTokens = Math.ceil(inputChars / 4);
|
|
64
|
+
let outputChars = 0;
|
|
65
|
+
if (response.content && Array.isArray(response.content)) {
|
|
66
|
+
for (const c of response.content) {
|
|
67
|
+
if (c.type === "text" && c.text) {
|
|
68
|
+
outputChars += c.text.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const outputTokens = Math.ceil(outputChars / 4);
|
|
73
|
+
const inputCost = (inputTokens / 1000000) * 3.0;
|
|
74
|
+
const outputCost = (outputTokens / 1000000) * 15.0;
|
|
75
|
+
const totalCost = inputCost + outputCost;
|
|
76
|
+
// Track Session Totals
|
|
77
|
+
const tokenFilePath = resolve(TOKEN_FILE);
|
|
78
|
+
let session = { inputTokens: 0, outputTokens: 0, totalCost: 0 };
|
|
79
|
+
const existing = readFileSafe(tokenFilePath);
|
|
80
|
+
if (existing) {
|
|
81
|
+
try {
|
|
82
|
+
session = JSON.parse(existing);
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
86
|
+
session.inputTokens += inputTokens;
|
|
87
|
+
session.outputTokens += outputTokens;
|
|
88
|
+
session.totalCost += totalCost;
|
|
89
|
+
writeFile(tokenFilePath, JSON.stringify(session, null, 2));
|
|
90
|
+
const telemetry = `\n\n> 📊 Telemetry\n> **Request:** Input: ~${inputTokens} | Output: ~${outputTokens} | Cost: ${totalCost.toFixed(4)}\n> **Session Total:** Input: ~${session.inputTokens} | Output: ~${session.outputTokens} | Cost: ${session.totalCost.toFixed(4)}`;
|
|
91
|
+
if (response.content && response.content.length > 0 && response.content[0].type === "text") {
|
|
92
|
+
response.content[0].text += telemetry;
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
}
|
|
96
|
+
// ─────────────────────────────────────────────
|
|
52
97
|
// run_pre_pr_checks helpers
|
|
53
98
|
// ─────────────────────────────────────────────
|
|
54
99
|
const CONSOLE_LOG_REGEX = [
|
|
@@ -282,6 +327,7 @@ server.tool("manage_branch_state", "Manage the structured JSON state for the cur
|
|
|
282
327
|
.optional()
|
|
283
328
|
.describe("Explicit actionable instruction for resuming. Required for 'update'."),
|
|
284
329
|
}, async ({ action, task_description, reference_file_path, pending_acs, completed_acs, next_step }) => {
|
|
330
|
+
const inputChars = JSON.stringify({ action, task_description, reference_file_path, pending_acs, completed_acs, next_step }).length;
|
|
285
331
|
const cwd = process.cwd();
|
|
286
332
|
const branch = getCurrentBranch(cwd);
|
|
287
333
|
const statePath = path.resolve(STATE_FILE);
|
|
@@ -297,18 +343,18 @@ server.tool("manage_branch_state", "Manage the structured JSON state for the cur
|
|
|
297
343
|
if (action === "read") {
|
|
298
344
|
const branchState = state[branch];
|
|
299
345
|
if (!branchState) {
|
|
300
|
-
return {
|
|
346
|
+
return appendTelemetry({
|
|
301
347
|
content: [
|
|
302
348
|
{
|
|
303
349
|
type: "text",
|
|
304
350
|
text: `No state found for branch '${branch}'. Please initialize first.`,
|
|
305
351
|
},
|
|
306
352
|
],
|
|
307
|
-
};
|
|
353
|
+
}, inputChars);
|
|
308
354
|
}
|
|
309
|
-
return {
|
|
355
|
+
return appendTelemetry({
|
|
310
356
|
content: [{ type: "text", text: JSON.stringify(branchState, null, 2) }],
|
|
311
|
-
};
|
|
357
|
+
}, inputChars);
|
|
312
358
|
}
|
|
313
359
|
if (action === "initialize") {
|
|
314
360
|
state[branch] = {
|
|
@@ -320,31 +366,42 @@ server.tool("manage_branch_state", "Manage the structured JSON state for the cur
|
|
|
320
366
|
last_updated: new Date().toISOString()
|
|
321
367
|
};
|
|
322
368
|
writeFile(statePath, JSON.stringify(state, null, 2));
|
|
323
|
-
|
|
369
|
+
let refText = "";
|
|
370
|
+
if (reference_file_path) {
|
|
371
|
+
const refPath = resolve(reference_file_path);
|
|
372
|
+
const content = readFileSafe(refPath);
|
|
373
|
+
if (content) {
|
|
374
|
+
refText = `\n\n**Reference Code (Compressed):**\n\`\`\`\n${compressCode(content)}\n\`\`\``;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
refText = `\n\n*(Warning: reference file '${reference_file_path}' not found)*`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return appendTelemetry({
|
|
324
381
|
content: [
|
|
325
382
|
{
|
|
326
383
|
type: "text",
|
|
327
|
-
text: `✅ Branch state initialized for '${branch}'.\n\nState:\n${JSON.stringify(state[branch], null, 2)}`,
|
|
384
|
+
text: `✅ Branch state initialized for '${branch}'.\n\nState:\n${JSON.stringify(state[branch], null, 2)}${refText}`,
|
|
328
385
|
},
|
|
329
386
|
],
|
|
330
|
-
};
|
|
387
|
+
}, inputChars);
|
|
331
388
|
}
|
|
332
389
|
if (action === "update") {
|
|
333
390
|
if (!state[branch]) {
|
|
334
|
-
return {
|
|
391
|
+
return appendTelemetry({
|
|
335
392
|
isError: true,
|
|
336
393
|
content: [
|
|
337
394
|
{ type: "text", text: `No state found for branch '${branch}'. Initialize first.` },
|
|
338
395
|
],
|
|
339
|
-
};
|
|
396
|
+
}, inputChars);
|
|
340
397
|
}
|
|
341
398
|
if (!next_step) {
|
|
342
|
-
return {
|
|
399
|
+
return appendTelemetry({
|
|
343
400
|
isError: true,
|
|
344
401
|
content: [
|
|
345
402
|
{ type: "text", text: "ERROR: 'next_step' is required when updating." },
|
|
346
403
|
],
|
|
347
|
-
};
|
|
404
|
+
}, inputChars);
|
|
348
405
|
}
|
|
349
406
|
if (pending_acs)
|
|
350
407
|
state[branch].pending_acs = pending_acs;
|
|
@@ -353,16 +410,16 @@ server.tool("manage_branch_state", "Manage the structured JSON state for the cur
|
|
|
353
410
|
state[branch].next_step = next_step;
|
|
354
411
|
state[branch].last_updated = new Date().toISOString();
|
|
355
412
|
writeFile(statePath, JSON.stringify(state, null, 2));
|
|
356
|
-
return {
|
|
413
|
+
return appendTelemetry({
|
|
357
414
|
content: [
|
|
358
415
|
{
|
|
359
416
|
type: "text",
|
|
360
417
|
text: `✅ Branch state updated for '${branch}'. Next step recorded:\n> ${next_step}`,
|
|
361
418
|
},
|
|
362
419
|
],
|
|
363
|
-
};
|
|
420
|
+
}, inputChars);
|
|
364
421
|
}
|
|
365
|
-
return { content: [{ type: "text", text: "Invalid action." }] };
|
|
422
|
+
return appendTelemetry({ content: [{ type: "text", text: "Invalid action." }] }, inputChars);
|
|
366
423
|
});
|
|
367
424
|
// ─── Tool 2: log_rejected_pattern ────────────────────────────────────────────
|
|
368
425
|
server.tool("log_rejected_pattern", "Record a coding pattern that was rejected by the team so it is never repeated. " +
|
|
@@ -390,6 +447,7 @@ server.tool("log_rejected_pattern", "Record a coding pattern that was rejected b
|
|
|
390
447
|
.optional()
|
|
391
448
|
.describe("Optional: the file or code area where this was observed."),
|
|
392
449
|
}, async ({ pattern, reason, category, file_context }) => {
|
|
450
|
+
const inputChars = JSON.stringify({ pattern, reason, category, file_context }).length;
|
|
393
451
|
const rejectedPath = resolve(REJECTED_FILE);
|
|
394
452
|
let records = [];
|
|
395
453
|
const existing = readFileSafe(rejectedPath);
|
|
@@ -411,7 +469,7 @@ server.tool("log_rejected_pattern", "Record a coding pattern that was rejected b
|
|
|
411
469
|
};
|
|
412
470
|
records.push(newEntry);
|
|
413
471
|
writeFile(rejectedPath, JSON.stringify(records, null, 2));
|
|
414
|
-
return {
|
|
472
|
+
return appendTelemetry({
|
|
415
473
|
content: [
|
|
416
474
|
{
|
|
417
475
|
type: "text",
|
|
@@ -422,7 +480,7 @@ server.tool("log_rejected_pattern", "Record a coding pattern that was rejected b
|
|
|
422
480
|
`This will be checked automatically in all future architectural suggestions.`,
|
|
423
481
|
},
|
|
424
482
|
],
|
|
425
|
-
};
|
|
483
|
+
}, inputChars);
|
|
426
484
|
});
|
|
427
485
|
// ─── Tool 3: run_pre_pr_checks ───────────────────────────────────────────────
|
|
428
486
|
server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks across the workspace. " +
|
|
@@ -448,6 +506,7 @@ server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks acr
|
|
|
448
506
|
.optional()
|
|
449
507
|
.describe("Explicitly skip specific checks. Use sparingly — document the reason in your PR description."),
|
|
450
508
|
}, async ({ work_item_id, root_dir, skip_checks = [] }) => {
|
|
509
|
+
const inputChars = JSON.stringify({ work_item_id, root_dir, skip_checks }).length;
|
|
451
510
|
const scanRoot = root_dir ? path.resolve(root_dir) : process.cwd();
|
|
452
511
|
const errors = [];
|
|
453
512
|
const warnings = [];
|
|
@@ -613,10 +672,10 @@ server.tool("run_pre_pr_checks", "Run all SecuFusion pre-PR guardrail checks acr
|
|
|
613
672
|
`- **Ready to raise PR** 🚀\n` +
|
|
614
673
|
rejectedReminder;
|
|
615
674
|
}
|
|
616
|
-
return {
|
|
675
|
+
return appendTelemetry({
|
|
617
676
|
content: [{ type: "text", text: report }],
|
|
618
677
|
isError: hasErrors,
|
|
619
|
-
};
|
|
678
|
+
}, inputChars);
|
|
620
679
|
});
|
|
621
680
|
// ─────────────────────────────────────────────
|
|
622
681
|
// Start transport
|