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.
- package/README.md +3 -3
- package/dist/api.d.ts +133 -111
- package/dist/api.js +468 -327
- package/dist/bin/brakit.js +864 -448
- package/dist/dashboard.html +2653 -0
- package/dist/mcp/server.js +248 -158
- package/dist/runtime/index.js +1357 -783
- package/package.json +3 -2
package/dist/mcp/server.js
CHANGED
|
@@ -9,20 +9,39 @@ import {
|
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
|
|
11
11
|
// src/constants/routes.ts
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
var
|
|
17
|
-
var
|
|
18
|
-
var
|
|
19
|
-
var
|
|
20
|
-
var
|
|
21
|
-
var
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return this.fetchJson(
|
|
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 {
|
|
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
|
|
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/
|
|
121
|
-
var
|
|
122
|
-
var
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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:
|
|
261
|
-
severity:
|
|
262
|
-
title:
|
|
263
|
-
endpoint,
|
|
264
|
-
description:
|
|
265
|
-
hint:
|
|
266
|
-
occurrences:
|
|
267
|
-
context: i
|
|
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
|
|
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 && !
|
|
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 && !
|
|
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
|
|
356
|
-
const
|
|
357
|
-
findings = findings.filter((f) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
533
|
-
` Hint: ${finding.
|
|
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.
|
|
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.
|
|
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.
|
|
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 [
|
|
588
|
-
client.
|
|
589
|
-
client.getSecurityFindings(),
|
|
590
|
-
client.getInsights(),
|
|
637
|
+
const [issuesData, metricsData] = await Promise.all([
|
|
638
|
+
client.getIssues(),
|
|
591
639
|
client.getLiveMetrics()
|
|
592
640
|
]);
|
|
593
|
-
const
|
|
594
|
-
const open =
|
|
595
|
-
const resolved =
|
|
596
|
-
const fixing =
|
|
597
|
-
const
|
|
598
|
-
const
|
|
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
|
-
`
|
|
610
|
-
`Performance
|
|
659
|
+
`Security issues: ${securityIssues.length}`,
|
|
660
|
+
`Performance issues: ${perfIssues.length}`,
|
|
611
661
|
"",
|
|
612
|
-
"---
|
|
613
|
-
`Total: ${
|
|
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.
|
|
623
|
-
lines.push(` ${f.
|
|
624
|
-
lines.push(` Fix: ${f.
|
|
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.
|
|
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
|
-
|
|
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]
|