@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.1 → 1.0.0-dev.2

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.
package/README.md CHANGED
@@ -94,11 +94,20 @@ Once both the MCP server and your React dev server are running, interact with yo
94
94
  | --- | --- |
95
95
  | `get_unnecessary_renders` | Returns all captured unnecessary re-renders. Optionally filter by `component` name. |
96
96
  | `get_render_summary` | Returns a summary of re-renders grouped by component with counts. |
97
+ | `get_commits` | Lists React commit IDs that have recorded render data. Use these IDs with `get_renders_by_commit`. |
98
+ | `get_renders_by_commit` | Returns all unnecessary re-renders for a specific React commit ID. |
97
99
  | `get_projects` | Lists all active projects (identified by their origin URL). |
98
100
  | `clear_renders` | Clears all stored render data. Optionally scope to a specific project. |
99
101
 
100
102
  When multiple projects are active, tools accept an optional `project` parameter (the browser's origin URL, e.g. `http://localhost:3000`). If omitted and only one project exists, it is auto-selected.
101
103
 
104
+ ### Commit-level grouping
105
+
106
+ Each render report is tagged with a React **commit ID**, allowing agents to inspect which components re-rendered together in the same commit. The client tracks commits by hooking into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot`, which React calls synchronously once per commit. A typical workflow:
107
+
108
+ 1. Call `get_commits` to list available commit IDs
109
+ 2. Call `get_renders_by_commit` with a specific ID to see all renders in that commit
110
+
102
111
  ## Architecture
103
112
 
104
113
  ```
@@ -33,11 +33,29 @@ function sanitizeReason(reason) {
33
33
  //#endregion
34
34
  //#region src/client/index.ts
35
35
  const DEFAULT_WS_URL = "ws://localhost:4649";
36
+ function patchDevToolsHook(onCommit) {
37
+ if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
38
+ supportsFiber: true,
39
+ inject() {},
40
+ onCommitFiberRoot() {},
41
+ onCommitFiberUnmount() {}
42
+ };
43
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
44
+ const original = hook.onCommitFiberRoot.bind(hook);
45
+ hook.onCommitFiberRoot = (...args) => {
46
+ onCommit();
47
+ return original(...args);
48
+ };
49
+ }
36
50
  function buildOptions(opts) {
37
51
  const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
38
52
  const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
39
53
  let ws = null;
40
54
  let queue = [];
55
+ let commitId = 0;
56
+ patchDevToolsHook(() => {
57
+ commitId++;
58
+ });
41
59
  function connect() {
42
60
  ws = new WebSocket(wsUrl);
43
61
  ws.addEventListener("open", () => {
@@ -61,6 +79,7 @@ function buildOptions(opts) {
61
79
  send({
62
80
  type: "render",
63
81
  projectId,
82
+ commitId,
64
83
  payload: {
65
84
  displayName: info.displayName,
66
85
  reason: sanitizeReason(info.reason),
@@ -1,6 +1,7 @@
1
1
  import { UpdateInfo } from "@welldone-software/why-did-you-render";
2
2
 
3
3
  //#region src/client/index.d.ts
4
+ sideEffect();
4
5
  interface ClientOptions {
5
6
  wsUrl?: string;
6
7
  projectId?: string;
@@ -1,6 +1,7 @@
1
1
  import { UpdateInfo } from "@welldone-software/why-did-you-render";
2
2
 
3
3
  //#region src/client/index.d.ts
4
+ sideEffect();
4
5
  interface ClientOptions {
5
6
  wsUrl?: string;
6
7
  projectId?: string;
@@ -32,11 +32,29 @@ function sanitizeReason(reason) {
32
32
  //#endregion
33
33
  //#region src/client/index.ts
34
34
  const DEFAULT_WS_URL = "ws://localhost:4649";
35
+ function patchDevToolsHook(onCommit) {
36
+ if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
37
+ supportsFiber: true,
38
+ inject() {},
39
+ onCommitFiberRoot() {},
40
+ onCommitFiberUnmount() {}
41
+ };
42
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
43
+ const original = hook.onCommitFiberRoot.bind(hook);
44
+ hook.onCommitFiberRoot = (...args) => {
45
+ onCommit();
46
+ return original(...args);
47
+ };
48
+ }
35
49
  function buildOptions(opts) {
36
50
  const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
37
51
  const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
38
52
  let ws = null;
39
53
  let queue = [];
54
+ let commitId = 0;
55
+ patchDevToolsHook(() => {
56
+ commitId++;
57
+ });
40
58
  function connect() {
41
59
  ws = new WebSocket(wsUrl);
42
60
  ws.addEventListener("open", () => {
@@ -60,6 +78,7 @@ function buildOptions(opts) {
60
78
  send({
61
79
  type: "render",
62
80
  projectId,
81
+ commitId,
63
82
  payload: {
64
83
  displayName: info.displayName,
65
84
  reason: sanitizeReason(info.reason),
@@ -23,7 +23,8 @@ function toResult(stored) {
23
23
  project: stored.projectId,
24
24
  displayName: stored.displayName,
25
25
  reason: stored.reason,
26
- ...stored.hookName != null && { hookName: stored.hookName }
26
+ ...stored.hookName != null && { hookName: stored.hookName },
27
+ ...stored.commitId != null && { commitId: stored.commitId }
27
28
  };
28
29
  }
29
30
  //#endregion
@@ -37,10 +38,11 @@ var RenderStore = class {
37
38
  this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
38
39
  mkdirSync(this.dir, { recursive: true });
39
40
  }
40
- addRender(report, projectId) {
41
+ addRender(report, projectId, commitId) {
41
42
  const stored = {
42
43
  ...report,
43
- projectId
44
+ projectId,
45
+ ...commitId != null && { commitId }
44
46
  };
45
47
  let buf = this.buffers.get(projectId);
46
48
  if (!buf) {
@@ -104,6 +106,13 @@ var RenderStore = class {
104
106
  }
105
107
  return [...projects];
106
108
  }
109
+ getCommitIds(projectId) {
110
+ const renders = this.getAllRenders(projectId);
111
+ return [...new Set(renders.map((r) => r.commitId).filter((id) => id != null))];
112
+ }
113
+ getRendersByCommit(commitId, projectId) {
114
+ return this.getAllRenders(projectId).filter((r) => r.commitId === commitId);
115
+ }
107
116
  getSummary(projectId) {
108
117
  const renders = this.getAllRenders(projectId);
109
118
  const summary = {};
@@ -156,7 +165,7 @@ function textResult(text) {
156
165
  }
157
166
  //#endregion
158
167
  //#region src/server/tools/clear-renders.ts
159
- function register$3(server) {
168
+ function register$5(server) {
160
169
  server.registerTool("clear_renders", {
161
170
  title: "Clear Renders",
162
171
  description: "Clears collected render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
@@ -169,8 +178,23 @@ function register$3(server) {
169
178
  });
170
179
  }
171
180
  //#endregion
181
+ //#region src/server/tools/get-commits.ts
182
+ function register$4(server) {
183
+ server.registerTool("get_commits", {
184
+ title: "Get Commits",
185
+ 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.",
186
+ inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
187
+ }, async ({ project }) => {
188
+ const resolved = resolveProject(project);
189
+ if (resolved.error) return textResult(resolved.error);
190
+ const commitIds = store.getCommitIds(resolved.projectId);
191
+ if (commitIds.length === 0) return textResult("No commits recorded yet. Make sure the browser is connected and triggering re-renders.");
192
+ return textResult(JSON.stringify(commitIds));
193
+ });
194
+ }
195
+ //#endregion
172
196
  //#region src/server/tools/get-projects.ts
173
- function register$2(server) {
197
+ function register$3(server) {
174
198
  server.registerTool("get_projects", {
175
199
  title: "Get Projects",
176
200
  description: "Returns a list of project identifiers (browser origin URLs) that have recorded render data.",
@@ -183,7 +207,7 @@ function register$2(server) {
183
207
  }
184
208
  //#endregion
185
209
  //#region src/server/tools/get-render-summary.ts
186
- function register$1(server) {
210
+ function register$2(server) {
187
211
  server.registerTool("get_render_summary", {
188
212
  title: "Get Render Summary",
189
213
  description: "Returns a summary of unnecessary 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.",
@@ -202,6 +226,24 @@ function register$1(server) {
202
226
  });
203
227
  }
204
228
  //#endregion
229
+ //#region src/server/tools/get-renders-by-commit.ts
230
+ function register$1(server) {
231
+ server.registerTool("get_renders_by_commit", {
232
+ title: "Get Renders by Commit",
233
+ description: "Returns all unnecessary re-renders for a specific React commit ID. Use get_commits first to discover available commit IDs.",
234
+ inputSchema: {
235
+ commitId: z.number().describe("The React commit ID to filter by."),
236
+ project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.")
237
+ }
238
+ }, async ({ commitId, project }) => {
239
+ const resolved = resolveProject(project);
240
+ if (resolved.error) return textResult(resolved.error);
241
+ const renders = store.getRendersByCommit(commitId, resolved.projectId);
242
+ if (renders.length === 0) return textResult(`No renders recorded for commit ${commitId}.`);
243
+ return textResult(JSON.stringify(renders, null, 2));
244
+ });
245
+ }
246
+ //#endregion
205
247
  //#region src/server/tools/get-unnecessary-renders.ts
206
248
  function register(server) {
207
249
  server.registerTool("get_unnecessary_renders", {
@@ -223,9 +265,11 @@ function register(server) {
223
265
  //#region src/server/tools/index.ts
224
266
  function registerTools(server) {
225
267
  register(server);
226
- register$1(server);
227
268
  register$2(server);
269
+ register$4(server);
270
+ register$1(server);
228
271
  register$3(server);
272
+ register$5(server);
229
273
  }
230
274
  //#endregion
231
275
  //#region src/server/liveness.ts
@@ -303,7 +347,7 @@ function createWsServer(port) {
303
347
  if (msg.type === "render") {
304
348
  const projectId = msg.projectId ?? "default";
305
349
  heartbeat.setProjectId(ws, projectId);
306
- store.addRender(msg.payload, projectId);
350
+ store.addRender(msg.payload, projectId, msg.commitId);
307
351
  }
308
352
  } catch {
309
353
  console.error("[wdyr-mcp] invalid message received");
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.1",
3
+ "version": "1.0.0-dev.2",
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",