This PR adds a new `getParentStack` tool to the Facade and an
integration of it in `react-devtools-cdt-mcp`.
Similarly to the owner stack tool, it receives a `uid` of a specific
component and returns structural parent stack. For every node, it
includes `uid`, `name`, `type`.
Adds a new tool to the Facade, where the input is the `HostInstance` and
the output is the corresponding component payload for the `HostFiber` of
this `HostInstance`. Supports multiple Host objects from Fiber:
`HostComponent`, `HostText`, `HostSingleton`, `HostHoistable`.
> [!NOTE]
> This doesn't bring much value now, because this returns the payload
for the corresponding HostFiber, not the closest user-defined Fiber.
This will be improved by adding a parentUid reference to the element
payload, so the agent can crawl up through the component tree.
> [!NOTE]
> I had to invalidate `node_modules` cache keys on CI, because we are
now using `react-devtools-facade` package and we need to create a
symlink to it, otherwise Flow will fail. We use a hash value of
`yarn.lock`, but it's not invalidated when a package inside yarn
workspace is using another private package.
---
This change introduces new `react-devtools-cdt-mcp` package, which is
responsible for installing React tools for `chrome-devtools-mcp` server.
It is using `react-devtools-facade` as a dependency: it doesn't own the
implementation of these tools, it just relays Facade's tools to
`chrome-devtools-mcp` format.
When CDT MCP navigates to a fresh page, it emits unique
`devtoolsdiscovery` method, that has `respondWith` method, which is the
integration point between runtime libraries and MCP server.
`react-devtools-cdt-mcp` is the owner for this integration. More details
are available here -
https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/third-party-developer-tools.md.
Here is the list of tools that are being installed:
- `react_get_component_tree`
- `react_get_component_by_uid`
- `react_find_components`
- `react_get_component_source`
- `react_get_owner_stack_trace`
- `react_get_owner_stack`
- `react_start_profiling`
- `react_stop_profiling `
- `react_get_trace_overview`
- `react_get_commit_report`
---
Quick demo how to test the package with `chrome-devtools-mcp` CLI. Steps
are also mentioned in `README` file:
https://github.com/user-attachments/assets/af9fefce-03f0-4ebd-a94f-e9dd72b87f4d
The tool now has a boolean argument which controls the presence of hook
tree in the return payload. If hooks were requested, and the inspection
fails, the error payload will be returned instead.
Instead of potential confusion coming from `stack` vs `branch`, we will
use `getOwnerStackTrace` for the error-like stack trace and
`getOwnerStack` for list of structured entities that represent owners.
Makes `installFacade` usable on pages that already have a DevTools
backend — most importantly the **React DevTools browser extension** — by
attaching to the existing `__REACT_DEVTOOLS_GLOBAL_HOOK__` instead of
refusing to install.
This is important, because otherwise if the page had Facade installed,
the user won't be able to use React DevTools browser extension.
Adds the profiler building blocks to `createTools` — per-commit render
timing on top of the component-tree tools from commit 2.
### Tools
- **`startProfiling(traceName?)`** → `{status: 'started', trace}`.
Begins a session that records timing on every commit. Errors if a
session is already active.
- **`stopProfiling()`** → `{status: 'stopped', traceName, commits}`
(commit count). Errors if no session is active.
- **`getTraceOverview(traceName)`** → one row per commit: `{commit,
committedAt, renderDuration, layoutDuration, passiveDuration,
componentsChanged}`.
- **`getCommitReport(traceName, commitIndex)`** → one commit's detail:
`{committedAt, priority, renderDuration, layoutDuration,
passiveDuration,
components}`, where `components` is `{label, name, type, actualDuration,
selfDuration}` sorted by `actualDuration` descending.
Durations are in milliseconds, or `null` when the build does not collect
profiler
timing. `passiveDuration` is attributed per root via the hook's
post-commit pass.
Adds the component-tree building blocks and the `createTools(facade)`
assembler — the first tools layered on top of the `installFacade` hook
from commit 1.
### `createTools(facade): Tools`
Reads the facade's tracked state (fiber roots + per-renderer internals)
and returns a plain `Tools` object — no globals; the integrator decides
what to do with it. Tools return **typed, plain JavaScript values** (or
`{error}`); serialization (to an integration package's wire format) is
left to the caller.
### Tools
- **`getComponentTree(depth?, rootUid?)`** — the component tree as a
flat array of `{uid, type, name, key, firstChild, nextSibling}` nodes
(an adjacency list referencing other nodes by label).
- **`getComponentByUid(uid)`** — one component's `{type, name, key?,
props?, hooks?}`. For function components, `hooks` is the inspected
hooks tree (nested `subHooks`), obtained via `react-debug-tools'`
`inspectHooksOfFiberWithoutDefaultDispatcher` with the renderer's
injected dispatcher (normalized by `getDispatcherRef`) — so hooks
introspection never falls back to, or bundles, React's shared internals.
- **`findComponents(name, rootUid?, page?, pageSize?)`** — paginated,
case-insensitive name search.
- **`getComponentSource(uid)`** — the component's definition location
`{name, fileName, line, column}` (or `null`).
- **`getOwnersStack(uid)`** — the raw JSX owner-stack string (DEV only).
- **`getOwnersBranch(uid)`** — the structured owner chain `[{uid, name,
type}]`, ordered immediate owner → root (DEV only).
### UIDs
Components are addressed by stable `rN` uids, assigned lazily and
memoized per fiber (and its alternate), so a component keeps the same
uid across re-renders and across every tool. These uids don't survive
page reloads.
Introduces **`react-devtools-facade`**, a private, source-only package
of building blocks for React runtime state introspection. Integrates
with Reconciler through React DevTools global hook.
### Plan
The facade is the shared, low-level layer that public,
MCP-server–specific packages build on:
- **`react-devtools-facade` (private)** — installs the React DevTools
global hook and exposes a small, framework-agnostic API: install the
hook, then assemble a set of tools from the returned handle. It installs
*no* tool globals and makes no decision about how tools are surfaced.
- **Integration packages (public)**, e.g. `react-devtools-cdt-mcp` —
compose the facade's tools and target a specific MCP server
(chrome-devtools-mcp first).
This keeps all runtime-introspection logic in one reusable place while
each
integration owns its own protocol, packaging, serialisation and globals.
### This PR
- `installFacade(target = globalThis): Facade` — installs **only**
`__REACT_DEVTOOLS_GLOBAL_HOOK__` (the global React looks for at init)
and returns a `Facade` handle `{hook, fiberRoots, rendererInternals,
profilingState}`. The hook tracks fiber roots on commit and forwards
commits to the profiling state when a session is active; building blocks
read from the returned handle and never touch globals.
- Guards against double-install (mixing with the full DevTools backend).
*Temporary*, will later add compatibility for the RDT extension
scenario.
- Package scaffold: `package.json` (private, source-only), `index.js`,
`README.md`.
- Excluded from the general jest runners; runs under the build-devtools
project, matching `react-devtools-shared` / `react-devtools-extensions`.
> Tool building blocks and `createTools(facade)` land in the follow-ups.