@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.1 → 1.0.0-dev.3
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 +9 -0
- package/dist/client/index.cjs +46 -7
- package/dist/client/index.d.cts +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +46 -7
- package/dist/server/index.js +55 -12
- package/package.json +1 -1
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
|
```
|
package/dist/client/index.cjs
CHANGED
|
@@ -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", () => {
|
|
@@ -57,16 +75,37 @@ function buildOptions(opts) {
|
|
|
57
75
|
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
58
76
|
else queue.push(msg);
|
|
59
77
|
}
|
|
60
|
-
|
|
78
|
+
let pendingBatch = null;
|
|
79
|
+
let flushScheduled = false;
|
|
80
|
+
function flushBatch() {
|
|
81
|
+
flushScheduled = false;
|
|
82
|
+
if (!pendingBatch || pendingBatch.reports.length === 0) return;
|
|
61
83
|
send({
|
|
62
|
-
type: "render",
|
|
84
|
+
type: "render-batch",
|
|
63
85
|
projectId,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
reason: sanitizeReason(info.reason),
|
|
67
|
-
hookName: info.hookName
|
|
68
|
-
}
|
|
86
|
+
commitId: pendingBatch.commitId,
|
|
87
|
+
payload: pendingBatch.reports
|
|
69
88
|
});
|
|
89
|
+
pendingBatch = null;
|
|
90
|
+
}
|
|
91
|
+
return { notifier(info) {
|
|
92
|
+
const report = {
|
|
93
|
+
displayName: info.displayName,
|
|
94
|
+
reason: sanitizeReason(info.reason),
|
|
95
|
+
hookName: info.hookName
|
|
96
|
+
};
|
|
97
|
+
if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
|
|
98
|
+
else {
|
|
99
|
+
if (pendingBatch) flushBatch();
|
|
100
|
+
pendingBatch = {
|
|
101
|
+
commitId,
|
|
102
|
+
reports: [report]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!flushScheduled) {
|
|
106
|
+
flushScheduled = true;
|
|
107
|
+
queueMicrotask(flushBatch);
|
|
108
|
+
}
|
|
70
109
|
} };
|
|
71
110
|
}
|
|
72
111
|
//#endregion
|
package/dist/client/index.d.cts
CHANGED
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -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", () => {
|
|
@@ -56,16 +74,37 @@ function buildOptions(opts) {
|
|
|
56
74
|
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
57
75
|
else queue.push(msg);
|
|
58
76
|
}
|
|
59
|
-
|
|
77
|
+
let pendingBatch = null;
|
|
78
|
+
let flushScheduled = false;
|
|
79
|
+
function flushBatch() {
|
|
80
|
+
flushScheduled = false;
|
|
81
|
+
if (!pendingBatch || pendingBatch.reports.length === 0) return;
|
|
60
82
|
send({
|
|
61
|
-
type: "render",
|
|
83
|
+
type: "render-batch",
|
|
62
84
|
projectId,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
reason: sanitizeReason(info.reason),
|
|
66
|
-
hookName: info.hookName
|
|
67
|
-
}
|
|
85
|
+
commitId: pendingBatch.commitId,
|
|
86
|
+
payload: pendingBatch.reports
|
|
68
87
|
});
|
|
88
|
+
pendingBatch = null;
|
|
89
|
+
}
|
|
90
|
+
return { notifier(info) {
|
|
91
|
+
const report = {
|
|
92
|
+
displayName: info.displayName,
|
|
93
|
+
reason: sanitizeReason(info.reason),
|
|
94
|
+
hookName: info.hookName
|
|
95
|
+
};
|
|
96
|
+
if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
|
|
97
|
+
else {
|
|
98
|
+
if (pendingBatch) flushBatch();
|
|
99
|
+
pendingBatch = {
|
|
100
|
+
commitId,
|
|
101
|
+
reports: [report]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!flushScheduled) {
|
|
105
|
+
flushScheduled = true;
|
|
106
|
+
queueMicrotask(flushBatch);
|
|
107
|
+
}
|
|
69
108
|
} };
|
|
70
109
|
}
|
|
71
110
|
//#endregion
|
package/dist/server/index.js
CHANGED
|
@@ -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$
|
|
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$
|
|
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$
|
|
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
|
|
@@ -300,11 +344,10 @@ function createWsServer(port) {
|
|
|
300
344
|
ws.on("message", (raw) => {
|
|
301
345
|
try {
|
|
302
346
|
const msg = JSON.parse(String(raw));
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
347
|
+
const projectId = msg.projectId ?? "default";
|
|
348
|
+
heartbeat.setProjectId(ws, projectId);
|
|
349
|
+
if (msg.type === "render") store.addRender(msg.payload, projectId, msg.commitId);
|
|
350
|
+
else if (msg.type === "render-batch") for (const report of msg.payload) store.addRender(report, projectId, msg.commitId);
|
|
308
351
|
} catch {
|
|
309
352
|
console.error("[wdyr-mcp] invalid message received");
|
|
310
353
|
}
|
package/package.json
CHANGED