[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