@0x1f320.sh/why-did-you-render-mcp 1.0.0 → 1.0.1-dev.1
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 +22 -2
- package/dist/server/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,15 +162,35 @@ Once both the MCP server and your React dev server are running, interact with yo
|
|
|
162
162
|
|
|
163
163
|
| Tool | Description |
|
|
164
164
|
| --- | --- |
|
|
165
|
-
| `
|
|
165
|
+
| `get_renders` | Returns all captured unnecessary re-renders, including stack traces. Optionally filter by `component` name. |
|
|
166
166
|
| `get_render_summary` | Returns a summary of re-renders grouped by component with counts. |
|
|
167
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. |
|
|
168
|
+
| `get_renders_by_commit` | Returns all unnecessary re-renders for a specific React commit ID, including stack traces. |
|
|
169
|
+
| `get_tracked_components` | Lists components currently tracked by why-did-you-render. |
|
|
169
170
|
| `get_projects` | Lists all active projects (identified by their origin URL). |
|
|
170
171
|
| `clear_renders` | Clears all stored render data. Optionally scope to a specific project. |
|
|
171
172
|
|
|
172
173
|
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.
|
|
173
174
|
|
|
175
|
+
### Stack traces
|
|
176
|
+
|
|
177
|
+
Each render report includes a `stackFrames` array that traces the hook chain and component tree that triggered the re-render. The client captures a stack trace on every render update, parses it with `error-stack-parser`, filters out React/WDYR internals, and resolves bundled locations back to original source files via source maps.
|
|
178
|
+
|
|
179
|
+
Each frame has the following structure:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
{
|
|
183
|
+
type: "hook" | "component", // "hook" for names starting with `use`, otherwise "component"
|
|
184
|
+
name: string, // e.g. "useFilter", "Dashboard"
|
|
185
|
+
location: {
|
|
186
|
+
path: string, // source file path (source-mapped when available)
|
|
187
|
+
line: number, // line number in the source file
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Agents can use `stackFrames` to pinpoint the exact source location of each unnecessary re-render — navigating directly to the file and line that caused it, without requiring manual browser inspection.
|
|
193
|
+
|
|
174
194
|
### Commit-level grouping
|
|
175
195
|
|
|
176
196
|
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:
|
package/dist/server/index.js
CHANGED
|
@@ -9,4 +9,4 @@ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{Stdio
|
|
|
9
9
|
`)}`)})}function F(e){e.registerTool(`get_render_summary`,{title:`Get Render Summary`,description:`Returns a summary of re-renders grouped by component name with counts. Use groupBy: 'commit' to get per-commit breakdowns instead of a single aggregate. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`),groupBy:n.enum([`commit`]).optional().describe(`Group results by commit. When set to 'commit', returns per-commit render summaries instead of a single aggregate.`)}},async({project:e,groupBy:t})=>{let n=A(e);return n.error?j(n.error):t===`commit`?R(n.projectId):L(n.projectId)})}function I(e){let t=[];return e.props>0&&t.push(`props: ${e.props}`),e.state>0&&t.push(`state: ${e.state}`),e.hooks>0&&t.push(`hooks: ${e.hooks}`),t.length>0?` — ${t.join(`, `)}`:``}function L(e){let t=k.getSummary(e);if(Object.keys(t).length===0)return j(`No renders recorded yet.`);let n=[];for(let[e,r]of Object.entries(t)){n.push(`[${e}]`);for(let[e,{count:t,reasons:i}]of Object.entries(r))n.push(` ${e}: ${t} re-render(s)${I(i)}`)}return j(`Re-render summary:\n\n${n.join(`
|
|
10
10
|
`)}`)}function R(e){let t=k.getSummaryByCommit(e);if(Object.keys(t).length===0)return j(`No renders with commit IDs recorded yet.`);let n=[];for(let[e,r]of Object.entries(t)){n.push(`[${e}]`);let t=Object.keys(r).map(Number).sort((e,t)=>e-t);for(let e of t){let t=r[e],i=Object.values(t).reduce((e,t)=>e+t.count,0);n.push(` Commit #${e} (${i} re-render(s)):`);for(let[e,{count:r,reasons:i}]of Object.entries(t))n.push(` ${e}: ${r}${I(i)}`)}}return j(`Re-render summary (by commit):\n\n${n.join(`
|
|
11
11
|
`)}`)}function z(e){e.registerTool(`get_renders_by_commit`,{title:`Get Renders by Commit`,description:`Returns all re-renders for a specific React commit ID, including stack traces that show the hook chain and component tree that triggered each render. Use the stackFrames field to locate the exact source file and line. Use get_commits first to discover available commit IDs.`,inputSchema:{commitId:n.number().describe(`The React commit ID to filter by.`),component:n.string().optional().describe(`Filter by component name. Omit to get all renders.`),project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({commitId:e,component:t,project:n})=>{let r=A(n);if(r.error)return j(r.error);let i=k.getRendersByCommit(e,r.projectId);return t&&(i=i.filter(e=>e.displayName===t)),i.length===0?j(t?`No renders recorded for component "${t}" in commit ${e}.`:`No renders recorded for commit ${e}.`):j(JSON.stringify(i,null,2))})}function B(e){e.registerTool(`get_renders`,{title:`Get Renders`,description:`Returns all re-renders collected from the browser, including stack traces that show the hook chain and component tree that triggered each render. Use the stackFrames field to locate the exact source file and line. If multiple projects are active and no project is specified, the tool will ask you to disambiguate by asking the user for their dev server URL.`,inputSchema:{component:n.string().optional().describe(`Filter by component name. Omit to get all renders.`),project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({component:e,project:t})=>{let n=A(t);if(n.error)return j(n.error);let r=e?k.getRendersByComponent(e,n.projectId):k.getAllRenders(n.projectId);return r.length===0?j(e?`No renders recorded for "${e}".`:`No renders recorded yet. Make sure the browser is connected and triggering re-renders.`):j(JSON.stringify(r,null,2))})}function V(e){let t=[];if(e.include?.length){t.push(` include:`);for(let n of e.include)t.push(` - /${n}/`)}if(e.exclude?.length){t.push(` exclude:`);for(let n of e.exclude)t.push(` - /${n}/`)}if(e.trackAllPureComponents!=null&&t.push(` trackAllPureComponents: ${e.trackAllPureComponents}`),e.trackHooks!=null&&t.push(` trackHooks: ${e.trackHooks}`),e.trackExtraHooks?.length){t.push(` trackExtraHooks:`);for(let n of e.trackExtraHooks)t.push(` - ${n}`)}return e.logOnDifferentValues!=null&&t.push(` logOnDifferentValues: ${e.logOnDifferentValues}`),e.logOwnerReasons!=null&&t.push(` logOwnerReasons: ${e.logOwnerReasons}`),t}function H(e){e.registerTool(`get_tracked_components`,{title:`Get Tracked Components`,description:`Returns the why-did-you-render configuration for the connected project, including include/exclude filters and tracking options. Also shows components observed in render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({project:e})=>{let t=A(e);if(t.error)return j(t.error);let n=k.getWdyrConfig(t.projectId),r=k.getTrackedComponents(t.projectId),i=Object.keys(n).length>0,a=Object.keys(r).length>0;if(!i&&!a)return j(`No configuration or tracked components found. Make sure the browser is connected and triggering re-renders.`);let o=[],s=new Set([...Object.keys(n),...Object.keys(r)]);for(let e of s){o.push(`[${e}]`);let t=n[e];if(t){o.push(`Configuration:`);let e=V(t);e.length>0?o.push(...e):o.push(` (default options)`)}let i=r[e];if(i?.observed.length){o.push(`Observed in renders:`);for(let e of i.observed)o.push(` - ${e}`)}}return j(o.join(`
|
|
12
|
-
`))})}function U(e){B(e),F(e),N(e),z(e),P(e),H(e),M(e)}function
|
|
12
|
+
`))})}function U(e){B(e),F(e),N(e),z(e),P(e),H(e),M(e)}const W=3e3;function G(e,t){e.on(`connection`,n=>{console.error(`[wdyr-mcp] browser connected (http://localhost:${t})`),n.data.projectId=null,n.on(`render`,(e,t,r)=>{n.data.projectId=t,k.addRender(e,t,r)}),n.on(`render-batch`,(e,t,r)=>{n.data.projectId=t;for(let n of e)k.addRender(n,t,r)}),n.on(`register`,(e,t)=>{n.data.projectId=t,k.setTrackedComponents(e,t)}),n.on(`config`,(e,t)=>{n.data.projectId=t,k.setWdyrConfig(e,t)}),n.on(`disconnect`,()=>{console.error(`[wdyr-mcp] browser disconnected`);let t=n.data.projectId;t&&([...e.sockets.sockets.values()].some(e=>e.id!==n.id&&e.data.projectId===t)||(console.error(`[wdyr-mcp] last client for ${t} disconnected, clearing render data`),k.clearRenders(t)))})})}function K(e){let t=null,n=null,r=null,i=!1;function a(){i||(r=f.createServer(),n=new p(r,{cors:{origin:`*`},serveClient:!1,transports:[`websocket`],maxHttpBufferSize:5e7}),G(n,e),r.once(`error`,t=>{t.code===`EADDRINUSE`?(console.error(`[wdyr-mcp] Port ${e} in use, will retry every ${W/1e3}s`),n?.close(),n=null,r=null,o()):console.error(`[wdyr-mcp] server error:`,t)}),r.listen(e,`127.0.0.1`,()=>{console.error(`[wdyr-mcp] socket.io server listening on http://localhost:${e}`),s()}))}function o(){t||i||(t=setInterval(a,W))}function s(){t&&=(clearInterval(t),null)}return a(),{close(){i=!0,s(),n?.close()}}}const q=new e({name:`why-did-you-render`,version:`0.0.0`});U(q);async function J(){let e=K(Number(process.env.WDYR_WS_PORT)||4649),n=new t;await q.connect(n),console.error(`[wdyr-mcp] MCP server running on stdio`);let r=!1;async function i(){r||(r=!0,console.error(`[wdyr-mcp] Shutting down…`),e.close(),await q.close(),process.exit(0))}process.stdin.on(`end`,i),process.on(`SIGTERM`,i),process.on(`SIGINT`,i)}J().catch(e=>{console.error(`[wdyr-mcp] Fatal error:`,e),process.exit(1)});export{};
|
package/package.json
CHANGED