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

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):
63
81
 
64
82
  ```json
65
83
  {
66
84
  "mcpServers": {
67
85
  "why-did-you-render": {
68
86
  "command": "npx",
69
- "args": ["@0x1f320.sh/why-did-you-render-mcp"]
87
+ "args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
70
88
  }
71
89
  }
72
90
  }
73
91
  ```
74
92
 
75
- Or if you installed it globally:
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:
76
103
 
77
104
  ```json
78
105
  {
79
106
  "mcpServers": {
80
107
  "why-did-you-render": {
81
- "command": "why-did-you-render-mcp"
108
+ "command": "npx",
109
+ "args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
82
110
  }
83
111
  }
84
112
  }
85
113
  ```
86
114
 
115
+ </details>
116
+
117
+ <details>
118
+ <summary>Windsurf</summary>
119
+
120
+ Add to `~/.codeium/windsurf/mcp_config.json`:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "why-did-you-render": {
126
+ "command": "npx",
127
+ "args": ["-y", "@0x1f320.sh/why-did-you-render-mcp"]
128
+ }
129
+ }
130
+ }
131
+ ```
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.
@@ -94,11 +164,20 @@ Once both the MCP server and your React dev server are running, interact with yo
94
164
  | --- | --- |
95
165
  | `get_unnecessary_renders` | Returns all captured unnecessary re-renders. Optionally filter by `component` name. |
96
166
  | `get_render_summary` | Returns a summary of re-renders grouped by component with counts. |
167
+ | `get_commits` | Lists React commit IDs that have recorded render data. Use these IDs with `get_renders_by_commit`. |
168
+ | `get_renders_by_commit` | Returns all unnecessary re-renders for a specific React commit ID. |
97
169
  | `get_projects` | Lists all active projects (identified by their origin URL). |
98
170
  | `clear_renders` | Clears all stored render data. Optionally scope to a specific project. |
99
171
 
100
172
  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
173
 
174
+ ### Commit-level grouping
175
+
176
+ 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:
177
+
178
+ 1. Call `get_commits` to list available commit IDs
179
+ 2. Call `get_renders_by_commit` with a specific ID to see all renders in that commit
180
+
102
181
  ## Architecture
103
182
 
104
183
  ```
@@ -117,6 +196,7 @@ Browser (project-b) ──┤
117
196
  - **Multiple MCP instances** can run simultaneously. Only the first instance starts the WebSocket server; others gracefully skip. All instances share the same JSONL data directory.
118
197
  - **Multi-project support** — Each project is identified by `location.origin`. Render data is stored in per-project JSONL files.
119
198
  - **No daemon required** — Each MCP instance is independent. The WebSocket server is opportunistically claimed by whichever instance starts first.
199
+ - **Value dictionary deduplication** — Render reports often repeat the same `prevValue`/`nextValue` objects across thousands of entries. Each JSONL file stores a content-addressed dictionary on its first line, mapping xxhash-wasm hashes to unique values. Render lines reference them via `@@ref:<hash>` sentinels instead of inlining the full object, dramatically reducing file size. Reads hydrate refs transparently.
120
200
 
121
201
  ## Configuration
122
202
 
@@ -1,14 +1,116 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/client/utils/describe-value.ts
3
+ const MAX_DEPTH = 8;
4
+ const REACT_ELEMENT_SYMBOL = Symbol.for("react.element");
5
+ const REACT_TRANSITIONAL_ELEMENT_SYMBOL = Symbol.for("react.transitional.element");
6
+ const REACT_MEMO_TYPE = Symbol.for("react.memo");
7
+ const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
8
+ function isReactElement(value) {
9
+ if (typeof value !== "object" || value === null) return false;
10
+ const v = value;
11
+ return v.$$typeof === REACT_ELEMENT_SYMBOL || v.$$typeof === REACT_TRANSITIONAL_ELEMENT_SYMBOL || v.$$typeof === 60103;
12
+ }
13
+ function resolveComponentInfo(type) {
14
+ let memo = false;
15
+ let forwardRef = false;
16
+ let current = type;
17
+ for (let i = 0; i < 5; i++) {
18
+ if (typeof current !== "object" || current === null) break;
19
+ const wrapper = current;
20
+ if (wrapper.$$typeof === REACT_MEMO_TYPE) {
21
+ memo = true;
22
+ current = wrapper.type;
23
+ } else if (wrapper.$$typeof === REACT_FORWARD_REF_TYPE) {
24
+ forwardRef = true;
25
+ current = wrapper.render;
26
+ } else break;
27
+ }
28
+ let name = "Unknown";
29
+ if (typeof current === "string") name = current;
30
+ else if (typeof current === "function") name = current.displayName || current.name || "Anonymous";
31
+ return {
32
+ name,
33
+ memo,
34
+ forwardRef
35
+ };
36
+ }
37
+ function serializeReactElement(el, seen, depth) {
38
+ const component = resolveComponentInfo(el.type);
39
+ const props = {};
40
+ if (el.props && typeof el.props === "object") for (const key of Object.keys(el.props)) {
41
+ if (key === "children") continue;
42
+ props[key] = serialize(el.props[key], seen, depth + 1);
43
+ }
44
+ return {
45
+ type: "react-node",
46
+ component,
47
+ props
48
+ };
49
+ }
50
+ function serialize(value, seen, depth) {
51
+ if (value === null) return null;
52
+ if (value === void 0) return null;
53
+ if (typeof value === "function") return {
54
+ type: "function",
55
+ name: value.name || "anonymous"
56
+ };
57
+ if (typeof value === "boolean") return value;
58
+ if (typeof value === "number") {
59
+ if (Number.isNaN(value)) return "NaN";
60
+ if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
61
+ if (Object.is(value, -0)) return "-0";
62
+ return value;
63
+ }
64
+ if (typeof value === "string") return value;
65
+ if (typeof value === "bigint") return value.toString();
66
+ if (typeof value === "symbol") return value.toString();
67
+ if (seen.has(value)) return "[Circular]";
68
+ if (depth >= MAX_DEPTH) return "[MaxDepth]";
69
+ seen.add(value);
70
+ if (isReactElement(value)) return serializeReactElement(value, seen, depth);
71
+ if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
72
+ const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
73
+ if (ctorName && ctorName !== "Object") {
74
+ if (value instanceof Date) return value.toISOString();
75
+ if (value instanceof RegExp) return String(value);
76
+ if (value instanceof Map) {
77
+ const entries = {};
78
+ for (const [k, v] of value.entries()) entries[String(k)] = serialize(v, seen, depth + 1);
79
+ return {
80
+ type: "Map",
81
+ entries
82
+ };
83
+ }
84
+ if (value instanceof Set) return {
85
+ type: "Set",
86
+ values: [...value].map((v) => serialize(v, seen, depth + 1))
87
+ };
88
+ if (value instanceof Promise) return "Promise";
89
+ if (value instanceof Error) return {
90
+ type: "Error",
91
+ name: value.name,
92
+ message: value.message
93
+ };
94
+ if (typeof Node !== "undefined" && value instanceof Node && value instanceof Element) {
95
+ const attrs = {};
96
+ for (const attr of value.attributes) attrs[attr.name] = attr.value;
97
+ return {
98
+ type: "dom",
99
+ tagName: value.tagName.toLowerCase(),
100
+ attrs
101
+ };
102
+ }
103
+ return {
104
+ type: "class",
105
+ name: ctorName
106
+ };
107
+ }
108
+ const result = {};
109
+ for (const key of Object.keys(value)) result[key] = serialize(value[key], seen, depth + 1);
110
+ return result;
111
+ }
3
112
  function describeValue(value) {
4
- if (value === null) return "null";
5
- if (value === void 0) return "undefined";
6
- if (typeof value === "function") return `function ${value.name || "anonymous"}`;
7
- if (typeof value !== "object") return String(value);
8
- if (Array.isArray(value)) return `Array(${value.length})`;
9
- const name = Object.getPrototypeOf(value)?.constructor?.name;
10
- if (name && name !== "Object") return name;
11
- return "Object";
113
+ return serialize(value, /* @__PURE__ */ new WeakSet(), 0);
12
114
  }
13
115
  //#endregion
14
116
  //#region src/client/utils/sanitize-differences.ts
@@ -33,40 +135,95 @@ function sanitizeReason(reason) {
33
135
  //#endregion
34
136
  //#region src/client/index.ts
35
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
+ }
143
+ function patchDevToolsHook(onCommit) {
144
+ if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
145
+ supportsFiber: true,
146
+ inject() {},
147
+ onCommitFiberRoot() {},
148
+ onCommitFiberUnmount() {}
149
+ };
150
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
151
+ const original = hook.onCommitFiberRoot.bind(hook);
152
+ hook.onCommitFiberRoot = (...args) => {
153
+ onCommit();
154
+ return original(...args);
155
+ };
156
+ }
36
157
  function buildOptions(opts) {
37
158
  const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
38
159
  const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
160
+ const MAX_QUEUE_SIZE = 1e3;
161
+ const BASE_DELAY = 1e3;
162
+ const MAX_DELAY = 3e4;
39
163
  let ws = null;
40
164
  let queue = [];
165
+ let commitId = 0;
166
+ let retryDelay = BASE_DELAY;
167
+ patchDevToolsHook(() => {
168
+ commitId++;
169
+ });
41
170
  function connect() {
42
171
  ws = new WebSocket(wsUrl);
43
172
  ws.addEventListener("open", () => {
173
+ log(`Connected to ${wsUrl}`);
174
+ retryDelay = BASE_DELAY;
44
175
  for (const msg of queue) ws?.send(JSON.stringify(msg));
45
176
  queue = [];
46
177
  });
47
178
  ws.addEventListener("close", () => {
48
179
  ws = null;
49
- setTimeout(connect, 1e3);
180
+ setTimeout(connect, retryDelay);
181
+ retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
50
182
  });
51
183
  ws.addEventListener("error", () => {
184
+ log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
52
185
  ws?.close();
53
186
  });
54
187
  }
55
188
  connect();
56
189
  function send(msg) {
57
190
  if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
58
- else queue.push(msg);
191
+ else {
192
+ if (queue.length >= MAX_QUEUE_SIZE) queue.shift();
193
+ queue.push(msg);
194
+ }
59
195
  }
60
- return { notifier(info) {
196
+ let pendingBatch = null;
197
+ let flushScheduled = false;
198
+ function flushBatch() {
199
+ flushScheduled = false;
200
+ if (!pendingBatch || pendingBatch.reports.length === 0) return;
61
201
  send({
62
- type: "render",
202
+ type: "render-batch",
63
203
  projectId,
64
- payload: {
65
- displayName: info.displayName,
66
- reason: sanitizeReason(info.reason),
67
- hookName: info.hookName
68
- }
204
+ commitId: pendingBatch.commitId,
205
+ payload: pendingBatch.reports
69
206
  });
207
+ pendingBatch = null;
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]
221
+ };
222
+ }
223
+ if (!flushScheduled) {
224
+ flushScheduled = true;
225
+ queueMicrotask(flushBatch);
226
+ }
70
227
  } };
71
228
  }
72
229
  //#endregion
@@ -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;
@@ -1,13 +1,115 @@
1
1
  //#region src/client/utils/describe-value.ts
2
+ const MAX_DEPTH = 8;
3
+ const REACT_ELEMENT_SYMBOL = Symbol.for("react.element");
4
+ const REACT_TRANSITIONAL_ELEMENT_SYMBOL = Symbol.for("react.transitional.element");
5
+ const REACT_MEMO_TYPE = Symbol.for("react.memo");
6
+ const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
7
+ function isReactElement(value) {
8
+ if (typeof value !== "object" || value === null) return false;
9
+ const v = value;
10
+ return v.$$typeof === REACT_ELEMENT_SYMBOL || v.$$typeof === REACT_TRANSITIONAL_ELEMENT_SYMBOL || v.$$typeof === 60103;
11
+ }
12
+ function resolveComponentInfo(type) {
13
+ let memo = false;
14
+ let forwardRef = false;
15
+ let current = type;
16
+ for (let i = 0; i < 5; i++) {
17
+ if (typeof current !== "object" || current === null) break;
18
+ const wrapper = current;
19
+ if (wrapper.$$typeof === REACT_MEMO_TYPE) {
20
+ memo = true;
21
+ current = wrapper.type;
22
+ } else if (wrapper.$$typeof === REACT_FORWARD_REF_TYPE) {
23
+ forwardRef = true;
24
+ current = wrapper.render;
25
+ } else break;
26
+ }
27
+ let name = "Unknown";
28
+ if (typeof current === "string") name = current;
29
+ else if (typeof current === "function") name = current.displayName || current.name || "Anonymous";
30
+ return {
31
+ name,
32
+ memo,
33
+ forwardRef
34
+ };
35
+ }
36
+ function serializeReactElement(el, seen, depth) {
37
+ const component = resolveComponentInfo(el.type);
38
+ const props = {};
39
+ if (el.props && typeof el.props === "object") for (const key of Object.keys(el.props)) {
40
+ if (key === "children") continue;
41
+ props[key] = serialize(el.props[key], seen, depth + 1);
42
+ }
43
+ return {
44
+ type: "react-node",
45
+ component,
46
+ props
47
+ };
48
+ }
49
+ function serialize(value, seen, depth) {
50
+ if (value === null) return null;
51
+ if (value === void 0) return null;
52
+ if (typeof value === "function") return {
53
+ type: "function",
54
+ name: value.name || "anonymous"
55
+ };
56
+ if (typeof value === "boolean") return value;
57
+ if (typeof value === "number") {
58
+ if (Number.isNaN(value)) return "NaN";
59
+ if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
60
+ if (Object.is(value, -0)) return "-0";
61
+ return value;
62
+ }
63
+ if (typeof value === "string") return value;
64
+ if (typeof value === "bigint") return value.toString();
65
+ if (typeof value === "symbol") return value.toString();
66
+ if (seen.has(value)) return "[Circular]";
67
+ if (depth >= MAX_DEPTH) return "[MaxDepth]";
68
+ seen.add(value);
69
+ if (isReactElement(value)) return serializeReactElement(value, seen, depth);
70
+ if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
71
+ const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
72
+ if (ctorName && ctorName !== "Object") {
73
+ if (value instanceof Date) return value.toISOString();
74
+ if (value instanceof RegExp) return String(value);
75
+ if (value instanceof Map) {
76
+ const entries = {};
77
+ for (const [k, v] of value.entries()) entries[String(k)] = serialize(v, seen, depth + 1);
78
+ return {
79
+ type: "Map",
80
+ entries
81
+ };
82
+ }
83
+ if (value instanceof Set) return {
84
+ type: "Set",
85
+ values: [...value].map((v) => serialize(v, seen, depth + 1))
86
+ };
87
+ if (value instanceof Promise) return "Promise";
88
+ if (value instanceof Error) return {
89
+ type: "Error",
90
+ name: value.name,
91
+ message: value.message
92
+ };
93
+ if (typeof Node !== "undefined" && value instanceof Node && value instanceof Element) {
94
+ const attrs = {};
95
+ for (const attr of value.attributes) attrs[attr.name] = attr.value;
96
+ return {
97
+ type: "dom",
98
+ tagName: value.tagName.toLowerCase(),
99
+ attrs
100
+ };
101
+ }
102
+ return {
103
+ type: "class",
104
+ name: ctorName
105
+ };
106
+ }
107
+ const result = {};
108
+ for (const key of Object.keys(value)) result[key] = serialize(value[key], seen, depth + 1);
109
+ return result;
110
+ }
2
111
  function describeValue(value) {
3
- if (value === null) return "null";
4
- if (value === void 0) return "undefined";
5
- if (typeof value === "function") return `function ${value.name || "anonymous"}`;
6
- if (typeof value !== "object") return String(value);
7
- if (Array.isArray(value)) return `Array(${value.length})`;
8
- const name = Object.getPrototypeOf(value)?.constructor?.name;
9
- if (name && name !== "Object") return name;
10
- return "Object";
112
+ return serialize(value, /* @__PURE__ */ new WeakSet(), 0);
11
113
  }
12
114
  //#endregion
13
115
  //#region src/client/utils/sanitize-differences.ts
@@ -32,40 +134,95 @@ function sanitizeReason(reason) {
32
134
  //#endregion
33
135
  //#region src/client/index.ts
34
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
+ }
142
+ function patchDevToolsHook(onCommit) {
143
+ if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
144
+ supportsFiber: true,
145
+ inject() {},
146
+ onCommitFiberRoot() {},
147
+ onCommitFiberUnmount() {}
148
+ };
149
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
150
+ const original = hook.onCommitFiberRoot.bind(hook);
151
+ hook.onCommitFiberRoot = (...args) => {
152
+ onCommit();
153
+ return original(...args);
154
+ };
155
+ }
35
156
  function buildOptions(opts) {
36
157
  const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
37
158
  const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
159
+ const MAX_QUEUE_SIZE = 1e3;
160
+ const BASE_DELAY = 1e3;
161
+ const MAX_DELAY = 3e4;
38
162
  let ws = null;
39
163
  let queue = [];
164
+ let commitId = 0;
165
+ let retryDelay = BASE_DELAY;
166
+ patchDevToolsHook(() => {
167
+ commitId++;
168
+ });
40
169
  function connect() {
41
170
  ws = new WebSocket(wsUrl);
42
171
  ws.addEventListener("open", () => {
172
+ log(`Connected to ${wsUrl}`);
173
+ retryDelay = BASE_DELAY;
43
174
  for (const msg of queue) ws?.send(JSON.stringify(msg));
44
175
  queue = [];
45
176
  });
46
177
  ws.addEventListener("close", () => {
47
178
  ws = null;
48
- setTimeout(connect, 1e3);
179
+ setTimeout(connect, retryDelay);
180
+ retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
49
181
  });
50
182
  ws.addEventListener("error", () => {
183
+ log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
51
184
  ws?.close();
52
185
  });
53
186
  }
54
187
  connect();
55
188
  function send(msg) {
56
189
  if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
57
- else queue.push(msg);
190
+ else {
191
+ if (queue.length >= MAX_QUEUE_SIZE) queue.shift();
192
+ queue.push(msg);
193
+ }
58
194
  }
59
- return { notifier(info) {
195
+ let pendingBatch = null;
196
+ let flushScheduled = false;
197
+ function flushBatch() {
198
+ flushScheduled = false;
199
+ if (!pendingBatch || pendingBatch.reports.length === 0) return;
60
200
  send({
61
- type: "render",
201
+ type: "render-batch",
62
202
  projectId,
63
- payload: {
64
- displayName: info.displayName,
65
- reason: sanitizeReason(info.reason),
66
- hookName: info.hookName
67
- }
203
+ commitId: pendingBatch.commitId,
204
+ payload: pendingBatch.reports
68
205
  });
206
+ pendingBatch = null;
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]
220
+ };
221
+ }
222
+ if (!flushScheduled) {
223
+ flushScheduled = true;
224
+ queueMicrotask(flushBatch);
225
+ }
69
226
  } };
70
227
  }
71
228
  //#endregion
@@ -2,14 +2,107 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
+ import xxhash from "xxhash-wasm";
8
9
  import { WebSocketServer } from "ws";
10
+ //#region src/server/store/utils/value-dict.ts
11
+ const DICT_KEY = "@@dict";
12
+ const REF_PREFIX = "@@ref:";
13
+ let h64ToString;
14
+ const ready = xxhash().then((api) => {
15
+ h64ToString = api.h64ToString;
16
+ });
17
+ function ensureReady() {
18
+ return ready;
19
+ }
20
+ function hashValue(value) {
21
+ return h64ToString(JSON.stringify(value));
22
+ }
23
+ function shouldDehydrate(value) {
24
+ return typeof value === "object" && value !== null;
25
+ }
26
+ function dehydrateDiffs(diffs, dict) {
27
+ if (!diffs) return false;
28
+ return diffs.map((d) => {
29
+ let { prevValue, nextValue } = d;
30
+ if (shouldDehydrate(prevValue)) {
31
+ const hash = hashValue(prevValue);
32
+ dict[hash] ??= prevValue;
33
+ prevValue = `${REF_PREFIX}${hash}`;
34
+ }
35
+ if (shouldDehydrate(nextValue)) {
36
+ const hash = hashValue(nextValue);
37
+ dict[hash] ??= nextValue;
38
+ nextValue = `${REF_PREFIX}${hash}`;
39
+ }
40
+ return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
41
+ ...d,
42
+ prevValue,
43
+ nextValue
44
+ };
45
+ });
46
+ }
47
+ function dehydrate(render, dict) {
48
+ const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
49
+ const newProps = dehydrateDiffs(propsDifferences, dict);
50
+ const newState = dehydrateDiffs(stateDifferences, dict);
51
+ const newHooks = dehydrateDiffs(hookDifferences, dict);
52
+ if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
53
+ return {
54
+ ...render,
55
+ reason: {
56
+ propsDifferences: newProps,
57
+ stateDifferences: newState,
58
+ hookDifferences: newHooks
59
+ }
60
+ };
61
+ }
62
+ function hydrateDiffs(diffs, dict) {
63
+ if (!diffs) return false;
64
+ return diffs.map((d) => {
65
+ let { prevValue, nextValue } = d;
66
+ if (typeof prevValue === "string" && prevValue.startsWith(REF_PREFIX)) prevValue = dict[prevValue.slice(6)] ?? prevValue;
67
+ if (typeof nextValue === "string" && nextValue.startsWith(REF_PREFIX)) nextValue = dict[nextValue.slice(6)] ?? nextValue;
68
+ return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
69
+ ...d,
70
+ prevValue,
71
+ nextValue
72
+ };
73
+ });
74
+ }
75
+ function hydrate(render, dict) {
76
+ const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
77
+ const newProps = hydrateDiffs(propsDifferences, dict);
78
+ const newState = hydrateDiffs(stateDifferences, dict);
79
+ const newHooks = hydrateDiffs(hookDifferences, dict);
80
+ if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
81
+ return {
82
+ ...render,
83
+ reason: {
84
+ propsDifferences: newProps,
85
+ stateDifferences: newState,
86
+ hookDifferences: newHooks
87
+ }
88
+ };
89
+ }
90
+ //#endregion
9
91
  //#region src/server/store/utils/read-jsonl.ts
10
92
  function readJsonl(file) {
11
93
  if (!existsSync(file)) return [];
12
- return readFileSync(file, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
94
+ const lines = readFileSync(file, "utf-8").split("\n").filter(Boolean);
95
+ if (lines.length === 0) return [];
96
+ let dict;
97
+ let startIdx = 0;
98
+ const first = JSON.parse(lines[0]);
99
+ if ("@@dict" in first) {
100
+ dict = first[DICT_KEY];
101
+ startIdx = 1;
102
+ }
103
+ const renders = lines.slice(startIdx).map((l) => JSON.parse(l));
104
+ if (!dict) return renders;
105
+ return renders.map((r) => hydrate(r, dict));
13
106
  }
14
107
  //#endregion
15
108
  //#region src/server/store/utils/sanitize-project-id.ts
@@ -23,54 +116,90 @@ function toResult(stored) {
23
116
  project: stored.projectId,
24
117
  displayName: stored.displayName,
25
118
  reason: stored.reason,
26
- ...stored.hookName != null && { hookName: stored.hookName }
119
+ ...stored.hookName != null && { hookName: stored.hookName },
120
+ ...stored.commitId != null && { commitId: stored.commitId }
27
121
  };
28
122
  }
29
123
  //#endregion
30
124
  //#region src/server/store/render-store.ts
31
125
  const FLUSH_DELAY_MS = 200;
126
+ const NOCOMMIT = "nocommit";
32
127
  var RenderStore = class {
33
128
  dir;
34
129
  buffers = /* @__PURE__ */ new Map();
35
130
  timers = /* @__PURE__ */ new Map();
131
+ dicts = /* @__PURE__ */ new Map();
132
+ bufferMeta = /* @__PURE__ */ new Map();
36
133
  constructor(dir) {
37
134
  this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
38
135
  mkdirSync(this.dir, { recursive: true });
39
136
  }
40
- addRender(report, projectId) {
137
+ addRender(report, projectId, commitId) {
41
138
  const stored = {
42
139
  ...report,
43
- projectId
140
+ projectId,
141
+ ...commitId != null && { commitId }
44
142
  };
45
- let buf = this.buffers.get(projectId);
143
+ const bk = this.bufferKey(projectId, commitId);
144
+ let buf = this.buffers.get(bk);
46
145
  if (!buf) {
47
146
  buf = [];
48
- this.buffers.set(projectId, buf);
147
+ this.buffers.set(bk, buf);
148
+ this.bufferMeta.set(bk, {
149
+ projectId,
150
+ commitId
151
+ });
49
152
  }
50
153
  buf.push(stored);
51
- const existing = this.timers.get(projectId);
154
+ const existing = this.timers.get(bk);
52
155
  if (existing) clearTimeout(existing);
53
- this.timers.set(projectId, setTimeout(() => this.flush(projectId), FLUSH_DELAY_MS));
156
+ this.timers.set(bk, setTimeout(() => {
157
+ this.flushAsync(projectId, commitId).catch((err) => console.error(`[wdyr-mcp] flush error for ${bk}:`, err));
158
+ }, FLUSH_DELAY_MS));
54
159
  }
55
- flush(projectId) {
56
- if (projectId) this.flushProject(projectId);
57
- else for (const id of this.buffers.keys()) this.flushProject(id);
160
+ async flushAsync(projectId, commitId) {
161
+ await ensureReady();
162
+ this.flush(projectId, commitId);
58
163
  }
59
- flushProject(projectId) {
60
- const buf = this.buffers.get(projectId);
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);
168
+ }
169
+ flushBuffer(bk) {
170
+ const buf = this.buffers.get(bk);
61
171
  if (!buf || buf.length === 0) return;
62
- const lines = buf.map((s) => JSON.stringify(s)).join("\n");
63
- appendFileSync(this.projectFile(projectId), `${lines}\n`);
172
+ const meta = this.bufferMeta.get(bk);
173
+ if (!meta) return;
174
+ let dict = this.dicts.get(bk);
175
+ if (!dict) {
176
+ dict = {};
177
+ this.dicts.set(bk, dict);
178
+ }
179
+ const dehydrated = buf.map((r) => dehydrate(r, dict));
180
+ const file = this.commitFile(meta.projectId, meta.commitId);
181
+ const existingLines = this.readDataLines(file);
182
+ const newLines = dehydrated.map((r) => JSON.stringify(r));
183
+ const allDataLines = [...existingLines, ...newLines];
184
+ writeFileSync(file, `${(Object.keys(dict).length > 0 ? [JSON.stringify({ [DICT_KEY]: dict }), ...allDataLines] : allDataLines).join("\n")}\n`);
64
185
  buf.length = 0;
65
- const timer = this.timers.get(projectId);
186
+ const timer = this.timers.get(bk);
66
187
  if (timer) {
67
188
  clearTimeout(timer);
68
- this.timers.delete(projectId);
189
+ this.timers.delete(bk);
69
190
  }
70
191
  }
192
+ readDataLines(file) {
193
+ if (!existsSync(file)) return [];
194
+ return readFileSync(file, "utf-8").split("\n").filter((line) => {
195
+ if (!line) return false;
196
+ if (line.startsWith(`{"@@dict"`)) return false;
197
+ return true;
198
+ });
199
+ }
71
200
  getAllRenders(projectId) {
72
201
  this.flush(projectId);
73
- 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));
74
203
  return this.jsonlFiles().flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
75
204
  }
76
205
  getRendersByComponent(componentName, projectId) {
@@ -78,32 +207,65 @@ var RenderStore = class {
78
207
  }
79
208
  clearRenders(projectId) {
80
209
  if (projectId) {
81
- this.buffers.delete(projectId);
82
- const timer = this.timers.get(projectId);
83
- if (timer) {
84
- clearTimeout(timer);
85
- 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
+ }
86
219
  }
87
- const file = this.projectFile(projectId);
88
- if (existsSync(file)) unlinkSync(file);
220
+ for (const f of this.projectFiles(projectId)) unlinkSync(join(this.dir, f));
89
221
  } else {
90
- for (const [id, timer] of this.timers) clearTimeout(timer);
222
+ for (const [, timer] of this.timers) clearTimeout(timer);
91
223
  this.buffers.clear();
92
224
  this.timers.clear();
225
+ this.dicts.clear();
226
+ this.bufferMeta.clear();
93
227
  for (const f of this.jsonlFiles()) unlinkSync(join(this.dir, f));
94
228
  }
95
229
  }
96
230
  getProjects() {
97
231
  this.flush();
98
232
  const projects = /* @__PURE__ */ new Set();
233
+ const seen = /* @__PURE__ */ new Set();
99
234
  for (const f of this.jsonlFiles()) {
100
- const firstLine = readFileSync(join(this.dir, f), "utf-8").split("\n")[0];
101
- if (!firstLine) continue;
102
- const stored = JSON.parse(firstLine);
103
- projects.add(stored.projectId);
235
+ const parsed = this.parseFilename(f);
236
+ if (!parsed) continue;
237
+ if (seen.has(parsed.projectSanitized)) continue;
238
+ seen.add(parsed.projectSanitized);
239
+ const lines = readFileSync(join(this.dir, f), "utf-8").split("\n");
240
+ for (const line of lines) {
241
+ if (!line) continue;
242
+ const obj = JSON.parse(line);
243
+ if ("@@dict" in obj) continue;
244
+ projects.add(obj.projectId);
245
+ break;
246
+ }
104
247
  }
105
248
  return [...projects];
106
249
  }
250
+ getCommitIds(projectId) {
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);
259
+ }
260
+ getRendersByCommit(commitId, projectId) {
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));
268
+ }
107
269
  getSummary(projectId) {
108
270
  const renders = this.getAllRenders(projectId);
109
271
  const summary = {};
@@ -114,8 +276,33 @@ var RenderStore = class {
114
276
  }
115
277
  return summary;
116
278
  }
117
- projectFile(projectId) {
118
- 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 };
119
306
  }
120
307
  jsonlFiles() {
121
308
  return readdirSync(this.dir).filter((f) => f.endsWith(".jsonl"));
@@ -156,7 +343,7 @@ function textResult(text) {
156
343
  }
157
344
  //#endregion
158
345
  //#region src/server/tools/clear-renders.ts
159
- function register$3(server) {
346
+ function register$5(server) {
160
347
  server.registerTool("clear_renders", {
161
348
  title: "Clear Renders",
162
349
  description: "Clears collected render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
@@ -169,8 +356,23 @@ function register$3(server) {
169
356
  });
170
357
  }
171
358
  //#endregion
359
+ //#region src/server/tools/get-commits.ts
360
+ function register$4(server) {
361
+ server.registerTool("get_commits", {
362
+ title: "Get Commits",
363
+ 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.",
364
+ inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
365
+ }, async ({ project }) => {
366
+ const resolved = resolveProject(project);
367
+ if (resolved.error) return textResult(resolved.error);
368
+ const commitIds = store.getCommitIds(resolved.projectId);
369
+ if (commitIds.length === 0) return textResult("No commits recorded yet. Make sure the browser is connected and triggering re-renders.");
370
+ return textResult(JSON.stringify(commitIds));
371
+ });
372
+ }
373
+ //#endregion
172
374
  //#region src/server/tools/get-projects.ts
173
- function register$2(server) {
375
+ function register$3(server) {
174
376
  server.registerTool("get_projects", {
175
377
  title: "Get Projects",
176
378
  description: "Returns a list of project identifiers (browser origin URLs) that have recorded render data.",
@@ -183,7 +385,7 @@ function register$2(server) {
183
385
  }
184
386
  //#endregion
185
387
  //#region src/server/tools/get-render-summary.ts
186
- function register$1(server) {
388
+ function register$2(server) {
187
389
  server.registerTool("get_render_summary", {
188
390
  title: "Get Render Summary",
189
391
  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 +404,26 @@ function register$1(server) {
202
404
  });
203
405
  }
204
406
  //#endregion
407
+ //#region src/server/tools/get-renders-by-commit.ts
408
+ function register$1(server) {
409
+ server.registerTool("get_renders_by_commit", {
410
+ title: "Get Renders by Commit",
411
+ description: "Returns all unnecessary re-renders for a specific React commit ID. Use get_commits first to discover available commit IDs.",
412
+ inputSchema: {
413
+ commitId: z.number().describe("The React commit ID to filter by."),
414
+ component: z.string().optional().describe("Filter by component name. Omit to get all renders."),
415
+ project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.")
416
+ }
417
+ }, async ({ commitId, component, project }) => {
418
+ const resolved = resolveProject(project);
419
+ if (resolved.error) return textResult(resolved.error);
420
+ let renders = store.getRendersByCommit(commitId, resolved.projectId);
421
+ if (component) renders = renders.filter((r) => r.displayName === component);
422
+ if (renders.length === 0) return textResult(component ? `No renders recorded for component "${component}" in commit ${commitId}.` : `No renders recorded for commit ${commitId}.`);
423
+ return textResult(JSON.stringify(renders, null, 2));
424
+ });
425
+ }
426
+ //#endregion
205
427
  //#region src/server/tools/get-unnecessary-renders.ts
206
428
  function register(server) {
207
429
  server.registerTool("get_unnecessary_renders", {
@@ -223,9 +445,11 @@ function register(server) {
223
445
  //#region src/server/tools/index.ts
224
446
  function registerTools(server) {
225
447
  register(server);
226
- register$1(server);
227
448
  register$2(server);
449
+ register$4(server);
450
+ register$1(server);
228
451
  register$3(server);
452
+ register$5(server);
229
453
  }
230
454
  //#endregion
231
455
  //#region src/server/liveness.ts
@@ -300,11 +524,10 @@ function createWsServer(port) {
300
524
  ws.on("message", (raw) => {
301
525
  try {
302
526
  const msg = JSON.parse(String(raw));
303
- if (msg.type === "render") {
304
- const projectId = msg.projectId ?? "default";
305
- heartbeat.setProjectId(ws, projectId);
306
- store.addRender(msg.payload, projectId);
307
- }
527
+ const projectId = msg.projectId ?? "default";
528
+ heartbeat.setProjectId(ws, projectId);
529
+ if (msg.type === "render") store.addRender(msg.payload, projectId, msg.commitId);
530
+ else if (msg.type === "render-batch") for (const report of msg.payload) store.addRender(report, projectId, msg.commitId);
308
531
  } catch {
309
532
  console.error("[wdyr-mcp] invalid message received");
310
533
  }
@@ -327,10 +550,22 @@ const server = new McpServer({
327
550
  });
328
551
  registerTools(server);
329
552
  async function main() {
330
- createWsServer(Number(process.env.WDYR_WS_PORT) || DEFAULT_WS_PORT);
553
+ const wss = createWsServer(Number(process.env.WDYR_WS_PORT) || DEFAULT_WS_PORT);
331
554
  const transport = new StdioServerTransport();
332
555
  await server.connect(transport);
333
556
  console.error("[wdyr-mcp] MCP server running on stdio");
557
+ let shuttingDown = false;
558
+ async function shutdown() {
559
+ if (shuttingDown) return;
560
+ shuttingDown = true;
561
+ console.error("[wdyr-mcp] Shutting down…");
562
+ wss?.close();
563
+ await server.close();
564
+ process.exit(0);
565
+ }
566
+ process.stdin.on("end", shutdown);
567
+ process.on("SIGTERM", shutdown);
568
+ process.on("SIGINT", shutdown);
334
569
  }
335
570
  main().catch((error) => {
336
571
  console.error("[wdyr-mcp] Fatal error:", error);
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.10",
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",
@@ -50,10 +50,13 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.12.1",
52
52
  "ws": "^8.18.0",
53
+ "xxhash-wasm": "^1.1.0",
53
54
  "zod": "^3.24.4"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@biomejs/biome": "^1.9.4",
58
+ "@semantic-release/changelog": "^6.0.3",
59
+ "@semantic-release/git": "^10.0.1",
57
60
  "@types/node": "^22.14.1",
58
61
  "@types/ws": "^8.18.0",
59
62
  "@vitest/coverage-v8": "^4.1.2",