@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # why-did-you-render-mcp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@0x1f320.sh/why-did-you-render-mcp.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@0x1f320.sh/why-did-you-render-mcp)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/0x1f320/why-did-you-render-mcp/ci.yml?style=flat&colorA=000000&colorB=000000)](https://github.com/0x1f320/why-did-you-render-mcp/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@0x1f320.sh/why-did-you-render-mcp?style=flat&colorA=000000&colorB=000000)](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
- notifier,
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
- Add the server to your MCP client configuration. For example, in Claude Desktop's `claude_desktop_config.json`:
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
- Or if you installed it globally:
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": "why-did-you-render-mcp"
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.
@@ -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
  }
@@ -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
  }
@@ -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
- let buf = this.buffers.get(projectId);
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(projectId, buf);
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(projectId);
154
+ const existing = this.timers.get(bk);
148
155
  if (existing) clearTimeout(existing);
149
- this.timers.set(projectId, setTimeout(() => {
150
- this.flushAsync(projectId).catch((err) => console.error(`[wdyr-mcp] flush error for ${projectId}:`, err));
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
- if (projectId) this.flushProject(projectId);
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.flushProject(projectId);
160
- else for (const id of this.buffers.keys()) this.flushProject(id);
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
- flushProject(projectId) {
163
- const buf = this.buffers.get(projectId);
169
+ flushBuffer(bk) {
170
+ const buf = this.buffers.get(bk);
164
171
  if (!buf || buf.length === 0) return;
165
- let dict = this.dicts.get(projectId);
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(projectId, dict);
177
+ this.dicts.set(bk, dict);
169
178
  }
170
179
  const dehydrated = buf.map((r) => dehydrate(r, dict));
171
- const file = this.projectFile(projectId);
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(projectId);
186
+ const timer = this.timers.get(bk);
178
187
  if (timer) {
179
188
  clearTimeout(timer);
180
- this.timers.delete(projectId);
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 readJsonl(this.projectFile(projectId)).map(toResult);
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.buffers.delete(projectId);
202
- this.dicts.delete(projectId);
203
- const timer = this.timers.get(projectId);
204
- if (timer) {
205
- clearTimeout(timer);
206
- this.timers.delete(projectId);
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 file = this.projectFile(projectId);
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 [id, timer] of this.timers) clearTimeout(timer);
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 parsed = JSON.parse(line);
226
- if ("@@dict" in parsed) continue;
227
- projects.add(parsed.projectId);
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
- const renders = this.getAllRenders(projectId);
235
- return [...new Set(renders.map((r) => r.commitId).filter((id) => id != null))];
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
- return this.getAllRenders(projectId).filter((r) => r.commitId === commitId);
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
- projectFile(projectId) {
251
- return join(this.dir, `${sanitizeProjectId(projectId)}.jsonl`);
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.7",
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",