brakit 0.7.5 → 0.8.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.
@@ -1,17 +1,944 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/constants/routes.ts
13
+ var DASHBOARD_API_REQUESTS, DASHBOARD_API_CLEAR, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_FINDINGS;
14
+ var init_routes = __esm({
15
+ "src/constants/routes.ts"() {
16
+ "use strict";
17
+ DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
18
+ DASHBOARD_API_CLEAR = "/__brakit/api/clear";
19
+ DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
20
+ DASHBOARD_API_ERRORS = "/__brakit/api/errors";
21
+ DASHBOARD_API_QUERIES = "/__brakit/api/queries";
22
+ DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
23
+ DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
24
+ DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
25
+ DASHBOARD_API_SECURITY = "/__brakit/api/security";
26
+ DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
27
+ }
28
+ });
29
+
30
+ // src/constants/limits.ts
31
+ var MAX_REQUEST_ENTRIES, MAX_TELEMETRY_ENTRIES;
32
+ var init_limits = __esm({
33
+ "src/constants/limits.ts"() {
34
+ "use strict";
35
+ MAX_REQUEST_ENTRIES = 1e3;
36
+ MAX_TELEMETRY_ENTRIES = 1e3;
37
+ }
38
+ });
39
+
40
+ // src/constants/thresholds.ts
41
+ var OVERFETCH_UNWRAP_MIN_SIZE;
42
+ var init_thresholds = __esm({
43
+ "src/constants/thresholds.ts"() {
44
+ "use strict";
45
+ OVERFETCH_UNWRAP_MIN_SIZE = 3;
46
+ }
47
+ });
48
+
49
+ // src/constants/transport.ts
50
+ var init_transport = __esm({
51
+ "src/constants/transport.ts"() {
52
+ "use strict";
53
+ }
54
+ });
55
+
56
+ // src/constants/metrics.ts
57
+ var METRICS_DIR, PORT_FILE;
58
+ var init_metrics = __esm({
59
+ "src/constants/metrics.ts"() {
60
+ "use strict";
61
+ METRICS_DIR = ".brakit";
62
+ PORT_FILE = ".brakit/port";
63
+ }
64
+ });
65
+
66
+ // src/constants/headers.ts
67
+ var init_headers = __esm({
68
+ "src/constants/headers.ts"() {
69
+ "use strict";
70
+ }
71
+ });
72
+
73
+ // src/constants/network.ts
74
+ var init_network = __esm({
75
+ "src/constants/network.ts"() {
76
+ "use strict";
77
+ }
78
+ });
79
+
80
+ // src/constants/mcp.ts
81
+ var MCP_SERVER_NAME, MCP_SERVER_VERSION, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
82
+ var init_mcp = __esm({
83
+ "src/constants/mcp.ts"() {
84
+ "use strict";
85
+ MCP_SERVER_NAME = "brakit";
86
+ MCP_SERVER_VERSION = "0.8.0";
87
+ INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
88
+ LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
89
+ CLIENT_FETCH_TIMEOUT_MS = 1e4;
90
+ HEALTH_CHECK_TIMEOUT_MS = 3e3;
91
+ DISCOVERY_POLL_INTERVAL_MS = 500;
92
+ MAX_TIMELINE_EVENTS = 20;
93
+ MAX_RESOLVED_DISPLAY = 5;
94
+ ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
95
+ }
96
+ });
97
+
98
+ // src/constants/index.ts
99
+ var init_constants = __esm({
100
+ "src/constants/index.ts"() {
101
+ "use strict";
102
+ init_routes();
103
+ init_limits();
104
+ init_thresholds();
105
+ init_transport();
106
+ init_metrics();
107
+ init_headers();
108
+ init_network();
109
+ init_mcp();
110
+ }
111
+ });
112
+
113
+ // src/store/finding-id.ts
114
+ import { createHash } from "crypto";
115
+ function computeFindingId(finding) {
116
+ const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
117
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
118
+ }
119
+ var init_finding_id = __esm({
120
+ "src/store/finding-id.ts"() {
121
+ "use strict";
122
+ }
123
+ });
124
+
125
+ // src/utils/endpoint.ts
126
+ function parseEndpointKey(endpoint) {
127
+ const spaceIdx = endpoint.indexOf(" ");
128
+ if (spaceIdx > 0) {
129
+ return { method: endpoint.slice(0, spaceIdx), path: endpoint.slice(spaceIdx + 1) };
130
+ }
131
+ return { path: endpoint };
132
+ }
133
+ var init_endpoint = __esm({
134
+ "src/utils/endpoint.ts"() {
135
+ "use strict";
136
+ }
137
+ });
138
+
139
+ // src/mcp/client.ts
140
+ var BrakitClient;
141
+ var init_client = __esm({
142
+ "src/mcp/client.ts"() {
143
+ "use strict";
144
+ init_routes();
145
+ init_mcp();
146
+ BrakitClient = class {
147
+ constructor(baseUrl) {
148
+ this.baseUrl = baseUrl;
149
+ }
150
+ async getRequests(params) {
151
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_REQUESTS}`);
152
+ if (params?.method) url.searchParams.set("method", params.method);
153
+ if (params?.status) url.searchParams.set("status", params.status);
154
+ if (params?.search) url.searchParams.set("search", params.search);
155
+ if (params?.limit) url.searchParams.set("limit", String(params.limit));
156
+ if (params?.offset) url.searchParams.set("offset", String(params.offset));
157
+ return this.fetchJson(url);
158
+ }
159
+ async getSecurityFindings() {
160
+ return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
161
+ }
162
+ async getInsights() {
163
+ return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
164
+ }
165
+ async getQueries(requestId) {
166
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
167
+ if (requestId) url.searchParams.set("requestId", requestId);
168
+ return this.fetchJson(url);
169
+ }
170
+ async getFetches(requestId) {
171
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_FETCHES}`);
172
+ if (requestId) url.searchParams.set("requestId", requestId);
173
+ return this.fetchJson(url);
174
+ }
175
+ async getErrors() {
176
+ return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_ERRORS}`);
177
+ }
178
+ async getActivity(requestId) {
179
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_ACTIVITY}`);
180
+ url.searchParams.set("requestId", requestId);
181
+ return this.fetchJson(url);
182
+ }
183
+ async getLiveMetrics() {
184
+ return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_METRICS_LIVE}`);
185
+ }
186
+ async getFindings(state) {
187
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_FINDINGS}`);
188
+ if (state) url.searchParams.set("state", state);
189
+ return this.fetchJson(url);
190
+ }
191
+ async clearAll() {
192
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
193
+ method: "POST",
194
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
195
+ });
196
+ return res.ok;
197
+ }
198
+ async isAlive() {
199
+ try {
200
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`, {
201
+ signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
202
+ });
203
+ return res.ok;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ async fetchJson(url) {
209
+ const res = await fetch(url.toString(), {
210
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
211
+ });
212
+ if (!res.ok) {
213
+ throw new Error(`Brakit API error: ${res.status} ${res.statusText}`);
214
+ }
215
+ return res.json();
216
+ }
217
+ };
218
+ }
219
+ });
220
+
221
+ // src/mcp/discovery.ts
222
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
223
+ import { resolve as resolve6 } from "path";
224
+ function discoverBrakitPort(cwd) {
225
+ const root = cwd ?? process.cwd();
226
+ const portPath = resolve6(root, PORT_FILE);
227
+ if (!existsSync4(portPath)) {
228
+ throw new Error(
229
+ `Brakit is not running. No port file found at ${portPath}.
230
+ Start your app with brakit enabled first.`
231
+ );
232
+ }
233
+ const raw = readFileSync4(portPath, "utf-8").trim();
234
+ const port = parseInt(raw, 10);
235
+ if (isNaN(port) || port < 1 || port > 65535) {
236
+ throw new Error(`Invalid port in ${portPath}: "${raw}"`);
237
+ }
238
+ return { port, baseUrl: `http://localhost:${port}` };
239
+ }
240
+ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
241
+ const deadline = Date.now() + timeoutMs;
242
+ while (Date.now() < deadline) {
243
+ try {
244
+ const result = discoverBrakitPort(cwd);
245
+ const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
246
+ if (res.ok) return result;
247
+ } catch {
248
+ }
249
+ await new Promise((r) => setTimeout(r, pollMs));
250
+ }
251
+ throw new Error(
252
+ "Timed out waiting for Brakit to start. Is your app running with brakit enabled?"
253
+ );
254
+ }
255
+ var init_discovery = __esm({
256
+ "src/mcp/discovery.ts"() {
257
+ "use strict";
258
+ init_constants();
259
+ init_mcp();
260
+ }
261
+ });
262
+
263
+ // src/mcp/enrichment.ts
264
+ import { createHash as createHash2 } from "crypto";
265
+ function computeInsightId(type, endpoint, desc) {
266
+ const key = `${type}:${endpoint}:${desc}`;
267
+ return createHash2("sha256").update(key).digest("hex").slice(0, 16);
268
+ }
269
+ async function enrichFindings(client) {
270
+ const [securityData, insightsData] = await Promise.all([
271
+ client.getSecurityFindings(),
272
+ client.getInsights()
273
+ ]);
274
+ const enriched = [];
275
+ for (const f of securityData.findings) {
276
+ let context = "";
277
+ try {
278
+ const { path } = parseEndpointKey(f.endpoint);
279
+ const reqData = await client.getRequests({ search: path, limit: 1 });
280
+ if (reqData.requests.length > 0) {
281
+ const req = reqData.requests[0];
282
+ if (req.id) {
283
+ const activity = await client.getActivity(req.id);
284
+ const queryCount = activity.counts?.queries ?? 0;
285
+ const fetchCount = activity.counts?.fetches ?? 0;
286
+ context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
287
+ }
288
+ }
289
+ } catch {
290
+ context = "(context unavailable)";
291
+ }
292
+ enriched.push({
293
+ findingId: computeFindingId(f),
294
+ severity: f.severity,
295
+ title: f.title,
296
+ endpoint: f.endpoint,
297
+ description: f.desc,
298
+ hint: f.hint,
299
+ occurrences: f.count,
300
+ context
301
+ });
302
+ }
303
+ for (const i of insightsData.insights) {
304
+ if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
305
+ const endpoint = i.nav ?? "global";
306
+ enriched.push({
307
+ findingId: computeInsightId(i.type, endpoint, i.desc),
308
+ severity: i.severity,
309
+ title: i.title,
310
+ endpoint,
311
+ description: i.desc,
312
+ hint: i.hint,
313
+ occurrences: 1,
314
+ context: i.detail ?? ""
315
+ });
316
+ }
317
+ return enriched;
318
+ }
319
+ async function enrichEndpoints(client, sortBy) {
320
+ const data = await client.getLiveMetrics();
321
+ const endpoints = data.endpoints.map((ep) => ({
322
+ ...ep.summary,
323
+ endpoint: ep.endpoint
324
+ }));
325
+ if (sortBy === "error_rate") {
326
+ endpoints.sort((a, b) => b.errorRate - a.errorRate);
327
+ } else if (sortBy === "query_count") {
328
+ endpoints.sort((a, b) => b.avgQueryCount - a.avgQueryCount);
329
+ } else if (sortBy === "requests") {
330
+ endpoints.sort((a, b) => b.totalRequests - a.totalRequests);
331
+ }
332
+ return endpoints;
333
+ }
334
+ async function enrichRequestDetail(client, opts) {
335
+ if (opts.requestId) {
336
+ const data = await client.getRequests({ search: opts.requestId, limit: 1 });
337
+ if (data.requests.length > 0) {
338
+ return buildRequestDetail(client, data.requests[0].id);
339
+ }
340
+ } else if (opts.endpoint) {
341
+ const { method, path } = parseEndpointKey(opts.endpoint);
342
+ const data = await client.getRequests({ method, search: path, limit: 1 });
343
+ if (data.requests.length > 0) {
344
+ return buildRequestDetail(client, data.requests[0].id);
345
+ }
346
+ }
347
+ return null;
348
+ }
349
+ async function buildRequestDetail(client, requestId) {
350
+ const [reqData, activity, queries, fetches] = await Promise.all([
351
+ client.getRequests({ search: requestId, limit: 1 }),
352
+ client.getActivity(requestId),
353
+ client.getQueries(requestId),
354
+ client.getFetches(requestId)
355
+ ]);
356
+ const req = reqData.requests[0];
357
+ return {
358
+ id: requestId,
359
+ method: req.method,
360
+ url: req.url,
361
+ statusCode: req.statusCode,
362
+ durationMs: req.durationMs,
363
+ queries: queries.entries,
364
+ fetches: fetches.entries,
365
+ timeline: activity.timeline
366
+ };
367
+ }
368
+ var init_enrichment = __esm({
369
+ "src/mcp/enrichment.ts"() {
370
+ "use strict";
371
+ init_mcp();
372
+ init_finding_id();
373
+ init_endpoint();
374
+ }
375
+ });
376
+
377
+ // src/mcp/tools/get-findings.ts
378
+ var VALID_SEVERITIES, VALID_STATES, getFindings;
379
+ var init_get_findings = __esm({
380
+ "src/mcp/tools/get-findings.ts"() {
381
+ "use strict";
382
+ init_enrichment();
383
+ VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
384
+ VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
385
+ getFindings = {
386
+ name: "get_findings",
387
+ 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.",
388
+ inputSchema: {
389
+ type: "object",
390
+ properties: {
391
+ severity: {
392
+ type: "string",
393
+ enum: ["critical", "warning"],
394
+ description: "Filter by severity level"
395
+ },
396
+ state: {
397
+ type: "string",
398
+ enum: ["open", "fixing", "resolved"],
399
+ description: "Filter by finding state (from finding lifecycle)"
400
+ }
401
+ }
402
+ },
403
+ async handler(client, args) {
404
+ const severity = args.severity;
405
+ const state = args.state;
406
+ if (severity && !VALID_SEVERITIES.has(severity)) {
407
+ return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
408
+ }
409
+ if (state && !VALID_STATES.has(state)) {
410
+ return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
411
+ }
412
+ let findings = await enrichFindings(client);
413
+ if (severity) {
414
+ findings = findings.filter((f) => f.severity === severity);
415
+ }
416
+ if (state) {
417
+ const stateful = await client.getFindings(state);
418
+ const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
419
+ findings = findings.filter((f) => statefulIds.has(f.findingId));
420
+ }
421
+ if (findings.length === 0) {
422
+ return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
423
+ }
424
+ const lines = [`Found ${findings.length} issue(s):
425
+ `];
426
+ for (const f of findings) {
427
+ lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
428
+ lines.push(` Endpoint: ${f.endpoint}`);
429
+ lines.push(` Issue: ${f.description}`);
430
+ if (f.context) lines.push(` Context: ${f.context}`);
431
+ lines.push(` Fix: ${f.hint}`);
432
+ lines.push("");
433
+ }
434
+ return { content: [{ type: "text", text: lines.join("\n") }] };
435
+ }
436
+ };
437
+ }
438
+ });
439
+
440
+ // src/mcp/tools/get-endpoints.ts
441
+ var VALID_SORT_KEYS, getEndpoints;
442
+ var init_get_endpoints = __esm({
443
+ "src/mcp/tools/get-endpoints.ts"() {
444
+ "use strict";
445
+ init_enrichment();
446
+ VALID_SORT_KEYS = /* @__PURE__ */ new Set(["p95", "error_rate", "query_count", "requests"]);
447
+ getEndpoints = {
448
+ name: "get_endpoints",
449
+ description: "Get a summary of all observed API endpoints with performance stats. Shows p95 latency, error rate, query count, and time breakdown for each endpoint. Use this to identify which endpoints need attention.",
450
+ inputSchema: {
451
+ type: "object",
452
+ properties: {
453
+ sort_by: {
454
+ type: "string",
455
+ enum: ["p95", "error_rate", "query_count", "requests"],
456
+ description: "Sort endpoints by this metric (default: p95 latency)"
457
+ }
458
+ }
459
+ },
460
+ async handler(client, args) {
461
+ const sortBy = args.sort_by;
462
+ if (sortBy && !VALID_SORT_KEYS.has(sortBy)) {
463
+ return { content: [{ type: "text", text: `Invalid sort_by "${sortBy}". Use: p95, error_rate, query_count, requests.` }], isError: true };
464
+ }
465
+ const endpoints = await enrichEndpoints(client, sortBy);
466
+ if (endpoints.length === 0) {
467
+ return {
468
+ content: [{ type: "text", text: "No endpoints observed yet. Make some requests to your app first." }]
469
+ };
470
+ }
471
+ const lines = [`${endpoints.length} endpoint(s) observed:
472
+ `];
473
+ for (const ep of endpoints) {
474
+ lines.push(`${ep.endpoint}`);
475
+ lines.push(` p95: ${ep.p95Ms}ms | Errors: ${(ep.errorRate * 100).toFixed(1)}% | Queries: ${ep.avgQueryCount}/req | Requests: ${ep.totalRequests}`);
476
+ lines.push(` Time breakdown: DB ${ep.avgQueryTimeMs}ms + Fetch ${ep.avgFetchTimeMs}ms + App ${ep.avgAppTimeMs}ms`);
477
+ lines.push("");
478
+ }
479
+ return { content: [{ type: "text", text: lines.join("\n") }] };
480
+ }
481
+ };
482
+ }
483
+ });
484
+
485
+ // src/mcp/tools/get-request-detail.ts
486
+ var getRequestDetail;
487
+ var init_get_request_detail = __esm({
488
+ "src/mcp/tools/get-request-detail.ts"() {
489
+ "use strict";
490
+ init_mcp();
491
+ init_enrichment();
492
+ getRequestDetail = {
493
+ name: "get_request_detail",
494
+ description: "Get full details of a specific HTTP request including all DB queries it fired, all fetches it made, the response, and a timeline of events. Use this to deeply understand what happens when a specific endpoint is hit.",
495
+ inputSchema: {
496
+ type: "object",
497
+ properties: {
498
+ request_id: {
499
+ type: "string",
500
+ description: "The specific request ID to look up"
501
+ },
502
+ endpoint: {
503
+ type: "string",
504
+ description: "Alternatively, get the latest request for an endpoint like 'GET /api/users'"
505
+ }
506
+ }
507
+ },
508
+ async handler(client, args) {
509
+ const requestId = args.request_id;
510
+ const endpoint = args.endpoint;
511
+ if (!requestId && !endpoint) {
512
+ return {
513
+ content: [{ type: "text", text: "Please provide either a request_id or an endpoint (e.g. 'GET /api/users')." }]
514
+ };
515
+ }
516
+ const detail = await enrichRequestDetail(client, { requestId, endpoint });
517
+ if (!detail) {
518
+ return {
519
+ content: [{ type: "text", text: `No request found for ${requestId ?? endpoint}.` }]
520
+ };
521
+ }
522
+ const lines = [
523
+ `Request: ${detail.method} ${detail.url}`,
524
+ `Status: ${detail.statusCode}`,
525
+ `Duration: ${detail.durationMs}ms`,
526
+ ""
527
+ ];
528
+ if (detail.queries.length > 0) {
529
+ lines.push(`DB Queries (${detail.queries.length}):`);
530
+ for (const q of detail.queries) {
531
+ const sql = q.sql ?? `${q.operation} ${q.table ?? q.model ?? ""}`;
532
+ lines.push(` [${q.durationMs}ms] ${sql}`);
533
+ }
534
+ lines.push("");
535
+ }
536
+ if (detail.fetches.length > 0) {
537
+ lines.push(`Outgoing Fetches (${detail.fetches.length}):`);
538
+ for (const f of detail.fetches) {
539
+ lines.push(` [${f.durationMs}ms] ${f.method} ${f.url} \u2192 ${f.statusCode}`);
540
+ }
541
+ lines.push("");
542
+ }
543
+ if (detail.timeline.length > 0) {
544
+ lines.push(`Timeline (${detail.timeline.length} events):`);
545
+ for (const event of detail.timeline.slice(0, MAX_TIMELINE_EVENTS)) {
546
+ lines.push(` ${event.type}: ${JSON.stringify(event.data)}`);
547
+ }
548
+ if (detail.timeline.length > MAX_TIMELINE_EVENTS) {
549
+ lines.push(` ... and ${detail.timeline.length - MAX_TIMELINE_EVENTS} more events`);
550
+ }
551
+ }
552
+ return { content: [{ type: "text", text: lines.join("\n") }] };
553
+ }
554
+ };
555
+ }
556
+ });
557
+
558
+ // src/mcp/tools/verify-fix.ts
559
+ var verifyFix;
560
+ var init_verify_fix = __esm({
561
+ "src/mcp/tools/verify-fix.ts"() {
562
+ "use strict";
563
+ verifyFix = {
564
+ name: "verify_fix",
565
+ description: "Verify whether a previously found security or performance issue has been resolved. After you fix code, the user should trigger the endpoint again, then call this tool to check if the finding still appears in Brakit's analysis.",
566
+ inputSchema: {
567
+ type: "object",
568
+ properties: {
569
+ finding_id: {
570
+ type: "string",
571
+ description: "The finding ID to verify"
572
+ },
573
+ endpoint: {
574
+ type: "string",
575
+ description: "Alternatively, check if a specific endpoint still has issues (e.g. 'GET /api/users')"
576
+ }
577
+ }
578
+ },
579
+ async handler(client, args) {
580
+ const findingId = args.finding_id;
581
+ const endpoint = args.endpoint;
582
+ if (findingId !== void 0 && findingId.trim() === "") {
583
+ return { content: [{ type: "text", text: "finding_id cannot be empty." }], isError: true };
584
+ }
585
+ if (endpoint !== void 0 && endpoint.trim() === "") {
586
+ return { content: [{ type: "text", text: "endpoint cannot be empty." }], isError: true };
587
+ }
588
+ if (findingId) {
589
+ const data = await client.getFindings();
590
+ const finding = data.findings.find((f) => f.findingId === findingId);
591
+ if (!finding) {
592
+ return {
593
+ content: [{
594
+ type: "text",
595
+ text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
596
+ }]
597
+ };
598
+ }
599
+ if (finding.state === "resolved") {
600
+ return {
601
+ content: [{
602
+ type: "text",
603
+ text: `RESOLVED: "${finding.finding.title}" on ${finding.finding.endpoint} is no longer detected. The fix worked.`
604
+ }]
605
+ };
606
+ }
607
+ return {
608
+ content: [{
609
+ type: "text",
610
+ text: [
611
+ `STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
612
+ ` State: ${finding.state}`,
613
+ ` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
614
+ ` Occurrences: ${finding.occurrences}`,
615
+ ` Issue: ${finding.finding.desc}`,
616
+ ` Hint: ${finding.finding.hint}`,
617
+ "",
618
+ "Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
619
+ ].join("\n")
620
+ }]
621
+ };
622
+ }
623
+ if (endpoint) {
624
+ const data = await client.getFindings();
625
+ const endpointFindings = data.findings.filter(
626
+ (f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
627
+ );
628
+ if (endpointFindings.length === 0) {
629
+ return {
630
+ content: [{
631
+ type: "text",
632
+ text: `No findings found for endpoint "${endpoint}". Either it's clean or it hasn't been analyzed yet.`
633
+ }]
634
+ };
635
+ }
636
+ const open = endpointFindings.filter((f) => f.state === "open");
637
+ const resolved = endpointFindings.filter((f) => f.state === "resolved");
638
+ const lines = [
639
+ `Endpoint: ${endpoint}`,
640
+ `Open issues: ${open.length}`,
641
+ `Resolved: ${resolved.length}`,
642
+ ""
643
+ ];
644
+ for (const f of open) {
645
+ lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
646
+ }
647
+ for (const f of resolved) {
648
+ lines.push(` [resolved] ${f.finding.title}`);
649
+ }
650
+ return { content: [{ type: "text", text: lines.join("\n") }] };
651
+ }
652
+ return {
653
+ content: [{
654
+ type: "text",
655
+ text: "Please provide either a finding_id or an endpoint to verify."
656
+ }]
657
+ };
658
+ }
659
+ };
660
+ }
661
+ });
662
+
663
+ // src/mcp/tools/get-report.ts
664
+ var getReport;
665
+ var init_get_report = __esm({
666
+ "src/mcp/tools/get-report.ts"() {
667
+ "use strict";
668
+ init_mcp();
669
+ getReport = {
670
+ name: "get_report",
671
+ description: "Generate a summary report of all findings: total found, open, resolved. Use this to get a high-level overview of the application's health.",
672
+ inputSchema: {
673
+ type: "object",
674
+ properties: {}
675
+ },
676
+ async handler(client, _args) {
677
+ const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
678
+ client.getFindings(),
679
+ client.getSecurityFindings(),
680
+ client.getInsights(),
681
+ client.getLiveMetrics()
682
+ ]);
683
+ const findings = findingsData.findings;
684
+ const open = findings.filter((f) => f.state === "open");
685
+ const resolved = findings.filter((f) => f.state === "resolved");
686
+ const fixing = findings.filter((f) => f.state === "fixing");
687
+ const criticalOpen = open.filter((f) => f.finding.severity === "critical");
688
+ const warningOpen = open.filter((f) => f.finding.severity === "warning");
689
+ const totalRequests = metricsData.endpoints.reduce(
690
+ (s, ep) => s + ep.summary.totalRequests,
691
+ 0
692
+ );
693
+ const lines = [
694
+ "=== Brakit Report ===",
695
+ "",
696
+ `Endpoints observed: ${metricsData.endpoints.length}`,
697
+ `Total requests captured: ${totalRequests}`,
698
+ `Active security rules: ${securityData.findings.length} finding(s)`,
699
+ `Performance insights: ${insightsData.insights.length} insight(s)`,
700
+ "",
701
+ "--- Finding Summary ---",
702
+ `Total: ${findings.length}`,
703
+ ` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
704
+ ` In progress: ${fixing.length}`,
705
+ ` Resolved: ${resolved.length}`
706
+ ];
707
+ if (criticalOpen.length > 0) {
708
+ lines.push("");
709
+ lines.push("--- Critical Issues (fix first) ---");
710
+ for (const f of criticalOpen) {
711
+ lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
712
+ lines.push(` ${f.finding.desc}`);
713
+ lines.push(` Fix: ${f.finding.hint}`);
714
+ }
715
+ }
716
+ if (resolved.length > 0) {
717
+ lines.push("");
718
+ lines.push("--- Recently Resolved ---");
719
+ for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
720
+ lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
721
+ }
722
+ if (resolved.length > MAX_RESOLVED_DISPLAY) {
723
+ lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
724
+ }
725
+ }
726
+ return { content: [{ type: "text", text: lines.join("\n") }] };
727
+ }
728
+ };
729
+ }
730
+ });
731
+
732
+ // src/mcp/tools/clear-findings.ts
733
+ var clearFindings;
734
+ var init_clear_findings = __esm({
735
+ "src/mcp/tools/clear-findings.ts"() {
736
+ "use strict";
737
+ clearFindings = {
738
+ name: "clear_findings",
739
+ description: "Reset finding history for a fresh session. Use this when you want to start tracking findings from scratch.",
740
+ inputSchema: {
741
+ type: "object",
742
+ properties: {}
743
+ },
744
+ async handler(client, _args) {
745
+ const ok = await client.clearAll();
746
+ if (!ok) {
747
+ return {
748
+ content: [{ type: "text", text: "Failed to clear findings. Is the app still running?" }]
749
+ };
750
+ }
751
+ return {
752
+ content: [{ type: "text", text: "All findings and captured data have been cleared. Start making requests to capture fresh data." }]
753
+ };
754
+ }
755
+ };
756
+ }
757
+ });
758
+
759
+ // src/mcp/tools/index.ts
760
+ function getToolDefinitions() {
761
+ return [...TOOL_MAP.values()].map((t) => ({
762
+ name: t.name,
763
+ description: t.description,
764
+ inputSchema: t.inputSchema
765
+ }));
766
+ }
767
+ function handleToolCall(client, name, args) {
768
+ const tool = TOOL_MAP.get(name);
769
+ if (!tool) {
770
+ return Promise.resolve({
771
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
772
+ isError: true
773
+ });
774
+ }
775
+ return tool.handler(client, args);
776
+ }
777
+ var TOOL_MAP;
778
+ var init_tools = __esm({
779
+ "src/mcp/tools/index.ts"() {
780
+ "use strict";
781
+ init_get_findings();
782
+ init_get_endpoints();
783
+ init_get_request_detail();
784
+ init_verify_fix();
785
+ init_get_report();
786
+ init_clear_findings();
787
+ TOOL_MAP = new Map(
788
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
789
+ );
790
+ }
791
+ });
792
+
793
+ // src/mcp/prompts.ts
794
+ var PROMPTS, PROMPT_MESSAGES;
795
+ var init_prompts = __esm({
796
+ "src/mcp/prompts.ts"() {
797
+ "use strict";
798
+ PROMPTS = [
799
+ {
800
+ name: "check-app",
801
+ description: "Check your running app for security vulnerabilities and performance issues"
802
+ },
803
+ {
804
+ name: "fix-findings",
805
+ description: "Find all open brakit findings and fix them one by one"
806
+ }
807
+ ];
808
+ PROMPT_MESSAGES = {
809
+ "check-app": [
810
+ "Check my running app for security and performance issues using brakit.",
811
+ "First get all findings, then get the endpoint summary.",
812
+ "For any critical or warning findings, get the request detail to understand the root cause.",
813
+ "Give me a clear report of what's wrong and offer to fix each issue."
814
+ ].join(" "),
815
+ "fix-findings": [
816
+ "Get all open brakit findings.",
817
+ "For each finding, get the request detail to understand the exact issue.",
818
+ "Then find the source code responsible and fix it.",
819
+ "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
820
+ ].join(" ")
821
+ };
822
+ }
823
+ });
824
+
825
+ // src/mcp/server.ts
826
+ var server_exports = {};
827
+ __export(server_exports, {
828
+ startMcpServer: () => startMcpServer
829
+ });
830
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
831
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
832
+ import {
833
+ ListToolsRequestSchema,
834
+ CallToolRequestSchema,
835
+ ListPromptsRequestSchema,
836
+ GetPromptRequestSchema
837
+ } from "@modelcontextprotocol/sdk/types.js";
838
+ async function startMcpServer() {
839
+ let discovery;
840
+ try {
841
+ discovery = await waitForBrakit(void 0, INITIAL_DISCOVERY_TIMEOUT_MS);
842
+ } catch {
843
+ discovery = null;
844
+ }
845
+ let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
846
+ const server = new Server(
847
+ { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
848
+ { capabilities: { tools: {}, prompts: {} } }
849
+ );
850
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
851
+ prompts: [...PROMPTS]
852
+ }));
853
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => ({
854
+ description: PROMPTS.find((p) => p.name === request.params.name)?.description,
855
+ messages: [{
856
+ role: "user",
857
+ content: {
858
+ type: "text",
859
+ text: PROMPT_MESSAGES[request.params.name] ?? "Check my app for issues."
860
+ }
861
+ }]
862
+ }));
863
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
864
+ tools: getToolDefinitions()
865
+ }));
866
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
867
+ const { name, arguments: args } = request.params;
868
+ let activeClient = cachedClient;
869
+ if (!activeClient) {
870
+ try {
871
+ const disc = await waitForBrakit(void 0, LAZY_DISCOVERY_TIMEOUT_MS);
872
+ activeClient = new BrakitClient(disc.baseUrl);
873
+ cachedClient = activeClient;
874
+ } catch {
875
+ return {
876
+ content: [{
877
+ type: "text",
878
+ text: "Brakit is not running. Start your app with brakit enabled first."
879
+ }],
880
+ isError: true
881
+ };
882
+ }
883
+ }
884
+ const alive = await activeClient.isAlive();
885
+ if (!alive) {
886
+ return {
887
+ content: [{
888
+ type: "text",
889
+ text: "Brakit appears to be down. Is your app still running?"
890
+ }],
891
+ isError: true
892
+ };
893
+ }
894
+ try {
895
+ return await handleToolCall(activeClient, name, args ?? {});
896
+ } catch (err) {
897
+ const message = err instanceof Error ? err.message : String(err);
898
+ return {
899
+ content: [{
900
+ type: "text",
901
+ text: `Error calling ${name}: ${message}`
902
+ }],
903
+ isError: true
904
+ };
905
+ }
906
+ });
907
+ const transport = new StdioServerTransport();
908
+ await server.connect(transport);
909
+ }
910
+ var init_server = __esm({
911
+ "src/mcp/server.ts"() {
912
+ "use strict";
913
+ init_client();
914
+ init_discovery();
915
+ init_tools();
916
+ init_mcp();
917
+ init_prompts();
918
+ }
919
+ });
920
+
2
921
  // bin/brakit.ts
3
922
  import { runMain } from "citty";
4
923
 
5
924
  // src/cli/commands/install.ts
6
925
  import { defineCommand } from "citty";
7
- import { resolve as resolve3, join as join2 } from "path";
8
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
926
+ import { resolve as resolve4, join as join2 } from "path";
927
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
9
928
  import { execSync } from "child_process";
10
929
  import pc from "picocolors";
11
930
 
12
- // src/detect/project.ts
13
- import { readFile as readFile2 } from "fs/promises";
14
- import { join } from "path";
931
+ // src/store/finding-store.ts
932
+ init_constants();
933
+ import {
934
+ readFileSync as readFileSync2,
935
+ writeFileSync as writeFileSync2,
936
+ existsSync as existsSync2,
937
+ mkdirSync as mkdirSync2,
938
+ renameSync
939
+ } from "fs";
940
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
941
+ import { resolve as resolve2 } from "path";
15
942
 
16
943
  // src/utils/fs.ts
17
944
  import { access } from "fs/promises";
@@ -26,7 +953,12 @@ async function fileExists(path) {
26
953
  }
27
954
  }
28
955
 
956
+ // src/store/finding-store.ts
957
+ init_finding_id();
958
+
29
959
  // src/detect/project.ts
960
+ import { readFile as readFile2 } from "fs/promises";
961
+ import { join } from "path";
30
962
  var FRAMEWORKS = [
31
963
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
32
964
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -386,6 +1318,31 @@ var corsCredentialsRule = {
386
1318
  }
387
1319
  };
388
1320
 
1321
+ // src/utils/response.ts
1322
+ init_thresholds();
1323
+ function unwrapResponse(parsed) {
1324
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1325
+ const obj = parsed;
1326
+ const keys = Object.keys(obj);
1327
+ if (keys.length > 3) return parsed;
1328
+ let best = null;
1329
+ let bestSize = 0;
1330
+ for (const key of keys) {
1331
+ const val = obj[key];
1332
+ if (Array.isArray(val) && val.length > bestSize) {
1333
+ best = val;
1334
+ bestSize = val.length;
1335
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
1336
+ const size = Object.keys(val).length;
1337
+ if (size > bestSize) {
1338
+ best = val;
1339
+ bestSize = size;
1340
+ }
1341
+ }
1342
+ }
1343
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1344
+ }
1345
+
389
1346
  // src/analysis/rules/response-pii-leak.ts
390
1347
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
391
1348
  var FULL_RECORD_MIN_FIELDS = 5;
@@ -430,28 +1387,6 @@ function hasInternalIds(obj) {
430
1387
  }
431
1388
  return false;
432
1389
  }
433
- function unwrapResponse(parsed) {
434
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
435
- const obj = parsed;
436
- const keys = Object.keys(obj);
437
- if (keys.length > 3) return parsed;
438
- let best = null;
439
- let bestSize = 0;
440
- for (const key of keys) {
441
- const val = obj[key];
442
- if (Array.isArray(val) && val.length > bestSize) {
443
- best = val;
444
- bestSize = val.length;
445
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
446
- const size = Object.keys(val).length;
447
- if (size > bestSize) {
448
- best = val;
449
- bestSize = size;
450
- }
451
- }
452
- }
453
- return best && bestSize >= 3 ? best : parsed;
454
- }
455
1390
  function detectPII(method, reqBody, resBody) {
456
1391
  const target = unwrapResponse(resBody);
457
1392
  if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
@@ -537,9 +1472,8 @@ var responsePiiLeakRule = {
537
1472
  }
538
1473
  };
539
1474
 
540
- // src/constants/limits.ts
541
- var MAX_REQUEST_ENTRIES = 1e3;
542
- var MAX_TELEMETRY_ENTRIES = 1e3;
1475
+ // src/store/request-store.ts
1476
+ init_constants();
543
1477
 
544
1478
  // src/utils/static-patterns.ts
545
1479
  var STATIC_PATTERNS = [
@@ -624,6 +1558,7 @@ var RequestStore = class {
624
1558
  var defaultStore = new RequestStore();
625
1559
 
626
1560
  // src/store/telemetry-store.ts
1561
+ init_constants();
627
1562
  import { randomUUID } from "crypto";
628
1563
  var TelemetryStore = class {
629
1564
  constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
@@ -677,25 +1612,79 @@ var QueryStore = class extends TelemetryStore {
677
1612
  var defaultQueryStore = new QueryStore();
678
1613
 
679
1614
  // src/store/metrics/metrics-store.ts
1615
+ init_constants();
680
1616
  import { randomUUID as randomUUID2 } from "crypto";
1617
+ init_endpoint();
681
1618
 
682
1619
  // src/store/metrics/persistence.ts
1620
+ init_constants();
683
1621
  import {
684
- readFileSync as readFileSync2,
685
- writeFileSync as writeFileSync2,
686
- mkdirSync as mkdirSync2,
687
- existsSync as existsSync2,
1622
+ readFileSync as readFileSync3,
1623
+ writeFileSync as writeFileSync3,
1624
+ mkdirSync as mkdirSync3,
1625
+ existsSync as existsSync3,
688
1626
  unlinkSync,
689
- renameSync
1627
+ renameSync as renameSync2
690
1628
  } from "fs";
691
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
692
- import { resolve as resolve2 } from "path";
1629
+ import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
1630
+ import { resolve as resolve3 } from "path";
693
1631
 
694
1632
  // src/analysis/group.ts
1633
+ init_constants();
695
1634
  import { randomUUID as randomUUID3 } from "crypto";
696
1635
 
1636
+ // src/analysis/label.ts
1637
+ init_constants();
1638
+
1639
+ // src/analysis/transforms.ts
1640
+ init_constants();
1641
+
1642
+ // src/analysis/insights/prepare.ts
1643
+ init_endpoint();
1644
+ init_constants();
1645
+
1646
+ // src/analysis/insights/rules/n1.ts
1647
+ init_endpoint();
1648
+ init_thresholds();
1649
+
1650
+ // src/analysis/insights/rules/cross-endpoint.ts
1651
+ init_endpoint();
1652
+ init_thresholds();
1653
+
1654
+ // src/analysis/insights/rules/redundant-query.ts
1655
+ init_endpoint();
1656
+ init_thresholds();
1657
+
1658
+ // src/analysis/insights/rules/error-hotspot.ts
1659
+ init_thresholds();
1660
+
1661
+ // src/analysis/insights/rules/duplicate.ts
1662
+ init_thresholds();
1663
+
1664
+ // src/analysis/insights/rules/slow.ts
1665
+ init_thresholds();
1666
+
1667
+ // src/analysis/insights/rules/query-heavy.ts
1668
+ init_thresholds();
1669
+
1670
+ // src/analysis/insights/rules/select-star.ts
1671
+ init_thresholds();
1672
+
1673
+ // src/analysis/insights/rules/high-rows.ts
1674
+ init_thresholds();
1675
+
1676
+ // src/analysis/insights/rules/response-overfetch.ts
1677
+ init_endpoint();
1678
+ init_thresholds();
1679
+
1680
+ // src/analysis/insights/rules/large-response.ts
1681
+ init_thresholds();
1682
+
1683
+ // src/analysis/insights/rules/regression.ts
1684
+ init_thresholds();
1685
+
697
1686
  // src/index.ts
698
- var VERSION = "0.7.5";
1687
+ var VERSION = "0.8.0";
699
1688
 
700
1689
  // src/cli/commands/install.ts
701
1690
  var IMPORT_LINE = `import "brakit";`;
@@ -715,7 +1704,7 @@ var install_default = defineCommand({
715
1704
  }
716
1705
  },
717
1706
  async run({ args }) {
718
- const rootDir = resolve3(args.dir);
1707
+ const rootDir = resolve4(args.dir);
719
1708
  const pkgPath = join2(rootDir, "package.json");
720
1709
  if (!await fileExists(pkgPath)) {
721
1710
  console.error(pc.red(" No package.json found. Run this from your project root."));
@@ -753,6 +1742,12 @@ var install_default = defineCommand({
753
1742
  } else {
754
1743
  printManualInstructions(project.framework);
755
1744
  }
1745
+ const mcpResult = await setupMcp(rootDir);
1746
+ if (mcpResult === "created" || mcpResult === "updated") {
1747
+ console.log(pc.green(" \u2713 Configured MCP for Claude Code / Cursor"));
1748
+ } else if (mcpResult === "exists") {
1749
+ console.log(pc.dim(" \u2713 MCP already configured"));
1750
+ }
756
1751
  console.log();
757
1752
  console.log(pc.dim(" Start your app and visit:"));
758
1753
  console.log(pc.bold(" http://localhost:<port>/__brakit"));
@@ -809,7 +1804,7 @@ async function setupNextjs(rootDir) {
809
1804
  `}`,
810
1805
  ``
811
1806
  ].join("\n");
812
- await writeFile3(absPath, content);
1807
+ await writeFile4(absPath, content);
813
1808
  return { action: "created", file: relPath, content };
814
1809
  }
815
1810
  async function setupNuxt(rootDir) {
@@ -825,9 +1820,9 @@ async function setupNuxt(rootDir) {
825
1820
  const content = `${IMPORT_LINE}
826
1821
  `;
827
1822
  const dir = join2(rootDir, "server/plugins");
828
- const { mkdirSync: mkdirSync3 } = await import("fs");
829
- mkdirSync3(dir, { recursive: true });
830
- await writeFile3(absPath, content);
1823
+ const { mkdirSync: mkdirSync4 } = await import("fs");
1824
+ mkdirSync4(dir, { recursive: true });
1825
+ await writeFile4(absPath, content);
831
1826
  return { action: "created", file: relPath, content };
832
1827
  }
833
1828
  async function setupPrepend(rootDir, ...candidates) {
@@ -838,7 +1833,7 @@ async function setupPrepend(rootDir, ...candidates) {
838
1833
  if (content.includes(IMPORT_MARKER)) {
839
1834
  return { action: "exists", file: relPath };
840
1835
  }
841
- await writeFile3(absPath, `${IMPORT_LINE}
1836
+ await writeFile4(absPath, `${IMPORT_LINE}
842
1837
  ${content}`);
843
1838
  return { action: "prepended", file: relPath };
844
1839
  }
@@ -872,6 +1867,45 @@ async function setupGeneric(rootDir) {
872
1867
  if (result.action !== "manual") return result;
873
1868
  return { action: "manual", file: null };
874
1869
  }
1870
+ var MCP_CONFIG = {
1871
+ mcpServers: {
1872
+ brakit: {
1873
+ command: "npx",
1874
+ args: ["brakit", "mcp"]
1875
+ }
1876
+ }
1877
+ };
1878
+ async function setupMcp(rootDir) {
1879
+ const mcpPath = join2(rootDir, ".mcp.json");
1880
+ if (await fileExists(mcpPath)) {
1881
+ const raw = await readFile3(mcpPath, "utf-8");
1882
+ try {
1883
+ const config = JSON.parse(raw);
1884
+ if (config?.mcpServers?.brakit) return "exists";
1885
+ config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
1886
+ await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
1887
+ await ensureGitignoreMcp(rootDir);
1888
+ return "updated";
1889
+ } catch {
1890
+ }
1891
+ }
1892
+ await writeFile4(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
1893
+ await ensureGitignoreMcp(rootDir);
1894
+ return "created";
1895
+ }
1896
+ async function ensureGitignoreMcp(rootDir) {
1897
+ const gitignorePath = join2(rootDir, ".gitignore");
1898
+ try {
1899
+ if (await fileExists(gitignorePath)) {
1900
+ const content = await readFile3(gitignorePath, "utf-8");
1901
+ if (content.split("\n").some((l) => l.trim() === ".mcp.json")) return;
1902
+ await writeFile4(gitignorePath, content.trimEnd() + "\n.mcp.json\n");
1903
+ } else {
1904
+ await writeFile4(gitignorePath, ".mcp.json\n");
1905
+ }
1906
+ } catch {
1907
+ }
1908
+ }
875
1909
  function printManualInstructions(framework) {
876
1910
  console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
877
1911
  console.log();
@@ -891,10 +1925,11 @@ function printManualInstructions(framework) {
891
1925
 
892
1926
  // src/cli/commands/uninstall.ts
893
1927
  import { defineCommand as defineCommand2 } from "citty";
894
- import { resolve as resolve4, join as join3 } from "path";
895
- import { readFile as readFile4, writeFile as writeFile4, unlink } from "fs/promises";
1928
+ import { resolve as resolve5, join as join3 } from "path";
1929
+ import { readFile as readFile4, writeFile as writeFile5, unlink, rm } from "fs/promises";
896
1930
  import { execSync as execSync2 } from "child_process";
897
1931
  import pc2 from "picocolors";
1932
+ init_constants();
898
1933
  var IMPORT_LINE2 = `import "brakit";`;
899
1934
  var CREATED_FILES = [
900
1935
  "src/instrumentation.ts",
@@ -932,7 +1967,7 @@ var uninstall_default = defineCommand2({
932
1967
  }
933
1968
  },
934
1969
  async run({ args }) {
935
- const rootDir = resolve4(args.dir);
1970
+ const rootDir = resolve5(args.dir);
936
1971
  let project = null;
937
1972
  try {
938
1973
  project = await detectProject(rootDir);
@@ -970,7 +2005,7 @@ var uninstall_default = defineCommand2({
970
2005
  const content = await readFile4(absPath, "utf-8");
971
2006
  if (!content.includes(IMPORT_LINE2)) continue;
972
2007
  const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE2.trim()).join("\n");
973
- await writeFile4(absPath, updated);
2008
+ await writeFile5(absPath, updated);
974
2009
  console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
975
2010
  removed = true;
976
2011
  break;
@@ -979,6 +2014,18 @@ var uninstall_default = defineCommand2({
979
2014
  if (!removed) {
980
2015
  console.log(pc2.dim(" No brakit instrumentation files found."));
981
2016
  }
2017
+ const mcpRemoved = await removeMcpConfig(rootDir);
2018
+ if (mcpRemoved) {
2019
+ console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
2020
+ }
2021
+ const dataRemoved = await removeBrakitData(rootDir);
2022
+ if (dataRemoved) {
2023
+ console.log(pc2.green(" \u2713 Removed .brakit directory"));
2024
+ }
2025
+ const gitignoreCleaned = await cleanGitignore(rootDir);
2026
+ if (gitignoreCleaned) {
2027
+ console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
2028
+ }
982
2029
  const pm = project?.packageManager ?? "npm";
983
2030
  const uninstalled = await uninstallPackage(rootDir, pm);
984
2031
  if (uninstalled) {
@@ -987,6 +2034,24 @@ var uninstall_default = defineCommand2({
987
2034
  console.log();
988
2035
  }
989
2036
  });
2037
+ async function removeMcpConfig(rootDir) {
2038
+ const mcpPath = join3(rootDir, ".mcp.json");
2039
+ if (!await fileExists(mcpPath)) return false;
2040
+ try {
2041
+ const raw = await readFile4(mcpPath, "utf-8");
2042
+ const config = JSON.parse(raw);
2043
+ if (!config?.mcpServers?.brakit) return false;
2044
+ delete config.mcpServers.brakit;
2045
+ if (Object.keys(config.mcpServers).length === 0) {
2046
+ await unlink(mcpPath);
2047
+ } else {
2048
+ await writeFile5(mcpPath, JSON.stringify(config, null, 2) + "\n");
2049
+ }
2050
+ return true;
2051
+ } catch {
2052
+ return false;
2053
+ }
2054
+ }
990
2055
  async function uninstallPackage(rootDir, pm) {
991
2056
  try {
992
2057
  const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
@@ -1009,12 +2074,42 @@ async function uninstallPackage(rootDir, pm) {
1009
2074
  }
1010
2075
  return true;
1011
2076
  }
2077
+ async function removeBrakitData(rootDir) {
2078
+ const dataDir = join3(rootDir, METRICS_DIR);
2079
+ if (!await fileExists(dataDir)) return false;
2080
+ try {
2081
+ await rm(dataDir, { recursive: true, force: true });
2082
+ return true;
2083
+ } catch {
2084
+ return false;
2085
+ }
2086
+ }
2087
+ async function cleanGitignore(rootDir) {
2088
+ const gitignorePath = join3(rootDir, ".gitignore");
2089
+ if (!await fileExists(gitignorePath)) return false;
2090
+ try {
2091
+ const content = await readFile4(gitignorePath, "utf-8");
2092
+ const lines = content.split("\n");
2093
+ const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
2094
+ if (filtered.length === lines.length) return false;
2095
+ await writeFile5(gitignorePath, filtered.join("\n"));
2096
+ return true;
2097
+ } catch {
2098
+ return false;
2099
+ }
2100
+ }
1012
2101
 
1013
2102
  // bin/brakit.ts
1014
2103
  var sub = process.argv[2];
1015
2104
  if (sub === "uninstall") {
1016
2105
  process.argv.splice(2, 1);
1017
2106
  runMain(uninstall_default);
2107
+ } else if (sub === "mcp") {
2108
+ Promise.resolve().then(() => (init_server(), server_exports)).then(({ startMcpServer: startMcpServer2 }) => startMcpServer2()).catch((err) => {
2109
+ process.stderr.write(`[brakit] MCP server failed: ${err.message}
2110
+ `);
2111
+ process.exitCode = 1;
2112
+ });
1018
2113
  } else {
1019
2114
  if (sub === "install") process.argv.splice(2, 1);
1020
2115
  runMain(install_default);