@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.12 → 1.0.0-dev.14

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.
@@ -206,25 +206,34 @@ function buildOptions(opts) {
206
206
  });
207
207
  pendingBatch = null;
208
208
  }
209
- return { notifier(info) {
210
- const report = {
211
- displayName: info.displayName,
212
- reason: sanitizeReason(info.reason),
213
- hookName: info.hookName
214
- };
215
- if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
216
- else {
217
- if (pendingBatch) flushBatch();
218
- pendingBatch = {
219
- commitId,
220
- reports: [report]
209
+ return {
210
+ registerComponents(components) {
211
+ send({
212
+ type: "register",
213
+ projectId,
214
+ components
215
+ });
216
+ },
217
+ notifier(info) {
218
+ const report = {
219
+ displayName: info.displayName,
220
+ reason: sanitizeReason(info.reason),
221
+ hookName: info.hookName
221
222
  };
223
+ if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
224
+ else {
225
+ if (pendingBatch) flushBatch();
226
+ pendingBatch = {
227
+ commitId,
228
+ reports: [report]
229
+ };
230
+ }
231
+ if (!flushScheduled) {
232
+ flushScheduled = true;
233
+ queueMicrotask(flushBatch);
234
+ }
222
235
  }
223
- if (!flushScheduled) {
224
- flushScheduled = true;
225
- queueMicrotask(flushBatch);
226
- }
227
- } };
236
+ };
228
237
  }
229
238
  //#endregion
230
239
  exports.buildOptions = buildOptions;
@@ -7,6 +7,7 @@ interface ClientOptions {
7
7
  projectId?: string;
8
8
  }
9
9
  declare function buildOptions(opts?: ClientOptions): {
10
+ registerComponents(components: string[]): void;
10
11
  notifier(info: UpdateInfo): void;
11
12
  };
12
13
  //#endregion
@@ -7,6 +7,7 @@ interface ClientOptions {
7
7
  projectId?: string;
8
8
  }
9
9
  declare function buildOptions(opts?: ClientOptions): {
10
+ registerComponents(components: string[]): void;
10
11
  notifier(info: UpdateInfo): void;
11
12
  };
12
13
  //#endregion
@@ -205,25 +205,34 @@ function buildOptions(opts) {
205
205
  });
206
206
  pendingBatch = null;
207
207
  }
208
- return { notifier(info) {
209
- const report = {
210
- displayName: info.displayName,
211
- reason: sanitizeReason(info.reason),
212
- hookName: info.hookName
213
- };
214
- if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
215
- else {
216
- if (pendingBatch) flushBatch();
217
- pendingBatch = {
218
- commitId,
219
- reports: [report]
208
+ return {
209
+ registerComponents(components) {
210
+ send({
211
+ type: "register",
212
+ projectId,
213
+ components
214
+ });
215
+ },
216
+ notifier(info) {
217
+ const report = {
218
+ displayName: info.displayName,
219
+ reason: sanitizeReason(info.reason),
220
+ hookName: info.hookName
220
221
  };
222
+ if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
223
+ else {
224
+ if (pendingBatch) flushBatch();
225
+ pendingBatch = {
226
+ commitId,
227
+ reports: [report]
228
+ };
229
+ }
230
+ if (!flushScheduled) {
231
+ flushScheduled = true;
232
+ queueMicrotask(flushBatch);
233
+ }
221
234
  }
222
- if (!flushScheduled) {
223
- flushScheduled = true;
224
- queueMicrotask(flushBatch);
225
- }
226
- } };
235
+ };
227
236
  }
228
237
  //#endregion
229
238
  export { buildOptions };
@@ -130,6 +130,7 @@ var RenderStore = class {
130
130
  timers = /* @__PURE__ */ new Map();
131
131
  dicts = /* @__PURE__ */ new Map();
132
132
  bufferMeta = /* @__PURE__ */ new Map();
133
+ trackedComponents = /* @__PURE__ */ new Map();
133
134
  constructor(dir) {
134
135
  this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
135
136
  mkdirSync(this.dir, { recursive: true });
@@ -276,6 +277,33 @@ var RenderStore = class {
276
277
  }
277
278
  return summary;
278
279
  }
280
+ getSummaryByCommit(projectId) {
281
+ const renders = this.getAllRenders(projectId);
282
+ const summary = {};
283
+ for (const r of renders) {
284
+ if (r.commitId == null) continue;
285
+ summary[r.project] ??= {};
286
+ summary[r.project][r.commitId] ??= {};
287
+ const commit = summary[r.project][r.commitId];
288
+ commit[r.displayName] = (commit[r.displayName] ?? 0) + 1;
289
+ }
290
+ return summary;
291
+ }
292
+ setTrackedComponents(components, projectId) {
293
+ this.trackedComponents.set(projectId, components);
294
+ }
295
+ getTrackedComponents(projectId) {
296
+ const result = {};
297
+ const projects = projectId ? [projectId] : this.getProjects();
298
+ for (const proj of projects) {
299
+ const observed = [...new Set(this.getAllRenders(proj).map((r) => r.displayName))];
300
+ result[proj] = {
301
+ registered: this.trackedComponents.get(proj) ?? [],
302
+ observed
303
+ };
304
+ }
305
+ return result;
306
+ }
279
307
  bufferKey(projectId, commitId) {
280
308
  return `${projectId}\0${commitId ?? NOCOMMIT}`;
281
309
  }
@@ -343,7 +371,7 @@ function textResult(text) {
343
371
  }
344
372
  //#endregion
345
373
  //#region src/server/tools/clear-renders.ts
346
- function register$5(server) {
374
+ function register$6(server) {
347
375
  server.registerTool("clear_renders", {
348
376
  title: "Clear Renders",
349
377
  description: "Clears collected render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
@@ -357,7 +385,7 @@ function register$5(server) {
357
385
  }
358
386
  //#endregion
359
387
  //#region src/server/tools/get-commits.ts
360
- function register$4(server) {
388
+ function register$5(server) {
361
389
  server.registerTool("get_commits", {
362
390
  title: "Get Commits",
363
391
  description: "Returns a list of React commit IDs that have recorded render data for a project. Use these IDs with get_renders_by_commit to inspect individual commits.",
@@ -372,7 +400,7 @@ function register$4(server) {
372
400
  }
373
401
  //#endregion
374
402
  //#region src/server/tools/get-projects.ts
375
- function register$3(server) {
403
+ function register$4(server) {
376
404
  server.registerTool("get_projects", {
377
405
  title: "Get Projects",
378
406
  description: "Returns a list of project identifiers (browser origin URLs) that have recorded render data.",
@@ -385,27 +413,50 @@ function register$3(server) {
385
413
  }
386
414
  //#endregion
387
415
  //#region src/server/tools/get-render-summary.ts
388
- function register$2(server) {
416
+ function register$3(server) {
389
417
  server.registerTool("get_render_summary", {
390
418
  title: "Get Render Summary",
391
- description: "Returns a summary of re-renders grouped by component name with counts. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
392
- inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
393
- }, async ({ project }) => {
419
+ description: "Returns a summary of re-renders grouped by component name with counts. Use groupBy: 'commit' to get per-commit breakdowns instead of a single aggregate. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
420
+ inputSchema: {
421
+ project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect."),
422
+ groupBy: z.enum(["commit"]).optional().describe("Group results by commit. When set to 'commit', returns per-commit render summaries instead of a single aggregate.")
423
+ }
424
+ }, async ({ project, groupBy }) => {
394
425
  const resolved = resolveProject(project);
395
426
  if (resolved.error) return textResult(resolved.error);
396
- const summary = store.getSummary(resolved.projectId);
397
- if (Object.keys(summary).length === 0) return textResult("No renders recorded yet.");
398
- const lines = [];
399
- for (const [projectId, components] of Object.entries(summary)) {
400
- lines.push(`[${projectId}]`);
401
- for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count} re-render(s)`);
402
- }
403
- return textResult(`Re-render summary:\n\n${lines.join("\n")}`);
427
+ if (groupBy === "commit") return commitSummary(resolved.projectId);
428
+ return aggregateSummary(resolved.projectId);
404
429
  });
405
430
  }
431
+ function aggregateSummary(projectId) {
432
+ const summary = store.getSummary(projectId);
433
+ if (Object.keys(summary).length === 0) return textResult("No renders recorded yet.");
434
+ const lines = [];
435
+ for (const [proj, components] of Object.entries(summary)) {
436
+ lines.push(`[${proj}]`);
437
+ for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count} re-render(s)`);
438
+ }
439
+ return textResult(`Re-render summary:\n\n${lines.join("\n")}`);
440
+ }
441
+ function commitSummary(projectId) {
442
+ const summary = store.getSummaryByCommit(projectId);
443
+ if (Object.keys(summary).length === 0) return textResult("No renders with commit IDs recorded yet.");
444
+ const lines = [];
445
+ for (const [proj, commits] of Object.entries(summary)) {
446
+ lines.push(`[${proj}]`);
447
+ const sortedCommitIds = Object.keys(commits).map(Number).sort((a, b) => a - b);
448
+ for (const commitId of sortedCommitIds) {
449
+ const components = commits[commitId];
450
+ const total = Object.values(components).reduce((s, c) => s + c, 0);
451
+ lines.push(` Commit #${commitId} (${total} re-render(s)):`);
452
+ for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count}`);
453
+ }
454
+ }
455
+ return textResult(`Re-render summary (by commit):\n\n${lines.join("\n")}`);
456
+ }
406
457
  //#endregion
407
458
  //#region src/server/tools/get-renders-by-commit.ts
408
- function register$1(server) {
459
+ function register$2(server) {
409
460
  server.registerTool("get_renders_by_commit", {
410
461
  title: "Get Renders by Commit",
411
462
  description: "Returns all re-renders for a specific React commit ID. Use get_commits first to discover available commit IDs.",
@@ -425,7 +476,7 @@ function register$1(server) {
425
476
  }
426
477
  //#endregion
427
478
  //#region src/server/tools/get-renders.ts
428
- function register(server) {
479
+ function register$1(server) {
429
480
  server.registerTool("get_renders", {
430
481
  title: "Get Renders",
431
482
  description: "Returns all re-renders collected from the browser. If multiple projects are active and no project is specified, the tool will ask you to disambiguate by asking the user for their dev server URL.",
@@ -442,14 +493,43 @@ function register(server) {
442
493
  });
443
494
  }
444
495
  //#endregion
496
+ //#region src/server/tools/get-tracked-components.ts
497
+ function register(server) {
498
+ server.registerTool("get_tracked_components", {
499
+ title: "Get Tracked Components",
500
+ description: "Returns the list of components being tracked by why-did-you-render. Shows both explicitly registered components (sent by the client) and components observed in render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
501
+ inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
502
+ }, async ({ project }) => {
503
+ const resolved = resolveProject(project);
504
+ if (resolved.error) return textResult(resolved.error);
505
+ const tracked = store.getTrackedComponents(resolved.projectId);
506
+ if (Object.keys(tracked).length === 0) return textResult("No tracked components found. Make sure the browser is connected and triggering re-renders.");
507
+ const lines = [];
508
+ for (const [proj, { registered, observed }] of Object.entries(tracked)) {
509
+ lines.push(`[${proj}]`);
510
+ if (registered.length > 0) {
511
+ lines.push(" Registered:");
512
+ for (const name of registered) lines.push(` - ${name}`);
513
+ }
514
+ if (observed.length > 0) {
515
+ lines.push(" Observed in renders:");
516
+ for (const name of observed) lines.push(` - ${name}`);
517
+ }
518
+ if (registered.length === 0 && observed.length === 0) lines.push(" No components tracked yet.");
519
+ }
520
+ return textResult(lines.join("\n"));
521
+ });
522
+ }
523
+ //#endregion
445
524
  //#region src/server/tools/index.ts
446
525
  function registerTools(server) {
447
- register(server);
448
- register$2(server);
449
- register$4(server);
450
526
  register$1(server);
451
527
  register$3(server);
452
528
  register$5(server);
529
+ register$2(server);
530
+ register$4(server);
531
+ register(server);
532
+ register$6(server);
453
533
  }
454
534
  //#endregion
455
535
  //#region src/server/liveness.ts
@@ -528,6 +608,7 @@ function createWsServer(port) {
528
608
  heartbeat.setProjectId(ws, projectId);
529
609
  if (msg.type === "render") store.addRender(msg.payload, projectId, msg.commitId);
530
610
  else if (msg.type === "render-batch") for (const report of msg.payload) store.addRender(report, projectId, msg.commitId);
611
+ else if (msg.type === "register") store.setTrackedComponents(msg.components, projectId);
531
612
  } catch {
532
613
  console.error("[wdyr-mcp] invalid message received");
533
614
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0x1f320.sh/why-did-you-render-mcp",
3
- "version": "1.0.0-dev.12",
3
+ "version": "1.0.0-dev.14",
4
4
  "type": "module",
5
5
  "description": "MCP server that collects why-did-you-render data from browser and exposes it to coding agents",
6
6
  "license": "MIT",