--- name: react-devtools-mcp description: Inspect and profile the React component tree of a running Storybook story through chrome-devtools-mcp (React DevTools over the Chrome DevTools Protocol, via react-devtools-cdt-mcp). Use when debugging why a @sanity/ui component re-renders, finding slow components, reading props and hooks at runtime, or investigating a perf regression in a story. Works from Cursor's MCP config and from the terminal (chrome-devtools CLI daemon), so it also works for cloud agents. --- # React DevTools for agents (chrome-devtools-mcp + react-devtools-cdt-mcp) [`react-devtools-cdt-mcp`](https://github.com/facebook/react/tree/main/packages/react-devtools-cdt-mcp) is **not** an MCP server. It is a browser library that installs a lightweight React DevTools hook in the page and registers twelve `react_*` tools that [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) discovers through its experimental third-party developer tools API (`--categoryExperimentalThirdParty=true`). `chrome-devtools-mcp` is the MCP server; it talks to Chrome over the Chrome DevTools Protocol. In this repo the Storybook dev server loads `react-devtools-cdt-mcp/register` in the preview iframe when `ENABLE_REACT_DEVTOOLS_MCP=true` (plugin in `apps/storybook/.storybook/main.ts`). Nothing changes for `pnpm dev` or the static build. ## Setup (three processes) ```bash # 1. Storybook with the React DevTools hook installed (port 6006, long-running) pnpm react-devtools-mcp:storybook # 2. Chrome with the remote debugging port open, showing one story # (url defaults to iframe.html?viewMode=story&id=primitives-button--default) pnpm react-devtools-mcp:chrome pnpm react-devtools-mcp:chrome "http://localhost:6006/iframe.html?viewMode=story&id=components-menubutton--default" pnpm react-devtools-mcp:chrome --headless # no display (CI, cloud VM without X) # 3. chrome-devtools-mcp attached to that Chrome — pick ONE of: # a) Cursor: .cursor/mcp.json defines the `chrome-devtools` server (the workspace-installed, # lockfile-pinned chrome-devtools-mcp under apps/storybook/node_modules) with # --categoryExperimentalThirdParty=true --browserUrl=http://127.0.0.1:9222 # b) Terminal / cloud agents (same tools, no MCP client needed): pnpm --filter sanity-ui-storybook exec chrome-devtools start \ --categoryExperimentalThirdParty=true --browserUrl=http://127.0.0.1:9222 ``` Stories **must** be opened through `iframe.html?viewMode=story&id=`. Tool discovery only looks at the top-level document; the manager UI at `/` embeds stories in an iframe and exposes nothing. Story ids are in `http://localhost:6006/index.json` (`primitives-button--default`, `components-menubutton--default`, ...). ## Workflow Every command below is shown for the CLI (`CD="pnpm --filter sanity-ui-storybook exec chrome-devtools"`). Through MCP the tool names are identical (`list_pages`, `list_3p_developer_tools`, `execute_3p_developer_tool`, `take_snapshot`, `click`, `navigate_page`); `params` is a JSON string. ```bash $CD list_pages # page ids; the story page is normally 1 $CD list_3p_developer_tools 1 # REQUIRED once per page load: discovers the react_* tools $CD execute_3p_developer_tool 1 react_get_component_tree --params '{"depth":40}' $CD execute_3p_developer_tool 1 react_find_components --params '{"name":"Button"}' $CD execute_3p_developer_tool 1 react_get_component_by_uid --params '{"uid":"r20","includeHooks":true}' $CD execute_3p_developer_tool 1 react_get_component_source --params '{"uid":"r20"}' $CD execute_3p_developer_tool 1 react_get_parent_stack --params '{"uid":"r20"}' # Profile an interaction $CD execute_3p_developer_tool 1 react_start_profiling --params '{"traceName":"open-menu"}' $CD take_snapshot 1 # find the element uid to interact with (e.g. 1_1) $CD click 1 1_1 $CD execute_3p_developer_tool 1 react_stop_profiling $CD execute_3p_developer_tool 1 react_get_trace_overview --params '{"traceName":"open-menu"}' $CD execute_3p_developer_tool 1 react_get_commit_report --params '{"traceName":"open-menu","commitIndex":1}' $CD navigate_page 1 --type url --url "http://localhost:6006/iframe.html?viewMode=story&id=" $CD stop # stop the daemon when done ``` Add `--output-format json` for machine-readable output: the result is `{"message": ""}`, i.e. parse `message` a second time. Tools: `react_get_component_tree`, `react_get_component_by_uid`, `react_get_component_by_dom_element` (`{"element":{"uid":"1_1"}}` from `take_snapshot`), `react_find_components`, `react_get_component_source`, `react_get_owner_stack_trace`, `react_get_parent_stack`, `react_get_owner_stack`, `react_start_profiling`, `react_stop_profiling`, `react_get_trace_overview`, `react_get_commit_report`. ## Reading the output Component uids (`r20`) are stable across tools and re-renders but reset on page reload — re-run `list_3p_developer_tools` and `react_get_component_tree` after `navigate_page` or a reload. React Compiler output shows as `Forget(ButtonComponent)`; styled-components as `styled.button`. Durations are milliseconds; `componentsChanged` counts fibers that rendered in that commit. Real run against `components-menubutton--default` (profile around a click on the "Open" button): ``` $CD execute_3p_developer_tool 1 react_stop_profiling {"status": "stopped", "traceName": "open-menu", "commits": 5} $CD execute_3p_developer_tool 1 react_get_trace_overview --params '{"traceName":"open-menu"}' [{"commit":0,"committedAt":732,"renderDuration":2.3,"layoutDuration":0.6,"passiveDuration":0.8,"componentsChanged":166}, {"commit":1,"committedAt":739,"renderDuration":2.5,"layoutDuration":2.5,"passiveDuration":0.2,"componentsChanged":156}, ...] $CD execute_3p_developer_tool 1 react_get_commit_report --params '{"traceName":"open-menu","commitIndex":1}' priority: Sync, 156 components, sorted by actualDuration: r150 forwardRef styled.div actual=3.00 self=0.30 r152 function Forget(FlexComponent) actual=2.70 self=0.10 r0 function Forget(MenuButton) actual=2.50 self=0.20 ``` `react_get_component_source` resolves to the workspace source through Vite, e.g. `http://localhost:6006/@fs/.../packages/ui/src/core/primitives/button/button.tsx:21`, and `react_get_component_by_uid` with `includeHooks` lists hooks such as `Theme_v2` with their values. ## Gotchas - `execute_3p_developer_tool` answers "Tool react_... not found" until `list_3p_developer_tools` has run for that page load. - No `react` group listed: the page was not loaded from the `ENABLE_REACT_DEVTOOLS_MCP=true` server, or you are on the manager page (`/`) instead of `iframe.html`. - The `chrome-devtools` CLI and the MCP server are the same daemon; do not run both against the same Chrome at once. `pnpm --filter sanity-ui-storybook exec chrome-devtools status` / `stop`. - Chrome keeps running after `react-devtools-mcp:chrome` returns (it prints the pid); the profile is reused from `apps/storybook/node_modules/.cache/react-devtools-mcp/chrome-profile`. Running the command again while that Chrome is up opens the url as a new tab in it ("Reused the browser that was already listening") instead of starting a second Chrome. - "Chrome exited before opening its debugging port": a Chrome using the profile is already running without `--remote-debugging-port`. Close it and retry. `--port` must be 1–65535. - Prefer `--headless` on machines without a display; the script turns it on automatically when `DISPLAY` is unset on Linux. Set `CHROME_PATH` if Chrome is not found. - To let `chrome-devtools-mcp` launch its own Chrome instead, drop `--browserUrl` (and skip step 2); then open the story with `navigate_page` or `new_page`.