@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.13 → 1.0.0-dev.15

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 });
@@ -272,7 +273,19 @@ var RenderStore = class {
272
273
  for (const r of renders) {
273
274
  summary[r.project] ??= {};
274
275
  const project = summary[r.project];
275
- project[r.displayName] = (project[r.displayName] ?? 0) + 1;
276
+ project[r.displayName] ??= {
277
+ count: 0,
278
+ reasons: {
279
+ props: 0,
280
+ state: 0,
281
+ hooks: 0
282
+ }
283
+ };
284
+ const entry = project[r.displayName];
285
+ entry.count++;
286
+ if (Array.isArray(r.reason.propsDifferences)) entry.reasons.props++;
287
+ if (Array.isArray(r.reason.stateDifferences)) entry.reasons.state++;
288
+ if (Array.isArray(r.reason.hookDifferences)) entry.reasons.hooks++;
276
289
  }
277
290
  return summary;
278
291
  }
@@ -284,10 +297,37 @@ var RenderStore = class {
284
297
  summary[r.project] ??= {};
285
298
  summary[r.project][r.commitId] ??= {};
286
299
  const commit = summary[r.project][r.commitId];
287
- commit[r.displayName] = (commit[r.displayName] ?? 0) + 1;
300
+ commit[r.displayName] ??= {
301
+ count: 0,
302
+ reasons: {
303
+ props: 0,
304
+ state: 0,
305
+ hooks: 0
306
+ }
307
+ };
308
+ const entry = commit[r.displayName];
309
+ entry.count++;
310
+ if (Array.isArray(r.reason.propsDifferences)) entry.reasons.props++;
311
+ if (Array.isArray(r.reason.stateDifferences)) entry.reasons.state++;
312
+ if (Array.isArray(r.reason.hookDifferences)) entry.reasons.hooks++;
288
313
  }
289
314
  return summary;
290
315
  }
316
+ setTrackedComponents(components, projectId) {
317
+ this.trackedComponents.set(projectId, components);
318
+ }
319
+ getTrackedComponents(projectId) {
320
+ const result = {};
321
+ const projects = projectId ? [projectId] : this.getProjects();
322
+ for (const proj of projects) {
323
+ const observed = [...new Set(this.getAllRenders(proj).map((r) => r.displayName))];
324
+ result[proj] = {
325
+ registered: this.trackedComponents.get(proj) ?? [],
326
+ observed
327
+ };
328
+ }
329
+ return result;
330
+ }
291
331
  bufferKey(projectId, commitId) {
292
332
  return `${projectId}\0${commitId ?? NOCOMMIT}`;
293
333
  }
@@ -355,7 +395,7 @@ function textResult(text) {
355
395
  }
356
396
  //#endregion
357
397
  //#region src/server/tools/clear-renders.ts
358
- function register$5(server) {
398
+ function register$6(server) {
359
399
  server.registerTool("clear_renders", {
360
400
  title: "Clear Renders",
361
401
  description: "Clears collected render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
@@ -369,7 +409,7 @@ function register$5(server) {
369
409
  }
370
410
  //#endregion
371
411
  //#region src/server/tools/get-commits.ts
372
- function register$4(server) {
412
+ function register$5(server) {
373
413
  server.registerTool("get_commits", {
374
414
  title: "Get Commits",
375
415
  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.",
@@ -384,7 +424,7 @@ function register$4(server) {
384
424
  }
385
425
  //#endregion
386
426
  //#region src/server/tools/get-projects.ts
387
- function register$3(server) {
427
+ function register$4(server) {
388
428
  server.registerTool("get_projects", {
389
429
  title: "Get Projects",
390
430
  description: "Returns a list of project identifiers (browser origin URLs) that have recorded render data.",
@@ -397,7 +437,7 @@ function register$3(server) {
397
437
  }
398
438
  //#endregion
399
439
  //#region src/server/tools/get-render-summary.ts
400
- function register$2(server) {
440
+ function register$3(server) {
401
441
  server.registerTool("get_render_summary", {
402
442
  title: "Get Render Summary",
403
443
  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.",
@@ -412,13 +452,20 @@ function register$2(server) {
412
452
  return aggregateSummary(resolved.projectId);
413
453
  });
414
454
  }
455
+ function formatReasons(reasons) {
456
+ const parts = [];
457
+ if (reasons.props > 0) parts.push(`props: ${reasons.props}`);
458
+ if (reasons.state > 0) parts.push(`state: ${reasons.state}`);
459
+ if (reasons.hooks > 0) parts.push(`hooks: ${reasons.hooks}`);
460
+ return parts.length > 0 ? ` — ${parts.join(", ")}` : "";
461
+ }
415
462
  function aggregateSummary(projectId) {
416
463
  const summary = store.getSummary(projectId);
417
464
  if (Object.keys(summary).length === 0) return textResult("No renders recorded yet.");
418
465
  const lines = [];
419
466
  for (const [proj, components] of Object.entries(summary)) {
420
467
  lines.push(`[${proj}]`);
421
- for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count} re-render(s)`);
468
+ for (const [name, { count, reasons }] of Object.entries(components)) lines.push(` ${name}: ${count} re-render(s)${formatReasons(reasons)}`);
422
469
  }
423
470
  return textResult(`Re-render summary:\n\n${lines.join("\n")}`);
424
471
  }
@@ -431,16 +478,16 @@ function commitSummary(projectId) {
431
478
  const sortedCommitIds = Object.keys(commits).map(Number).sort((a, b) => a - b);
432
479
  for (const commitId of sortedCommitIds) {
433
480
  const components = commits[commitId];
434
- const total = Object.values(components).reduce((s, c) => s + c, 0);
481
+ const total = Object.values(components).reduce((s, c) => s + c.count, 0);
435
482
  lines.push(` Commit #${commitId} (${total} re-render(s)):`);
436
- for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count}`);
483
+ for (const [name, { count, reasons }] of Object.entries(components)) lines.push(` ${name}: ${count}${formatReasons(reasons)}`);
437
484
  }
438
485
  }
439
486
  return textResult(`Re-render summary (by commit):\n\n${lines.join("\n")}`);
440
487
  }
441
488
  //#endregion
442
489
  //#region src/server/tools/get-renders-by-commit.ts
443
- function register$1(server) {
490
+ function register$2(server) {
444
491
  server.registerTool("get_renders_by_commit", {
445
492
  title: "Get Renders by Commit",
446
493
  description: "Returns all re-renders for a specific React commit ID. Use get_commits first to discover available commit IDs.",
@@ -460,7 +507,7 @@ function register$1(server) {
460
507
  }
461
508
  //#endregion
462
509
  //#region src/server/tools/get-renders.ts
463
- function register(server) {
510
+ function register$1(server) {
464
511
  server.registerTool("get_renders", {
465
512
  title: "Get Renders",
466
513
  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.",
@@ -477,14 +524,43 @@ function register(server) {
477
524
  });
478
525
  }
479
526
  //#endregion
527
+ //#region src/server/tools/get-tracked-components.ts
528
+ function register(server) {
529
+ server.registerTool("get_tracked_components", {
530
+ title: "Get Tracked Components",
531
+ 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.",
532
+ inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
533
+ }, async ({ project }) => {
534
+ const resolved = resolveProject(project);
535
+ if (resolved.error) return textResult(resolved.error);
536
+ const tracked = store.getTrackedComponents(resolved.projectId);
537
+ if (Object.keys(tracked).length === 0) return textResult("No tracked components found. Make sure the browser is connected and triggering re-renders.");
538
+ const lines = [];
539
+ for (const [proj, { registered, observed }] of Object.entries(tracked)) {
540
+ lines.push(`[${proj}]`);
541
+ if (registered.length > 0) {
542
+ lines.push(" Registered:");
543
+ for (const name of registered) lines.push(` - ${name}`);
544
+ }
545
+ if (observed.length > 0) {
546
+ lines.push(" Observed in renders:");
547
+ for (const name of observed) lines.push(` - ${name}`);
548
+ }
549
+ if (registered.length === 0 && observed.length === 0) lines.push(" No components tracked yet.");
550
+ }
551
+ return textResult(lines.join("\n"));
552
+ });
553
+ }
554
+ //#endregion
480
555
  //#region src/server/tools/index.ts
481
556
  function registerTools(server) {
482
- register(server);
483
- register$2(server);
484
- register$4(server);
485
557
  register$1(server);
486
558
  register$3(server);
487
559
  register$5(server);
560
+ register$2(server);
561
+ register$4(server);
562
+ register(server);
563
+ register$6(server);
488
564
  }
489
565
  //#endregion
490
566
  //#region src/server/liveness.ts
@@ -563,6 +639,7 @@ function createWsServer(port) {
563
639
  heartbeat.setProjectId(ws, projectId);
564
640
  if (msg.type === "render") store.addRender(msg.payload, projectId, msg.commitId);
565
641
  else if (msg.type === "render-batch") for (const report of msg.payload) store.addRender(report, projectId, msg.commitId);
642
+ else if (msg.type === "register") store.setTrackedComponents(msg.components, projectId);
566
643
  } catch {
567
644
  console.error("[wdyr-mcp] invalid message received");
568
645
  }
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.13",
3
+ "version": "1.0.0-dev.15",
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",