@samitouri / QOS-React-1 / commits / 2b4064eb9b

[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

Jorge Cabiedes committed Jun 2, 2025 at 21:42 UTC 2b4064eb9b40f65d20a03ce93b246ad762d562e6
12 files changed +171
.eslintrc.js
+2
@@ -496,6 +496,7 @@ module.exports = {
496 'packages/react-devtools-shared/src/devtools/views/**/*.js',
497 'packages/react-devtools-shared/src/hook.js',
498 'packages/react-devtools-shared/src/backend/console.js',
499 + 'packages/react-devtools-shared/src/backend/fiber/renderer.js',
500 'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
501 'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
502 ],
@@ -504,6 +505,7 @@ module.exports = {
505 __IS_FIREFOX__: 'readonly',
506 __IS_EDGE__: 'readonly',
507 __IS_NATIVE__: 'readonly',
508 + __IS_INTERNAL_MCP_BUILD__: 'readonly',
509 __IS_INTERNAL_VERSION__: 'readonly',
510 chrome: 'readonly',
511 },
compiler/packages/react-mcp-server/src/index.ts
+40
@@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
21 import assertExhaustive from './utils/assertExhaustive';
22 import {convert} from 'html-to-text';
23 import {measurePerformance} from './tools/runtimePerf';
24 +import {parseReactComponentTree} from './tools/componentTree';
25
26 function calculateMean(values: number[]): string {
27 return values.length > 0
@@ -366,6 +367,45 @@ ${calculateMean(results.renderTime)}
367 },
368 );
369
370 +server.tool(
371 + 'parse-react-component-tree',
372 + `
373 + This tool gets the component tree of a React App.
374 + 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,
375 + the default url will be used (http://localhost:3000).
376 +
377 + <requirements>
378 + - The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
379 + - 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
380 + the following comand in the terminal:
381 + MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
382 + Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
383 + </requirements>
384 + `,
385 + {
386 + url: z.string().optional().default('http://localhost:3000'),
387 + },
388 + async ({url}) => {
389 + try {
390 + const componentTree = await parseReactComponentTree(url);
391 +
392 + return {
393 + content: [
394 + {
395 + type: 'text' as const,
396 + text: componentTree,
397 + },
398 + ],
399 + };
400 + } catch (err) {
401 + return {
402 + isError: true,
403 + content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
404 + };
405 + }
406 + },
407 +);
408 +
409 server.prompt('review-react-code', () => ({
410 messages: [
411 {
compiler/packages/react-mcp-server/src/tools/componentTree.ts new
+38
@@ -0,0 +1,38 @@
1 +import puppeteer from 'puppeteer';
2 +
3 +export async function parseReactComponentTree(url: string): Promise<string> {
4 + try {
5 + const browser = await puppeteer.connect({
6 + browserURL: 'http://127.0.0.1:9222',
7 + defaultViewport: null,
8 + });
9 +
10 + const pages = await browser.pages();
11 +
12 + let localhostPage = null;
13 + for (const page of pages) {
14 + const pageUrl = await page.url();
15 +
16 + if (pageUrl.startsWith(url)) {
17 + localhostPage = page;
18 + break;
19 + }
20 + }
21 +
22 + if (localhostPage) {
23 + const componentTree = await localhostPage.evaluate(() => {
24 + return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces
25 + .get(1)
26 + .__internal_only_getComponentTree();
27 + });
28 +
29 + return componentTree;
30 + } else {
31 + throw new Error(
32 + `Could not open the page at ${url}. Is your server running?`,
33 + );
34 + }
35 + } catch (error) {
36 + throw new Error('Failed extract component tree' + error);
37 + }
38 +}
packages/react-devtools-core/webpack.backend.js
+1
@@ -72,6 +72,7 @@ module.exports = {
72 __IS_CHROME__: false,
73 __IS_EDGE__: false,
74 __IS_NATIVE__: true,
75 + __IS_INTERNAL_MCP_BUILD__: false,
76 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
77 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
78 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
packages/react-devtools-core/webpack.standalone.js
+1
@@ -91,6 +91,7 @@ module.exports = {
91 __IS_FIREFOX__: false,
92 __IS_CHROME__: false,
93 __IS_EDGE__: false,
94 + __IS_INTERNAL_MCP_BUILD__: false,
95 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
96 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
97 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
packages/react-devtools-extensions/webpack.backend.js
+1
@@ -78,6 +78,7 @@ module.exports = {
78 __IS_FIREFOX__: IS_FIREFOX,
79 __IS_EDGE__: IS_EDGE,
80 __IS_NATIVE__: false,
81 + __IS_INTERNAL_MCP_BUILD__: false,
82 }),
83 new Webpack.SourceMapDevToolPlugin({
84 filename: '[file].map',
packages/react-devtools-extensions/webpack.config.js
+3
@@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
33 const IS_EDGE = process.env.IS_EDGE === 'true';
34 const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';
35
36 +const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
37 +
38 const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
39
40 const babelOptions = {
@@ -113,6 +115,7 @@ module.exports = {
115 __IS_FIREFOX__: IS_FIREFOX,
116 __IS_EDGE__: IS_EDGE,
117 __IS_NATIVE__: false,
118 + __IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD,
119 __IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
120 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
121 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
packages/react-devtools-fusebox/webpack.config.frontend.js
+1
@@ -86,6 +86,7 @@ module.exports = {
86 __IS_CHROME__: false,
87 __IS_FIREFOX__: false,
88 __IS_EDGE__: false,
89 + __IS_INTERNAL_MCP_BUILD__: false,
90 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`,
91 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
92 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
packages/react-devtools-inline/webpack.config.js
+1
@@ -78,6 +78,7 @@ module.exports = {
78 __IS_FIREFOX__: false,
79 __IS_EDGE__: false,
80 __IS_NATIVE__: false,
81 + __IS_INTERNAL_MCP_BUILD__: false,
82 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
83 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
84 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+81
@@ -5859,6 +5859,86 @@ export function attach(
5859 return unresolvedSource;
5860 }
5861
5862 + type InternalMcpFunctions = {
5863 + __internal_only_getComponentTree?: Function,
5864 + };
5865 +
5866 + const internalMcpFunctions: InternalMcpFunctions = {};
5867 + if (__IS_INTERNAL_MCP_BUILD__) {
5868 + // eslint-disable-next-line no-inner-declarations
5869 + function __internal_only_getComponentTree(): string {
5870 + let treeString = '';
5871 +
5872 + function buildTreeString(
5873 + instance: DevToolsInstance,
5874 + prefix: string = '',
5875 + isLastChild: boolean = true,
5876 + ): void {
5877 + if (!instance) return;
5878 +
5879 + const name =
5880 + (instance.kind !== VIRTUAL_INSTANCE
5881 + ? getDisplayNameForFiber(instance.data)
5882 + : instance.data.name) || 'Unknown';
5883 +
5884 + const id = instance.id !== undefined ? instance.id : 'unknown';
5885 +
5886 + if (name !== 'createRoot()') {
5887 + treeString +=
5888 + prefix +
5889 + (isLastChild ? '└── ' : '├── ') +
5890 + name +
5891 + ' (id: ' +
5892 + id +
5893 + ')\n';
5894 + }
5895 +
5896 + const childPrefix = prefix + (isLastChild ? ' ' : '│ ');
5897 +
5898 + let childCount = 0;
5899 + let tempChild = instance.firstChild;
5900 + while (tempChild !== null) {
5901 + childCount++;
5902 + tempChild = tempChild.nextSibling;
5903 + }
5904 +
5905 + let child = instance.firstChild;
5906 + let currentChildIndex = 0;
5907 +
5908 + while (child !== null) {
5909 + currentChildIndex++;
5910 + const isLastSibling = currentChildIndex === childCount;
5911 + buildTreeString(child, childPrefix, isLastSibling);
5912 + child = child.nextSibling;
5913 + }
5914 + }
5915 +
5916 + const rootInstances: Array<DevToolsInstance> = [];
5917 + idToDevToolsInstanceMap.forEach(instance => {
5918 + if (instance.parent === null || instance.parent.parent === null) {
5919 + rootInstances.push(instance);
5920 + }
5921 + });
5922 +
5923 + if (rootInstances.length > 0) {
5924 + for (let i = 0; i < rootInstances.length; i++) {
5925 + const isLast = i === rootInstances.length - 1;
5926 + buildTreeString(rootInstances[i], '', isLast);
5927 + if (!isLast) {
5928 + treeString += '\n';
5929 + }
5930 + }
5931 + } else {
5932 + treeString = 'No component tree found.';
5933 + }
5934 +
5935 + return treeString;
5936 + }
5937 +
5938 + internalMcpFunctions.__internal_only_getComponentTree =
5939 + __internal_only_getComponentTree;
5940 + }
5941 +
5942 return {
5943 cleanup,
5944 clearErrorsAndWarnings,
@@ -5898,5 +5978,6 @@ export function attach(
5978 storeAsGlobal,
5979 updateComponentFilters,
5980 getEnvironmentNames,
5981 + ...internalMcpFunctions,
5982 };
5983 }
scripts/flow/react-devtools.js
+1
@@ -16,5 +16,6 @@ declare const __IS_FIREFOX__: boolean;
16 declare const __IS_CHROME__: boolean;
17 declare const __IS_EDGE__: boolean;
18 declare const __IS_NATIVE__: boolean;
19 +declare const __IS_INTERNAL_MCP_BUILD__: boolean;
20
21 declare const chrome: any;
scripts/jest/devtools/setupEnv.js
+1
@@ -15,6 +15,7 @@ global.__IS_FIREFOX__ = false;
15 global.__IS_CHROME__ = false;
16 global.__IS_EDGE__ = false;
17 global.__IS_NATIVE__ = false;
18 +global.__IS_INTERNAL_MCP_BUILD__ = false;
19
20 const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
21