diff --git a/.eslintrc.js b/.eslintrc.js
index c1eb5b34ebe82..1bb4e868d0694 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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',
},
diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts
index 2ec747eac4dfd..2871027d64ce3 100644
--- a/compiler/packages/react-mcp-server/src/index.ts
+++ b/compiler/packages/react-mcp-server/src/index.ts
@@ -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).
+
+
+ - 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"
+
+ `,
+ {
+ 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: [
{
diff --git a/compiler/packages/react-mcp-server/src/tools/componentTree.ts b/compiler/packages/react-mcp-server/src/tools/componentTree.ts
new file mode 100644
index 0000000000000..a124066a9424e
--- /dev/null
+++ b/compiler/packages/react-mcp-server/src/tools/componentTree.ts
@@ -0,0 +1,38 @@
+import puppeteer from 'puppeteer';
+
+export async function parseReactComponentTree(url: string): Promise {
+ 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);
+ }
+}
diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js
index 32d4fadcb5884..c1312fc6d8ec8 100644
--- a/packages/react-devtools-core/webpack.backend.js
+++ b/packages/react-devtools-core/webpack.backend.js
@@ -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}"`,
diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js
index 8caadec10b070..6a9636c6911b1 100644
--- a/packages/react-devtools-core/webpack.standalone.js
+++ b/packages/react-devtools-core/webpack.standalone.js
@@ -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,
diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js
index effa6cc330bb0..4bfa05183067e 100644
--- a/packages/react-devtools-extensions/webpack.backend.js
+++ b/packages/react-devtools-extensions/webpack.backend.js
@@ -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',
diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js
index 51b8f4e2105e3..4a3052517c851 100644
--- a/packages/react-devtools-extensions/webpack.config.js
+++ b/packages/react-devtools-extensions/webpack.config.js
@@ -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}"`,
diff --git a/packages/react-devtools-fusebox/webpack.config.frontend.js b/packages/react-devtools-fusebox/webpack.config.frontend.js
index ab7906ca84d63..ea04f4dad2d0d 100644
--- a/packages/react-devtools-fusebox/webpack.config.frontend.js
+++ b/packages/react-devtools-fusebox/webpack.config.frontend.js
@@ -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,
diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js
index 3a92dff1f2195..9fa900dfa65f2 100644
--- a/packages/react-devtools-inline/webpack.config.js
+++ b/packages/react-devtools-inline/webpack.config.js
@@ -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,
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index 4fc59a24d9272..94246df6485e4 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -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 = [];
+ 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,
};
}
diff --git a/scripts/flow/react-devtools.js b/scripts/flow/react-devtools.js
index 4e0f2a915ede6..09a251bbe2f5f 100644
--- a/scripts/flow/react-devtools.js
+++ b/scripts/flow/react-devtools.js
@@ -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;
diff --git a/scripts/jest/devtools/setupEnv.js b/scripts/jest/devtools/setupEnv.js
index a797c0951435f..32bf13e686c77 100644
--- a/scripts/jest/devtools/setupEnv.js
+++ b/scripts/jest/devtools/setupEnv.js
@@ -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;