brakit 0.7.6 → 0.8.1

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