Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions compiler/packages/react-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
import {parseReactComponentTree} from './tools/componentTree';

function calculateMean(values: number[]): string {
return values.length > 0
Expand Down Expand Up @@ -366,6 +367,38 @@ ${calculateMean(results.renderTime)}
},
);

server.tool(
'parse-react-component-tree',
`
This tool gets the component tree of a React App.
passing in a url will attempt to connect to the browser and get the current state of the component tree. If no url is passed in,
the default url will be used (http://localhost:3000).

<requirements>
- The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
- Also the user should be running a Chrome browser running on debug mode on port 9222. If you receive an error message, advise the user to run
the following comand in the terminal:
MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
</requirements>
`,
{
url: z.string().optional().default('http://localhost:3000'),
},
async ({url}) => {
const componentTree = await parseReactComponentTree(url);

return {
content: [
{
type: 'text' as const,
text: componentTree,
},
],
};
Copy link
Member

Choose a reason for hiding this comment

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

probably want to wrap this in a try/catch and return the error response to the llm, like we do in other tools

},
);

server.prompt('review-react-code', () => ({
messages: [
{
Expand Down
36 changes: 36 additions & 0 deletions compiler/packages/react-mcp-server/src/tools/componentTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import puppeteer from 'puppeteer';

export async function parseReactComponentTree(url: string): Promise<string> {
try {
const browser = await puppeteer.connect({
browserURL: 'http://127.0.0.1:9222',
defaultViewport: null,
});

const pages = await browser.pages();

let localhostPage = null;
for (const page of pages) {
const pageUrl = await page.url();

if (pageUrl.startsWith(url)) {
localhostPage = page;
break;
}
}

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.

.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();

});

return componentTree;
} else {
throw new Error('Localhost page not found');
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
throw new Error('Localhost page not found');
throw new Error(`Could not open the page at ${url}. Is your server running?`);

}
} catch (error) {
throw new Error('Failed extract component tree' + error);
}
}
3 changes: 3 additions & 0 deletions packages/react-devtools-extensions/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.


const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';

const babelOptions = {
Expand Down Expand Up @@ -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,

__IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
Expand Down
74 changes: 74 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5859,6 +5859,79 @@
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 {
    ...
  }
}

Copy link
Contributor Author

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 {
    ...
  }
}

Hmm this doesn't seem to work, I get a not defined error

let treeString = '';

function buildTreeString(
instance: DevToolsInstance,
prefix: string = '',
isLastChild: boolean = true,
): void {
if (!instance) return;

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.

: instance.data.name) || 'Unknown';

const id = instance.id !== undefined ? instance.id : 'unknown';

if (name !== 'createRoot()') {
treeString +=
prefix +
(isLastChild ? '└── ' : '├── ') +
name +
' (id: ' +
id +
')\n';
}

const childPrefix = prefix + (isLastChild ? ' ' : '│ ');

let childCount = 0;
let tempChild = instance.firstChild;
while (tempChild !== null) {
childCount++;
tempChild = tempChild.nextSibling;
}

let child = instance.firstChild;
let currentChildIndex = 0;

while (child !== null) {
currentChildIndex++;
const isLastSibling = currentChildIndex === childCount;
buildTreeString(child, childPrefix, isLastSibling);
child = child.nextSibling;
}
}

const rootInstances: Array<DevToolsInstance> = [];
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, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yes, I think we'll benefit from having more data to pipe into the LLM I'll remove this line

instance.parent.parent === null)
) {
rootInstances.push(instance);
}
});

if (rootInstances.length > 0) {
for (let i = 0; i < rootInstances.length; i++) {
const isLast = i === rootInstances.length - 1;
buildTreeString(rootInstances[i], '', isLast);
if (!isLast) {
treeString += '\n';
}
}
} else {
treeString = 'No component tree found.';
}

return treeString;
}

return {
cleanup,
clearErrorsAndWarnings,
Expand All @@ -5873,6 +5946,7 @@
getNearestMountedDOMNode,
getElementIDForHostInstance,
getInstanceAndStyle,
...(__IS_INTERNAL__ && {internal_only_getComponentTree}),

Check failure on line 5949 in packages/react-devtools-shared/src/backend/fiber/renderer.js

View workflow job for this annotation

GitHub Actions / Run eslint

'__IS_INTERNAL__' is not defined
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}),

getOwnersList,
getPathForElement,
getProfilingData,
Expand Down
1 change: 1 addition & 0 deletions scripts/flow/react-devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ declare const __IS_FIREFOX__: boolean;
declare const __IS_CHROME__: boolean;
declare const __IS_EDGE__: boolean;
declare const __IS_NATIVE__: boolean;
declare const __IS_INTERNAL__: boolean;

declare const chrome: any;
Loading