brakit 0.7.6 → 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,10 +1318,8 @@ var corsCredentialsRule = {
386
1318
  }
387
1319
  };
388
1320
 
389
- // src/constants/thresholds.ts
390
- var OVERFETCH_UNWRAP_MIN_SIZE = 3;
391
-
392
1321
  // src/utils/response.ts
1322
+ init_thresholds();
393
1323
  function unwrapResponse(parsed) {
394
1324
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
395
1325
  const obj = parsed;
@@ -542,9 +1472,8 @@ var responsePiiLeakRule = {
542
1472
  }
543
1473
  };
544
1474
 
545
- // src/constants/limits.ts
546
- var MAX_REQUEST_ENTRIES = 1e3;
547
- var MAX_TELEMETRY_ENTRIES = 1e3;
1475
+ // src/store/request-store.ts
1476
+ init_constants();
548
1477
 
549
1478
  // src/utils/static-patterns.ts
550
1479
  var STATIC_PATTERNS = [
@@ -629,6 +1558,7 @@ var RequestStore = class {
629
1558
  var defaultStore = new RequestStore();
630
1559
 
631
1560
  // src/store/telemetry-store.ts
1561
+ init_constants();
632
1562
  import { randomUUID } from "crypto";
633
1563
  var TelemetryStore = class {
634
1564
  constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
@@ -682,25 +1612,79 @@ var QueryStore = class extends TelemetryStore {
682
1612
  var defaultQueryStore = new QueryStore();
683
1613
 
684
1614
  // src/store/metrics/metrics-store.ts
1615
+ init_constants();
685
1616
  import { randomUUID as randomUUID2 } from "crypto";
1617
+ init_endpoint();
686
1618
 
687
1619
  // src/store/metrics/persistence.ts
1620
+ init_constants();
688
1621
  import {
689
- readFileSync as readFileSync2,
690
- writeFileSync as writeFileSync2,
691
- mkdirSync as mkdirSync2,
692
- existsSync as existsSync2,
1622
+ readFileSync as readFileSync3,
1623
+ writeFileSync as writeFileSync3,
1624
+ mkdirSync as mkdirSync3,
1625
+ existsSync as existsSync3,
693
1626
  unlinkSync,
694
- renameSync
1627
+ renameSync as renameSync2
695
1628
  } from "fs";
696
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
697
- 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";
698
1631
 
699
1632
  // src/analysis/group.ts
1633
+ init_constants();
700
1634
  import { randomUUID as randomUUID3 } from "crypto";
701
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
+
702
1686
  // src/index.ts
703
- var VERSION = "0.7.6";
1687
+ var VERSION = "0.8.0";
704
1688
 
705
1689
  // src/cli/commands/install.ts
706
1690
  var IMPORT_LINE = `import "brakit";`;
@@ -720,7 +1704,7 @@ var install_default = defineCommand({
720
1704
  }
721
1705
  },
722
1706
  async run({ args }) {
723
- const rootDir = resolve3(args.dir);
1707
+ const rootDir = resolve4(args.dir);
724
1708
  const pkgPath = join2(rootDir, "package.json");
725
1709
  if (!await fileExists(pkgPath)) {
726
1710
  console.error(pc.red(" No package.json found. Run this from your project root."));
@@ -758,6 +1742,12 @@ var install_default = defineCommand({
758
1742
  } else {
759
1743
  printManualInstructions(project.framework);
760
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
+ }
761
1751
  console.log();
762
1752
  console.log(pc.dim(" Start your app and visit:"));
763
1753
  console.log(pc.bold(" http://localhost:<port>/__brakit"));
@@ -814,7 +1804,7 @@ async function setupNextjs(rootDir) {
814
1804
  `}`,
815
1805
  ``
816
1806
  ].join("\n");
817
- await writeFile3(absPath, content);
1807
+ await writeFile4(absPath, content);
818
1808
  return { action: "created", file: relPath, content };
819
1809
  }
820
1810
  async function setupNuxt(rootDir) {
@@ -830,9 +1820,9 @@ async function setupNuxt(rootDir) {
830
1820
  const content = `${IMPORT_LINE}
831
1821
  `;
832
1822
  const dir = join2(rootDir, "server/plugins");
833
- const { mkdirSync: mkdirSync3 } = await import("fs");
834
- mkdirSync3(dir, { recursive: true });
835
- await writeFile3(absPath, content);
1823
+ const { mkdirSync: mkdirSync4 } = await import("fs");
1824
+ mkdirSync4(dir, { recursive: true });
1825
+ await writeFile4(absPath, content);
836
1826
  return { action: "created", file: relPath, content };
837
1827
  }
838
1828
  async function setupPrepend(rootDir, ...candidates) {
@@ -843,7 +1833,7 @@ async function setupPrepend(rootDir, ...candidates) {
843
1833
  if (content.includes(IMPORT_MARKER)) {
844
1834
  return { action: "exists", file: relPath };
845
1835
  }
846
- await writeFile3(absPath, `${IMPORT_LINE}
1836
+ await writeFile4(absPath, `${IMPORT_LINE}
847
1837
  ${content}`);
848
1838
  return { action: "prepended", file: relPath };
849
1839
  }
@@ -877,6 +1867,45 @@ async function setupGeneric(rootDir) {
877
1867
  if (result.action !== "manual") return result;
878
1868
  return { action: "manual", file: null };
879
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
+ }
880
1909
  function printManualInstructions(framework) {
881
1910
  console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
882
1911
  console.log();
@@ -896,10 +1925,11 @@ function printManualInstructions(framework) {
896
1925
 
897
1926
  // src/cli/commands/uninstall.ts
898
1927
  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";
1928
+ import { resolve as resolve5, join as join3 } from "path";
1929
+ import { readFile as readFile4, writeFile as writeFile5, unlink, rm } from "fs/promises";
901
1930
  import { execSync as execSync2 } from "child_process";
902
1931
  import pc2 from "picocolors";
1932
+ init_constants();
903
1933
  var IMPORT_LINE2 = `import "brakit";`;
904
1934
  var CREATED_FILES = [
905
1935
  "src/instrumentation.ts",
@@ -937,7 +1967,7 @@ var uninstall_default = defineCommand2({
937
1967
  }
938
1968
  },
939
1969
  async run({ args }) {
940
- const rootDir = resolve4(args.dir);
1970
+ const rootDir = resolve5(args.dir);
941
1971
  let project = null;
942
1972
  try {
943
1973
  project = await detectProject(rootDir);
@@ -975,7 +2005,7 @@ var uninstall_default = defineCommand2({
975
2005
  const content = await readFile4(absPath, "utf-8");
976
2006
  if (!content.includes(IMPORT_LINE2)) continue;
977
2007
  const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE2.trim()).join("\n");
978
- await writeFile4(absPath, updated);
2008
+ await writeFile5(absPath, updated);
979
2009
  console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
980
2010
  removed = true;
981
2011
  break;
@@ -984,6 +2014,18 @@ var uninstall_default = defineCommand2({
984
2014
  if (!removed) {
985
2015
  console.log(pc2.dim(" No brakit instrumentation files found."));
986
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
+ }
987
2029
  const pm = project?.packageManager ?? "npm";
988
2030
  const uninstalled = await uninstallPackage(rootDir, pm);
989
2031
  if (uninstalled) {
@@ -992,6 +2034,24 @@ var uninstall_default = defineCommand2({
992
2034
  console.log();
993
2035
  }
994
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
+ }
995
2055
  async function uninstallPackage(rootDir, pm) {
996
2056
  try {
997
2057
  const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
@@ -1014,12 +2074,42 @@ async function uninstallPackage(rootDir, pm) {
1014
2074
  }
1015
2075
  return true;
1016
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
+ }
1017
2101
 
1018
2102
  // bin/brakit.ts
1019
2103
  var sub = process.argv[2];
1020
2104
  if (sub === "uninstall") {
1021
2105
  process.argv.splice(2, 1);
1022
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
+ });
1023
2113
  } else {
1024
2114
  if (sub === "install") process.argv.splice(2, 1);
1025
2115
  runMain(install_default);