brakit 0.8.4 → 0.8.6

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.
@@ -9,20 +9,39 @@ import {
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
10
 
11
11
  // src/constants/routes.ts
12
- var DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
13
- var DASHBOARD_API_CLEAR = "/__brakit/api/clear";
14
- var DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
15
- var DASHBOARD_API_ERRORS = "/__brakit/api/errors";
16
- var DASHBOARD_API_QUERIES = "/__brakit/api/queries";
17
- var DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
18
- var DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
19
- var DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
20
- var DASHBOARD_API_SECURITY = "/__brakit/api/security";
21
- var DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
12
+ var DASHBOARD_PREFIX = "/__brakit";
13
+ var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
14
+ var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
15
+ var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
16
+ var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
17
+ var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
18
+ var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
19
+ var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
20
+ var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
21
+ var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
22
+ var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
23
+ var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
24
+ var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
25
+ var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
26
+ var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
27
+ var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
28
+ var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
29
+ var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
30
+ var VALID_TABS_TUPLE = [
31
+ "overview",
32
+ "actions",
33
+ "requests",
34
+ "fetches",
35
+ "queries",
36
+ "errors",
37
+ "logs",
38
+ "performance",
39
+ "security"
40
+ ];
41
+ var VALID_TABS = new Set(VALID_TABS_TUPLE);
22
42
 
23
43
  // src/constants/mcp.ts
24
44
  var MCP_SERVER_NAME = "brakit";
25
- var MCP_SERVER_VERSION = "0.8.4";
26
45
  var INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
27
46
  var LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
28
47
  var CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -32,6 +51,7 @@ var MAX_DISCOVERY_DEPTH = 5;
32
51
  var MAX_TIMELINE_EVENTS = 20;
33
52
  var MAX_RESOLVED_DISPLAY = 5;
34
53
  var ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
54
+ var MCP_SERVER_VERSION = "0.8.6";
35
55
 
36
56
  // src/mcp/client.ts
37
57
  var BrakitClient = class {
@@ -47,11 +67,11 @@ var BrakitClient = class {
47
67
  if (params?.offset) url.searchParams.set("offset", String(params.offset));
48
68
  return this.fetchJson(url);
49
69
  }
50
- async getSecurityFindings() {
51
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
52
- }
53
- async getInsights() {
54
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
70
+ async getIssues(params) {
71
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
72
+ if (params?.state) url.searchParams.set("state", params.state);
73
+ if (params?.category) url.searchParams.set("category", params.category);
74
+ return this.fetchJson(url);
55
75
  }
56
76
  async getQueries(requestId) {
57
77
  const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
@@ -79,6 +99,19 @@ var BrakitClient = class {
79
99
  if (state) url.searchParams.set("state", state);
80
100
  return this.fetchJson(url);
81
101
  }
102
+ async reportFix(findingId, status, notes) {
103
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json" },
106
+ body: JSON.stringify({ findingId, status, notes }),
107
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
108
+ });
109
+ if (!res.ok) return false;
110
+ const contentType = res.headers.get("content-type") ?? "";
111
+ if (!contentType.includes("application/json")) return false;
112
+ const body = await res.json();
113
+ return body.ok === true;
114
+ }
82
115
  async clearAll() {
83
116
  const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
84
117
  method: "POST",
@@ -108,59 +141,77 @@ var BrakitClient = class {
108
141
  };
109
142
 
110
143
  // src/mcp/discovery.ts
111
- import { readFileSync, existsSync, readdirSync, statSync } from "fs";
144
+ import { readFile, readdir, stat } from "fs/promises";
112
145
  import { resolve, dirname } from "path";
113
146
 
114
147
  // src/constants/limits.ts
115
- var MAX_INGEST_BYTES = 10 * 1024 * 1024;
148
+ var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
149
+
150
+ // src/constants/thresholds.ts
151
+ var STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
116
152
 
117
153
  // src/constants/metrics.ts
118
154
  var PORT_FILE = ".brakit/port";
119
155
 
120
- // src/constants/severity.ts
121
- var SEVERITY_CRITICAL = "critical";
122
- var SEVERITY_WARNING = "warning";
123
- var SEVERITY_INFO = "info";
124
- var SEVERITY_ICON_MAP = {
125
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
126
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
127
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
128
- };
156
+ // src/constants/network.ts
157
+ var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
158
+ var PORT_MIN = 1;
159
+ var PORT_MAX = 65535;
160
+
161
+ // src/constants/lifecycle.ts
162
+ var VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
163
+ var VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
164
+ var VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
165
+
166
+ // src/utils/log.ts
167
+ var PREFIX = "[brakit]";
168
+ function brakitDebug(message) {
169
+ if (process.env.DEBUG_BRAKIT) {
170
+ process.stderr.write(`${PREFIX}:debug ${message}
171
+ `);
172
+ }
173
+ }
129
174
 
130
175
  // src/mcp/discovery.ts
131
- function readPort(portPath) {
132
- if (!existsSync(portPath)) return null;
133
- const raw = readFileSync(portPath, "utf-8").trim();
134
- const port = parseInt(raw, 10);
135
- return isNaN(port) || port < 1 || port > 65535 ? null : port;
176
+ async function readPort(portPath) {
177
+ try {
178
+ const raw = (await readFile(portPath, "utf-8")).trim();
179
+ const port = parseInt(raw, 10);
180
+ return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
181
+ } catch {
182
+ return null;
183
+ }
136
184
  }
137
- function portInDir(dir) {
185
+ async function portInDir(dir) {
138
186
  return readPort(resolve(dir, PORT_FILE));
139
187
  }
140
- function portInChildren(dir) {
188
+ async function portInChildren(dir) {
141
189
  try {
142
- for (const entry of readdirSync(dir)) {
190
+ const entries = await readdir(dir);
191
+ for (const entry of entries) {
143
192
  if (entry.startsWith(".") || entry === "node_modules") continue;
144
193
  const child = resolve(dir, entry);
145
194
  try {
146
- if (!statSync(child).isDirectory()) continue;
147
- } catch {
195
+ if (!(await stat(child)).isDirectory()) continue;
196
+ } catch (err) {
197
+ brakitDebug(`discovery: stat failed for ${child}: ${err}`);
148
198
  continue;
149
199
  }
150
- const port = portInDir(child);
200
+ const port = await portInDir(child);
151
201
  if (port) return port;
152
202
  }
153
- } catch {
203
+ } catch (err) {
204
+ brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
154
205
  }
155
206
  return null;
156
207
  }
157
- function searchForPort(startDir) {
208
+ async function searchForPort(startDir) {
158
209
  const start = resolve(startDir);
159
- const initial = portInDir(start) ?? portInChildren(start);
210
+ const initial = await portInDir(start) ?? await portInChildren(start);
160
211
  if (initial) return initial;
161
212
  let dir = dirname(start);
162
213
  for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
163
- const port = portInDir(dir);
214
+ const port = await portInDir(dir) ?? await portInChildren(dir);
164
215
  if (port) return port;
165
216
  const parent = dirname(dir);
166
217
  if (parent === dir) break;
@@ -168,8 +219,8 @@ function searchForPort(startDir) {
168
219
  }
169
220
  return null;
170
221
  }
171
- function discoverBrakitPort(cwd) {
172
- const port = searchForPort(cwd ?? process.cwd());
222
+ async function discoverBrakitPort(cwd) {
223
+ const port = await searchForPort(cwd ?? process.cwd());
173
224
  if (!port) {
174
225
  throw new Error(
175
226
  "Brakit is not running. Start your app with brakit enabled first."
@@ -181,7 +232,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
181
232
  const deadline = Date.now() + timeoutMs;
182
233
  while (Date.now() < deadline) {
183
234
  try {
184
- const result = discoverBrakitPort(cwd);
235
+ const result = await discoverBrakitPort(cwd);
185
236
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
186
237
  if (res.ok) return result;
187
238
  } catch {
@@ -193,16 +244,6 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
193
244
  );
194
245
  }
195
246
 
196
- // src/mcp/enrichment.ts
197
- import { createHash as createHash2 } from "crypto";
198
-
199
- // src/store/finding-id.ts
200
- import { createHash } from "crypto";
201
- function computeFindingId(finding) {
202
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
203
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
204
- }
205
-
206
247
  // src/utils/endpoint.ts
207
248
  function parseEndpointKey(endpoint) {
208
249
  const spaceIdx = endpoint.indexOf(" ");
@@ -213,58 +254,48 @@ function parseEndpointKey(endpoint) {
213
254
  }
214
255
 
215
256
  // src/mcp/enrichment.ts
216
- function computeInsightId(type, endpoint, desc) {
217
- const key = `${type}:${endpoint}:${desc}`;
218
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
219
- }
220
257
  async function enrichFindings(client) {
221
- const [securityData, insightsData] = await Promise.all([
222
- client.getSecurityFindings(),
223
- client.getInsights()
224
- ]);
225
- const enriched = [];
226
- for (const f of securityData.findings) {
227
- let context = "";
228
- try {
229
- const { path } = parseEndpointKey(f.endpoint);
230
- const reqData = await client.getRequests({ search: path, limit: 1 });
231
- if (reqData.requests.length > 0) {
232
- const req = reqData.requests[0];
233
- if (req.id) {
234
- const activity = await client.getActivity(req.id);
235
- const queryCount = activity.counts?.queries ?? 0;
236
- const fetchCount = activity.counts?.fetches ?? 0;
237
- context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
258
+ const issuesData = await client.getIssues();
259
+ const issues = issuesData.issues.filter(
260
+ (si) => si.state !== "resolved" && si.state !== "stale"
261
+ );
262
+ const contexts = await Promise.all(
263
+ issues.map(async (si) => {
264
+ const endpoint = si.issue.endpoint;
265
+ if (!endpoint) return si.issue.detail ?? "";
266
+ try {
267
+ const { path } = parseEndpointKey(endpoint);
268
+ const reqData = await client.getRequests({ search: path, limit: 1 });
269
+ if (reqData.requests.length > 0) {
270
+ const req = reqData.requests[0];
271
+ if (req.id) {
272
+ const activity = await client.getActivity(req.id);
273
+ const queryCount = activity.counts?.queries ?? 0;
274
+ const fetchCount = activity.counts?.fetches ?? 0;
275
+ return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
276
+ }
238
277
  }
278
+ } catch {
279
+ return "(context unavailable)";
239
280
  }
240
- } catch {
241
- context = "(context unavailable)";
242
- }
243
- enriched.push({
244
- findingId: computeFindingId(f),
245
- severity: f.severity,
246
- title: f.title,
247
- endpoint: f.endpoint,
248
- description: f.desc,
249
- hint: f.hint,
250
- occurrences: f.count,
251
- context
252
- });
253
- }
254
- for (const si of insightsData.insights) {
255
- if (si.state === "resolved") continue;
256
- const i = si.insight;
257
- if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
258
- const endpoint = i.nav ?? "global";
281
+ return si.issue.detail ?? "";
282
+ })
283
+ );
284
+ const enriched = [];
285
+ for (let i = 0; i < issues.length; i++) {
286
+ const si = issues[i];
287
+ if (!ENRICHMENT_SEVERITY_FILTER.includes(si.issue.severity)) continue;
259
288
  enriched.push({
260
- findingId: computeInsightId(i.type, endpoint, i.desc),
261
- severity: i.severity,
262
- title: i.title,
263
- endpoint,
264
- description: i.desc,
265
- hint: i.hint,
266
- occurrences: 1,
267
- context: i.detail ?? ""
289
+ findingId: si.issueId,
290
+ severity: si.issue.severity,
291
+ title: si.issue.title,
292
+ endpoint: si.issue.endpoint ?? "global",
293
+ description: si.issue.desc,
294
+ hint: si.issue.hint,
295
+ occurrences: si.occurrences,
296
+ context: contexts[i],
297
+ aiStatus: si.aiStatus,
298
+ aiNotes: si.aiNotes
268
299
  });
269
300
  }
270
301
  return enriched;
@@ -317,9 +348,18 @@ async function buildRequestDetail(client, req) {
317
348
  };
318
349
  }
319
350
 
351
+ // src/utils/type-guards.ts
352
+ function isNonEmptyString(val) {
353
+ return typeof val === "string" && val.trim().length > 0;
354
+ }
355
+ function isValidIssueState(val) {
356
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
357
+ }
358
+ function isValidAiFixStatus(val) {
359
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
360
+ }
361
+
320
362
  // src/mcp/tools/get-findings.ts
321
- var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
322
- var VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
323
363
  var getFindings = {
324
364
  name: "get_findings",
325
365
  description: "Get all security findings and performance insights from the running app. Returns enriched findings with actionable fix hints, endpoint context, and evidence. Use this to understand what issues exist in the running application.",
@@ -333,28 +373,28 @@ var getFindings = {
333
373
  },
334
374
  state: {
335
375
  type: "string",
336
- enum: ["open", "fixing", "resolved"],
337
- description: "Filter by finding state (from finding lifecycle)"
376
+ enum: ["open", "fixing", "resolved", "stale", "regressed"],
377
+ description: "Filter by issue state"
338
378
  }
339
379
  }
340
380
  },
341
381
  async handler(client, args) {
342
382
  const severity = args.severity;
343
383
  const state = args.state;
344
- if (severity && !VALID_SEVERITIES.has(severity)) {
384
+ if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
345
385
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
346
386
  }
347
- if (state && !VALID_STATES.has(state)) {
348
- return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
387
+ if (state && !isValidIssueState(state)) {
388
+ return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
349
389
  }
350
390
  let findings = await enrichFindings(client);
351
391
  if (severity) {
352
392
  findings = findings.filter((f) => f.severity === severity);
353
393
  }
354
394
  if (state) {
355
- const stateful = await client.getFindings(state);
356
- const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
357
- findings = findings.filter((f) => statefulIds.has(f.findingId));
395
+ const issuesData = await client.getIssues({ state });
396
+ const issueIds = new Set(issuesData.issues.map((i) => i.issueId));
397
+ findings = findings.filter((f) => issueIds.has(f.findingId));
358
398
  }
359
399
  if (findings.length === 0) {
360
400
  return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
@@ -363,10 +403,18 @@ var getFindings = {
363
403
  `];
364
404
  for (const f of findings) {
365
405
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
406
+ lines.push(` ID: ${f.findingId}`);
366
407
  lines.push(` Endpoint: ${f.endpoint}`);
367
408
  lines.push(` Issue: ${f.description}`);
368
409
  if (f.context) lines.push(` Context: ${f.context}`);
369
410
  lines.push(` Fix: ${f.hint}`);
411
+ if (f.aiStatus === "fixed") {
412
+ lines.push(` AI Status: fixed (awaiting verification)`);
413
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
414
+ } else if (f.aiStatus === "wont_fix") {
415
+ lines.push(` AI Status: won't fix`);
416
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
417
+ }
370
418
  lines.push("");
371
419
  }
372
420
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -504,20 +552,21 @@ var verifyFix = {
504
552
  }
505
553
  if (findingId) {
506
554
  const data = await client.getFindings();
507
- const finding = data.findings.find((f) => f.findingId === findingId);
555
+ const finding = data.findings.find((f) => f.issueId === findingId);
508
556
  if (!finding) {
509
557
  return {
510
558
  content: [{
511
559
  type: "text",
512
560
  text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
513
- }]
561
+ }],
562
+ isError: true
514
563
  };
515
564
  }
516
565
  if (finding.state === "resolved") {
517
566
  return {
518
567
  content: [{
519
568
  type: "text",
520
- text: `RESOLVED: "${finding.finding.title}" on ${finding.finding.endpoint} is no longer detected. The fix worked.`
569
+ text: `RESOLVED: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"} is no longer detected. The fix worked.`
521
570
  }]
522
571
  };
523
572
  }
@@ -525,12 +574,12 @@ var verifyFix = {
525
574
  content: [{
526
575
  type: "text",
527
576
  text: [
528
- `STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
577
+ `STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
529
578
  ` State: ${finding.state}`,
530
579
  ` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
531
580
  ` Occurrences: ${finding.occurrences}`,
532
- ` Issue: ${finding.finding.desc}`,
533
- ` Hint: ${finding.finding.hint}`,
581
+ ` Issue: ${finding.issue.desc}`,
582
+ ` Hint: ${finding.issue.hint}`,
534
583
  "",
535
584
  "Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
536
585
  ].join("\n")
@@ -540,7 +589,7 @@ var verifyFix = {
540
589
  if (endpoint) {
541
590
  const data = await client.getFindings();
542
591
  const endpointFindings = data.findings.filter(
543
- (f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
592
+ (f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
544
593
  );
545
594
  if (endpointFindings.length === 0) {
546
595
  return {
@@ -550,7 +599,7 @@ var verifyFix = {
550
599
  }]
551
600
  };
552
601
  }
553
- const open = endpointFindings.filter((f) => f.state === "open");
602
+ const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
554
603
  const resolved = endpointFindings.filter((f) => f.state === "resolved");
555
604
  const lines = [
556
605
  `Endpoint: ${endpoint}`,
@@ -559,10 +608,10 @@ var verifyFix = {
559
608
  ""
560
609
  ];
561
610
  for (const f of open) {
562
- lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
611
+ lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
563
612
  }
564
613
  for (const f of resolved) {
565
- lines.push(` [resolved] ${f.finding.title}`);
614
+ lines.push(` [resolved] ${f.issue.title}`);
566
615
  }
567
616
  return { content: [{ type: "text", text: lines.join("\n") }] };
568
617
  }
@@ -570,7 +619,8 @@ var verifyFix = {
570
619
  content: [{
571
620
  type: "text",
572
621
  text: "Please provide either a finding_id or an endpoint to verify."
573
- }]
622
+ }],
623
+ isError: true
574
624
  };
575
625
  }
576
626
  };
@@ -584,51 +634,52 @@ var getReport = {
584
634
  properties: {}
585
635
  },
586
636
  async handler(client, _args) {
587
- const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
588
- client.getFindings(),
589
- client.getSecurityFindings(),
590
- client.getInsights(),
637
+ const [issuesData, metricsData] = await Promise.all([
638
+ client.getIssues(),
591
639
  client.getLiveMetrics()
592
640
  ]);
593
- const findings = findingsData.findings;
594
- const open = findings.filter((f) => f.state === "open");
595
- const resolved = findings.filter((f) => f.state === "resolved");
596
- const fixing = findings.filter((f) => f.state === "fixing");
597
- const criticalOpen = open.filter((f) => f.finding.severity === "critical");
598
- const warningOpen = open.filter((f) => f.finding.severity === "warning");
641
+ const issues = issuesData.issues;
642
+ const open = issues.filter((f) => f.state === "open" || f.state === "regressed");
643
+ const resolved = issues.filter((f) => f.state === "resolved");
644
+ const fixing = issues.filter((f) => f.state === "fixing");
645
+ const stale = issues.filter((f) => f.state === "stale");
646
+ const criticalOpen = open.filter((f) => f.issue.severity === "critical");
647
+ const warningOpen = open.filter((f) => f.issue.severity === "warning");
648
+ const securityIssues = issues.filter((f) => f.category === "security");
649
+ const perfIssues = issues.filter((f) => f.category === "performance");
599
650
  const totalRequests = metricsData.endpoints.reduce(
600
651
  (s, ep) => s + ep.summary.totalRequests,
601
652
  0
602
653
  );
603
- const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
604
654
  const lines = [
605
655
  "=== Brakit Report ===",
606
656
  "",
607
657
  `Endpoints observed: ${metricsData.endpoints.length}`,
608
658
  `Total requests captured: ${totalRequests}`,
609
- `Active security rules: ${securityData.findings.length} finding(s)`,
610
- `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
659
+ `Security issues: ${securityIssues.length}`,
660
+ `Performance issues: ${perfIssues.length}`,
611
661
  "",
612
- "--- Finding Summary ---",
613
- `Total: ${findings.length}`,
662
+ "--- Issue Summary ---",
663
+ `Total: ${issues.length}`,
614
664
  ` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
615
665
  ` In progress: ${fixing.length}`,
616
- ` Resolved: ${resolved.length}`
666
+ ` Resolved: ${resolved.length}`,
667
+ ` Stale: ${stale.length}`
617
668
  ];
618
669
  if (criticalOpen.length > 0) {
619
670
  lines.push("");
620
671
  lines.push("--- Critical Issues (fix first) ---");
621
672
  for (const f of criticalOpen) {
622
- lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
623
- lines.push(` ${f.finding.desc}`);
624
- lines.push(` Fix: ${f.finding.hint}`);
673
+ lines.push(` [CRITICAL] ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
674
+ lines.push(` ${f.issue.desc}`);
675
+ lines.push(` Fix: ${f.issue.hint}`);
625
676
  }
626
677
  }
627
678
  if (resolved.length > 0) {
628
679
  lines.push("");
629
680
  lines.push("--- Recently Resolved ---");
630
681
  for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
631
- lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
682
+ lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
632
683
  }
633
684
  if (resolved.length > MAX_RESOLVED_DISPLAY) {
634
685
  lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
@@ -659,9 +710,57 @@ var clearFindings = {
659
710
  }
660
711
  };
661
712
 
713
+ // src/mcp/tools/report-fix.ts
714
+ var reportFix = {
715
+ name: "report_fix",
716
+ description: "Report the result of fixing a brakit finding. Call this after attempting to fix each finding to update the dashboard with the outcome. Use status 'fixed' when you've applied a fix, or 'wont_fix' when the issue can't be resolved (e.g. third-party library, by design).",
717
+ inputSchema: {
718
+ type: "object",
719
+ properties: {
720
+ finding_id: {
721
+ type: "string",
722
+ description: "The finding ID to report on"
723
+ },
724
+ status: {
725
+ type: "string",
726
+ description: "Whether the fix was applied or can't be fixed",
727
+ enum: ["fixed", "wont_fix"]
728
+ },
729
+ summary: {
730
+ type: "string",
731
+ description: "Brief description of what was done or why it can't be fixed"
732
+ }
733
+ },
734
+ required: ["finding_id", "status", "summary"]
735
+ },
736
+ async handler(client, args) {
737
+ const { finding_id, status, summary } = args;
738
+ if (!isNonEmptyString(finding_id)) {
739
+ return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
740
+ }
741
+ if (!isValidAiFixStatus(status)) {
742
+ return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
743
+ }
744
+ if (!isNonEmptyString(summary)) {
745
+ return { content: [{ type: "text", text: "summary is required." }], isError: true };
746
+ }
747
+ const ok = await client.reportFix(finding_id, status, summary);
748
+ if (!ok) {
749
+ return {
750
+ content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
751
+ isError: true
752
+ };
753
+ }
754
+ const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
755
+ return {
756
+ content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
757
+ };
758
+ }
759
+ };
760
+
662
761
  // src/mcp/tools/index.ts
663
762
  var TOOL_MAP = new Map(
664
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
763
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
665
764
  );
666
765
  function getToolDefinitions() {
667
766
  return [...TOOL_MAP.values()].map((t) => ({
@@ -682,6 +781,7 @@ function handleToolCall(client, name, args) {
682
781
  }
683
782
 
684
783
  // src/mcp/prompts.ts
784
+ var SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing any brakit finding, always call the report_fix tool with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did. This updates the dashboard in real-time so the developer can track AI-driven fixes.";
685
785
  var PROMPTS = [
686
786
  {
687
787
  name: "check-app",
@@ -693,18 +793,8 @@ var PROMPTS = [
693
793
  }
694
794
  ];
695
795
  var PROMPT_MESSAGES = {
696
- "check-app": [
697
- "Check my running app for security and performance issues using brakit.",
698
- "First get all findings, then get the endpoint summary.",
699
- "For any critical or warning findings, get the request detail to understand the root cause.",
700
- "Give me a clear report of what's wrong and offer to fix each issue."
701
- ].join(" "),
702
- "fix-findings": [
703
- "Get all open brakit findings.",
704
- "For each finding, get the request detail to understand the exact issue.",
705
- "Then find the source code responsible and fix it.",
706
- "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
707
- ].join(" ")
796
+ "check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing any issue, call report_fix with the finding_id, status, and summary.",
797
+ "fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Call report_fix with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did\n4. Move to the next finding\n\nAfter all findings are processed, ask me to re-trigger the endpoints so brakit can verify the fixes."
708
798
  };
709
799
 
710
800
  // src/mcp/server.ts
@@ -718,7 +808,7 @@ async function startMcpServer() {
718
808
  let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
719
809
  const server = new Server(
720
810
  { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
721
- { capabilities: { tools: {}, prompts: {} } }
811
+ { capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
722
812
  );
723
813
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
724
814
  prompts: [...PROMPTS]