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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/index.js +77 -18
  3. 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 both files to your repo so the entire team benefits from the shared knowledge.
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
- return {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",