Skip to content

[mcp] Add MCP tool to print out the component tree of the currently open React App #33305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

jorge-cab
Copy link
Contributor

Summary

This tool leverages DevTools to get the component tree from the currently open React App. This gives realtime information to agents about the state of the app.

How did you test this change?

Tested integration with Claude Desktop

@react-sizebot
Copy link

react-sizebot commented May 19, 2025

Comparing: 462d08f...1e4614b

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 529.74 kB 529.74 kB = 93.49 kB 93.49 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 651.48 kB 651.48 kB = 114.76 kB 114.75 kB
facebook-www/ReactDOM-prod.classic.js = 675.72 kB 675.72 kB = 118.85 kB 118.84 kB
facebook-www/ReactDOM-prod.modern.js = 666.00 kB 666.00 kB = 117.23 kB 117.23 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 1e4614b

@jorge-cab jorge-cab requested review from hoxyq and poteto May 20, 2025 17:44
@jorge-cab
Copy link
Contributor Author

@hoxyq Added the flag to webpack.config.js instead of webpack.backend.js because for some reason the variable would not be recognized in renderer.js unless set on webpack.config.js

@jorge-cab jorge-cab marked this pull request as ready for review May 20, 2025 17:46
@@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
const IS_EDGE = process.env.IS_EDGE === 'true';
const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';

const IS_INTERNAL = process.env.IS_INTERNAL === 'true';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const IS_INTERNAL = process.env.IS_INTERNAL === 'true';
const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your comment here, this should be correct, yeah. Because installHook entrypoint, which imports fiber/renderer.js is listed in webpack.config.js.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix failing jobs on CI, please add __IS_INTERNAL_MCP_BUILD__: false to other build scripts, where applicable. You can check where __IS_CHROME__: false is defined, for example.

@@ -113,6 +115,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
__IS_INTERNAL__: IS_INTERNAL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__IS_INTERNAL__: IS_INTERNAL,
__IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD,

@@ -504,6 +505,7 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL__: 'readonly',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__IS_INTERNAL__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',

@@ -5873,6 +5946,7 @@ export function attach(
getNearestMountedDOMNode,
getElementIDForHostInstance,
getInstanceAndStyle,
...(__IS_INTERNAL__ && {internal_only_getComponentTree}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...(__IS_INTERNAL__ && {internal_only_getComponentTree}),
...(__IS_INTERNAL_MCP_BUILD__ && {internal_only_getComponentTree}),

const componentTree = await localhostPage.evaluate(() => {
return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
.get(1)
.getComponentTree();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.getComponentTree();
.__internal_only_getComponentTree();

@@ -5859,6 +5859,79 @@ export function attach(
return unresolvedSource;
}

function internal_only_getComponentTree(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function internal_only_getComponentTree(): string {
function __internal_only_getComponentTree(): string {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can you try gating the definition of this function in __IS_INTERNAL_MCP_BUILD__?

if (__IS_INTERNAL_MCP_BUILD__) {
  function __internal_only_getComponentTree(): string {
    ...
  }
}

if (localhostPage) {
const componentTree = await localhostPage.evaluate(() => {
return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
.get(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get(1) will usually return you the Fiber renderer, basically the client-side renderer of React.

In case of RSC, there could also be another renderer. I am not sure about the order of registration, but it would probably be registered after the Fiber one.

For component tree, we probably care only about Fiber renderer, but worth keeping in mind that there could be rare cases where there are multiple renderers.


const name =
(instance.kind !== VIRTUAL_INSTANCE
? getDisplayNameForFiber(instance.data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a custom logic for Compiler, whereas every Fiber that has a trace of useMemoCache would have a Forget(...) prefix. Also for React.memo and HOC.

You kinda creating a dependency here between RDT and MCP, because if next time we decide to change Forget to anything else like Compiled, it would require updating MCP prompt or whatever.

I am not against keeping it like this for now, but maybe worth forking the getDisplayNameForFiber function and adding some customisation.

idToDevToolsInstanceMap.forEach(instance => {
if (
instance.parent === null ||
(instance.parent.kind === FILTERED_FIBER_INSTANCE &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How complete you want this tree representation to be? Right now we filter out lots of things, see shouldFilterFiber implementation.

Maybe you should get a tree representation as full as it is.

Also, this filter out things that are defined in user filters. For example, I think we have a default filter for DOM-elements, like div, span, ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants