@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.7 → 1.0.0-dev.9
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 +77 -7
- package/dist/client/index.cjs +7 -0
- package/dist/client/index.js +7 -0
- package/dist/server/index.js +90 -36
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# why-did-you-render-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@0x1f320.sh/why-did-you-render-mcp)
|
|
4
|
+
[](https://github.com/0x1f320/why-did-you-render-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@0x1f320.sh/why-did-you-render-mcp)
|
|
6
|
+
|
|
3
7
|
An [MCP](https://modelcontextprotocol.io/) server that bridges [why-did-you-render](https://github.com/welldone-software/why-did-you-render) data from the browser to coding agents. It captures unnecessary React re-render reports in real time and exposes them as MCP tools, so agents can diagnose and fix performance issues without manual browser inspection.
|
|
4
8
|
|
|
5
9
|
## How It Works
|
|
@@ -39,10 +43,8 @@ import whyDidYouRender from "@welldone-software/why-did-you-render";
|
|
|
39
43
|
import { buildOptions } from "@0x1f320.sh/why-did-you-render-mcp/client";
|
|
40
44
|
|
|
41
45
|
if (process.env.NODE_ENV === "development") {
|
|
42
|
-
const { notifier } = buildOptions();
|
|
43
|
-
|
|
44
46
|
whyDidYouRender(React, {
|
|
45
|
-
|
|
47
|
+
...buildOptions(),
|
|
46
48
|
trackAllPureComponents: true,
|
|
47
49
|
});
|
|
48
50
|
}
|
|
@@ -59,31 +61,99 @@ const { notifier } = buildOptions({
|
|
|
59
61
|
|
|
60
62
|
### 2. Add the MCP server to your agent
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
<details>
|
|
65
|
+
<summary>Claude Code</summary>
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
claude mcp add why-did-you-render -- npx -y @0x1f320.sh/why-did-you-render-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary>Claude Desktop</summary>
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
claude mcp add-json why-did-you-render '{"command":"npx","args":["-y","@0x1f320.sh/why-did-you-render-mcp"]}' -s user
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or manually edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"why-did-you-render": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary>Cursor</summary>
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
cursor --add-mcp '{"name":"why-did-you-render","command":"npx","args":["-y","@0x1f320.sh/why-did-you-render-mcp"]}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or add to `.cursor/mcp.json` in your project:
|
|
63
103
|
|
|
64
104
|
```json
|
|
65
105
|
{
|
|
66
106
|
"mcpServers": {
|
|
67
107
|
"why-did-you-render": {
|
|
68
108
|
"command": "npx",
|
|
69
|
-
"args": ["@0x1f320.sh/why-did-you-render-mcp"]
|
|
109
|
+
"args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
|
|
70
110
|
}
|
|
71
111
|
}
|
|
72
112
|
}
|
|
73
113
|
```
|
|
74
114
|
|
|
75
|
-
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
<summary>Windsurf</summary>
|
|
119
|
+
|
|
120
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
76
121
|
|
|
77
122
|
```json
|
|
78
123
|
{
|
|
79
124
|
"mcpServers": {
|
|
80
125
|
"why-did-you-render": {
|
|
81
|
-
"command": "
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
|
|
82
128
|
}
|
|
83
129
|
}
|
|
84
130
|
}
|
|
85
131
|
```
|
|
86
132
|
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>VS Code (GitHub Copilot)</summary>
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
code --add-mcp '{"name":"why-did-you-render","command":"npx","args":["-y","@0x1f320.sh/why-did-you-render-mcp"]}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or add to `.vscode/mcp.json` in your project:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"servers": {
|
|
147
|
+
"why-did-you-render": {
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
</details>
|
|
156
|
+
|
|
87
157
|
### 3. Start your dev server and interact with the app
|
|
88
158
|
|
|
89
159
|
Once both the MCP server and your React dev server are running, interact with your app in the browser. The agent can now query re-render data using the MCP tools below.
|
package/dist/client/index.cjs
CHANGED
|
@@ -135,6 +135,11 @@ function sanitizeReason(reason) {
|
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/client/index.ts
|
|
137
137
|
const DEFAULT_WS_URL = "ws://localhost:4649";
|
|
138
|
+
const PREFIX_STYLE = "color: #38bdf8; font-weight: bold";
|
|
139
|
+
const RESET_STYLE = "color: inherit; font-weight: normal";
|
|
140
|
+
function log(message) {
|
|
141
|
+
console.log(`%c[WDYR MCP]%c ${message}`, PREFIX_STYLE, RESET_STYLE);
|
|
142
|
+
}
|
|
138
143
|
function patchDevToolsHook(onCommit) {
|
|
139
144
|
if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
140
145
|
supportsFiber: true,
|
|
@@ -165,6 +170,7 @@ function buildOptions(opts) {
|
|
|
165
170
|
function connect() {
|
|
166
171
|
ws = new WebSocket(wsUrl);
|
|
167
172
|
ws.addEventListener("open", () => {
|
|
173
|
+
log(`Connected to ${wsUrl}`);
|
|
168
174
|
retryDelay = BASE_DELAY;
|
|
169
175
|
for (const msg of queue) ws?.send(JSON.stringify(msg));
|
|
170
176
|
queue = [];
|
|
@@ -175,6 +181,7 @@ function buildOptions(opts) {
|
|
|
175
181
|
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
|
|
176
182
|
});
|
|
177
183
|
ws.addEventListener("error", () => {
|
|
184
|
+
log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
|
|
178
185
|
ws?.close();
|
|
179
186
|
});
|
|
180
187
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -134,6 +134,11 @@ function sanitizeReason(reason) {
|
|
|
134
134
|
//#endregion
|
|
135
135
|
//#region src/client/index.ts
|
|
136
136
|
const DEFAULT_WS_URL = "ws://localhost:4649";
|
|
137
|
+
const PREFIX_STYLE = "color: #38bdf8; font-weight: bold";
|
|
138
|
+
const RESET_STYLE = "color: inherit; font-weight: normal";
|
|
139
|
+
function log(message) {
|
|
140
|
+
console.log(`%c[WDYR MCP]%c ${message}`, PREFIX_STYLE, RESET_STYLE);
|
|
141
|
+
}
|
|
137
142
|
function patchDevToolsHook(onCommit) {
|
|
138
143
|
if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
139
144
|
supportsFiber: true,
|
|
@@ -164,6 +169,7 @@ function buildOptions(opts) {
|
|
|
164
169
|
function connect() {
|
|
165
170
|
ws = new WebSocket(wsUrl);
|
|
166
171
|
ws.addEventListener("open", () => {
|
|
172
|
+
log(`Connected to ${wsUrl}`);
|
|
167
173
|
retryDelay = BASE_DELAY;
|
|
168
174
|
for (const msg of queue) ws?.send(JSON.stringify(msg));
|
|
169
175
|
queue = [];
|
|
@@ -174,6 +180,7 @@ function buildOptions(opts) {
|
|
|
174
180
|
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
|
|
175
181
|
});
|
|
176
182
|
ws.addEventListener("error", () => {
|
|
183
|
+
log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
|
|
177
184
|
ws?.close();
|
|
178
185
|
});
|
|
179
186
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -123,11 +123,13 @@ function toResult(stored) {
|
|
|
123
123
|
//#endregion
|
|
124
124
|
//#region src/server/store/render-store.ts
|
|
125
125
|
const FLUSH_DELAY_MS = 200;
|
|
126
|
+
const NOCOMMIT = "nocommit";
|
|
126
127
|
var RenderStore = class {
|
|
127
128
|
dir;
|
|
128
129
|
buffers = /* @__PURE__ */ new Map();
|
|
129
130
|
timers = /* @__PURE__ */ new Map();
|
|
130
131
|
dicts = /* @__PURE__ */ new Map();
|
|
132
|
+
bufferMeta = /* @__PURE__ */ new Map();
|
|
131
133
|
constructor(dir) {
|
|
132
134
|
this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
|
|
133
135
|
mkdirSync(this.dir, { recursive: true });
|
|
@@ -138,46 +140,53 @@ var RenderStore = class {
|
|
|
138
140
|
projectId,
|
|
139
141
|
...commitId != null && { commitId }
|
|
140
142
|
};
|
|
141
|
-
|
|
143
|
+
const bk = this.bufferKey(projectId, commitId);
|
|
144
|
+
let buf = this.buffers.get(bk);
|
|
142
145
|
if (!buf) {
|
|
143
146
|
buf = [];
|
|
144
|
-
this.buffers.set(
|
|
147
|
+
this.buffers.set(bk, buf);
|
|
148
|
+
this.bufferMeta.set(bk, {
|
|
149
|
+
projectId,
|
|
150
|
+
commitId
|
|
151
|
+
});
|
|
145
152
|
}
|
|
146
153
|
buf.push(stored);
|
|
147
|
-
const existing = this.timers.get(
|
|
154
|
+
const existing = this.timers.get(bk);
|
|
148
155
|
if (existing) clearTimeout(existing);
|
|
149
|
-
this.timers.set(
|
|
150
|
-
this.flushAsync(projectId).catch((err) => console.error(`[wdyr-mcp] flush error for ${
|
|
156
|
+
this.timers.set(bk, setTimeout(() => {
|
|
157
|
+
this.flushAsync(projectId, commitId).catch((err) => console.error(`[wdyr-mcp] flush error for ${bk}:`, err));
|
|
151
158
|
}, FLUSH_DELAY_MS));
|
|
152
159
|
}
|
|
153
|
-
async flushAsync(projectId) {
|
|
160
|
+
async flushAsync(projectId, commitId) {
|
|
154
161
|
await ensureReady();
|
|
155
|
-
|
|
156
|
-
else for (const id of this.buffers.keys()) this.flushProject(id);
|
|
162
|
+
this.flush(projectId, commitId);
|
|
157
163
|
}
|
|
158
|
-
flush(projectId) {
|
|
159
|
-
if (projectId) this.
|
|
160
|
-
else for (const
|
|
164
|
+
flush(projectId, commitId) {
|
|
165
|
+
if (projectId != null && commitId !== void 0) this.flushBuffer(this.bufferKey(projectId, commitId));
|
|
166
|
+
else if (projectId != null) for (const bk of this.bufferKeysForProject(projectId)) this.flushBuffer(bk);
|
|
167
|
+
else for (const bk of [...this.buffers.keys()]) this.flushBuffer(bk);
|
|
161
168
|
}
|
|
162
|
-
|
|
163
|
-
const buf = this.buffers.get(
|
|
169
|
+
flushBuffer(bk) {
|
|
170
|
+
const buf = this.buffers.get(bk);
|
|
164
171
|
if (!buf || buf.length === 0) return;
|
|
165
|
-
|
|
172
|
+
const meta = this.bufferMeta.get(bk);
|
|
173
|
+
if (!meta) return;
|
|
174
|
+
let dict = this.dicts.get(bk);
|
|
166
175
|
if (!dict) {
|
|
167
176
|
dict = {};
|
|
168
|
-
this.dicts.set(
|
|
177
|
+
this.dicts.set(bk, dict);
|
|
169
178
|
}
|
|
170
179
|
const dehydrated = buf.map((r) => dehydrate(r, dict));
|
|
171
|
-
const file = this.
|
|
180
|
+
const file = this.commitFile(meta.projectId, meta.commitId);
|
|
172
181
|
const existingLines = this.readDataLines(file);
|
|
173
182
|
const newLines = dehydrated.map((r) => JSON.stringify(r));
|
|
174
183
|
const allDataLines = [...existingLines, ...newLines];
|
|
175
184
|
writeFileSync(file, `${(Object.keys(dict).length > 0 ? [JSON.stringify({ [DICT_KEY]: dict }), ...allDataLines] : allDataLines).join("\n")}\n`);
|
|
176
185
|
buf.length = 0;
|
|
177
|
-
const timer = this.timers.get(
|
|
186
|
+
const timer = this.timers.get(bk);
|
|
178
187
|
if (timer) {
|
|
179
188
|
clearTimeout(timer);
|
|
180
|
-
this.timers.delete(
|
|
189
|
+
this.timers.delete(bk);
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
readDataLines(file) {
|
|
@@ -190,7 +199,7 @@ var RenderStore = class {
|
|
|
190
199
|
}
|
|
191
200
|
getAllRenders(projectId) {
|
|
192
201
|
this.flush(projectId);
|
|
193
|
-
if (projectId) return
|
|
202
|
+
if (projectId) return this.projectFiles(projectId).flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
194
203
|
return this.jsonlFiles().flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
195
204
|
}
|
|
196
205
|
getRendersByComponent(componentName, projectId) {
|
|
@@ -198,44 +207,64 @@ var RenderStore = class {
|
|
|
198
207
|
}
|
|
199
208
|
clearRenders(projectId) {
|
|
200
209
|
if (projectId) {
|
|
201
|
-
this.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
for (const bk of this.bufferKeysForProject(projectId)) {
|
|
211
|
+
this.buffers.delete(bk);
|
|
212
|
+
this.dicts.delete(bk);
|
|
213
|
+
this.bufferMeta.delete(bk);
|
|
214
|
+
const timer = this.timers.get(bk);
|
|
215
|
+
if (timer) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
this.timers.delete(bk);
|
|
218
|
+
}
|
|
207
219
|
}
|
|
208
|
-
const
|
|
209
|
-
if (existsSync(file)) unlinkSync(file);
|
|
220
|
+
for (const f of this.projectFiles(projectId)) unlinkSync(join(this.dir, f));
|
|
210
221
|
} else {
|
|
211
|
-
for (const [
|
|
222
|
+
for (const [, timer] of this.timers) clearTimeout(timer);
|
|
212
223
|
this.buffers.clear();
|
|
213
224
|
this.timers.clear();
|
|
214
225
|
this.dicts.clear();
|
|
226
|
+
this.bufferMeta.clear();
|
|
215
227
|
for (const f of this.jsonlFiles()) unlinkSync(join(this.dir, f));
|
|
216
228
|
}
|
|
217
229
|
}
|
|
218
230
|
getProjects() {
|
|
219
231
|
this.flush();
|
|
220
232
|
const projects = /* @__PURE__ */ new Set();
|
|
233
|
+
const seen = /* @__PURE__ */ new Set();
|
|
221
234
|
for (const f of this.jsonlFiles()) {
|
|
235
|
+
const parsed = this.parseFilename(f);
|
|
236
|
+
if (!parsed) continue;
|
|
237
|
+
if (seen.has(parsed.projectSanitized)) continue;
|
|
238
|
+
seen.add(parsed.projectSanitized);
|
|
222
239
|
const lines = readFileSync(join(this.dir, f), "utf-8").split("\n");
|
|
223
240
|
for (const line of lines) {
|
|
224
241
|
if (!line) continue;
|
|
225
|
-
const
|
|
226
|
-
if ("@@dict" in
|
|
227
|
-
projects.add(
|
|
242
|
+
const obj = JSON.parse(line);
|
|
243
|
+
if ("@@dict" in obj) continue;
|
|
244
|
+
projects.add(obj.projectId);
|
|
228
245
|
break;
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
return [...projects];
|
|
232
249
|
}
|
|
233
250
|
getCommitIds(projectId) {
|
|
234
|
-
|
|
235
|
-
|
|
251
|
+
this.flush(projectId);
|
|
252
|
+
const files = projectId ? this.projectFiles(projectId) : this.jsonlFiles();
|
|
253
|
+
const ids = /* @__PURE__ */ new Set();
|
|
254
|
+
for (const f of files) {
|
|
255
|
+
const parsed = this.parseFilename(f);
|
|
256
|
+
if (parsed?.commitId != null) ids.add(parsed.commitId);
|
|
257
|
+
}
|
|
258
|
+
return [...ids].sort((a, b) => a - b);
|
|
236
259
|
}
|
|
237
260
|
getRendersByCommit(commitId, projectId) {
|
|
238
|
-
|
|
261
|
+
if (projectId) {
|
|
262
|
+
this.flush(projectId, commitId);
|
|
263
|
+
return readJsonl(this.commitFile(projectId, commitId)).map(toResult);
|
|
264
|
+
}
|
|
265
|
+
this.flush();
|
|
266
|
+
const suffix = `_commit_${commitId}.jsonl`;
|
|
267
|
+
return this.jsonlFiles().filter((f) => f.endsWith(suffix)).flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
239
268
|
}
|
|
240
269
|
getSummary(projectId) {
|
|
241
270
|
const renders = this.getAllRenders(projectId);
|
|
@@ -247,8 +276,33 @@ var RenderStore = class {
|
|
|
247
276
|
}
|
|
248
277
|
return summary;
|
|
249
278
|
}
|
|
250
|
-
|
|
251
|
-
return
|
|
279
|
+
bufferKey(projectId, commitId) {
|
|
280
|
+
return `${projectId}\0${commitId ?? NOCOMMIT}`;
|
|
281
|
+
}
|
|
282
|
+
bufferKeysForProject(projectId) {
|
|
283
|
+
const prefix = `${projectId}\0`;
|
|
284
|
+
return [...this.buffers.keys()].filter((bk) => bk.startsWith(prefix));
|
|
285
|
+
}
|
|
286
|
+
commitFile(projectId, commitId) {
|
|
287
|
+
const sanitized = sanitizeProjectId(projectId);
|
|
288
|
+
const suffix = commitId != null ? `_commit_${commitId}` : `_${NOCOMMIT}`;
|
|
289
|
+
return join(this.dir, `${sanitized}${suffix}.jsonl`);
|
|
290
|
+
}
|
|
291
|
+
projectFiles(projectId) {
|
|
292
|
+
const prefix = sanitizeProjectId(projectId);
|
|
293
|
+
return readdirSync(this.dir).filter((f) => f.startsWith(prefix) && f.endsWith(".jsonl"));
|
|
294
|
+
}
|
|
295
|
+
parseFilename(filename) {
|
|
296
|
+
if (!filename.endsWith(".jsonl")) return null;
|
|
297
|
+
const base = filename.slice(0, -6);
|
|
298
|
+
const commitMatch = base.match(/^(.+)_commit_(\d+)$/);
|
|
299
|
+
if (commitMatch) return {
|
|
300
|
+
projectSanitized: commitMatch[1],
|
|
301
|
+
commitId: Number(commitMatch[2])
|
|
302
|
+
};
|
|
303
|
+
const nocommitMatch = base.match(/^(.+)_nocommit$/);
|
|
304
|
+
if (nocommitMatch) return { projectSanitized: nocommitMatch[1] };
|
|
305
|
+
return { projectSanitized: base };
|
|
252
306
|
}
|
|
253
307
|
jsonlFiles() {
|
|
254
308
|
return readdirSync(this.dir).filter((f) => f.endsWith(".jsonl"));
|
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.
|
|
3
|
+
"version": "1.0.0-dev.9",
|
|
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",
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@biomejs/biome": "^1.9.4",
|
|
58
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
59
|
+
"@semantic-release/git": "^10.0.1",
|
|
58
60
|
"@types/node": "^22.14.1",
|
|
59
61
|
"@types/ws": "^8.18.0",
|
|
60
62
|
"@vitest/coverage-v8": "^4.1.2",
|