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

## 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
This commit is contained in:
Jorge Cabiedes 2025-06-02 21:42:34 -07:00 committed by GitHub
parent 3531b26729
commit 2b4064eb9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 171 additions and 0 deletions

View File

@ -496,6 +496,7 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@ -504,6 +505,7 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},

View File

@ -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
@ -366,6 +367,45 @@ ${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}) => {
try {
const componentTree = await parseReactComponentTree(url);
return {
content: [
{
type: 'text' as const,
text: componentTree,
},
],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
}
},
);
server.prompt('review-react-code', () => ({
messages: [
{

View File

@ -0,0 +1,38 @@
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)
.__internal_only_getComponentTree();
});
return componentTree;
} else {
throw new Error(
`Could not open the page at ${url}. Is your server running?`,
);
}
} catch (error) {
throw new Error('Failed extract component tree' + error);
}
}

View File

@ -72,6 +72,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_EDGE__: false,
__IS_NATIVE__: true,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,

View File

@ -91,6 +91,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_CHROME__: false,
__IS_EDGE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
}),
new Webpack.SourceMapDevToolPlugin({
filename: '[file].map',

View File

@ -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_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
const babelOptions = {
@ -113,6 +115,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
__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}"`,

View File

@ -86,6 +86,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_FIREFOX__: false,
__IS_EDGE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_EDGE__: false,
__IS_NATIVE__: false,
__IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

View File

@ -5859,6 +5859,86 @@ export function attach(
return unresolvedSource;
}
type InternalMcpFunctions = {
__internal_only_getComponentTree?: Function,
};
const internalMcpFunctions: InternalMcpFunctions = {};
if (__IS_INTERNAL_MCP_BUILD__) {
// eslint-disable-next-line no-inner-declarations
function __internal_only_getComponentTree(): string {
let treeString = '';
function buildTreeString(
instance: DevToolsInstance,
prefix: string = '',
isLastChild: boolean = true,
): void {
if (!instance) return;
const name =
(instance.kind !== VIRTUAL_INSTANCE
? getDisplayNameForFiber(instance.data)
: 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.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;
}
internalMcpFunctions.__internal_only_getComponentTree =
__internal_only_getComponentTree;
}
return {
cleanup,
clearErrorsAndWarnings,
@ -5898,5 +5978,6 @@ export function attach(
storeAsGlobal,
updateComponentFilters,
getEnvironmentNames,
...internalMcpFunctions,
};
}

View File

@ -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_MCP_BUILD__: boolean;
declare const chrome: any;

View File

@ -15,6 +15,7 @@ global.__IS_FIREFOX__ = false;
global.__IS_CHROME__ = false;
global.__IS_EDGE__ = false;
global.__IS_NATIVE__ = false;
global.__IS_INTERNAL_MCP_BUILD__ = false;
const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;