Skip to content

Commit 2b4064e

Browse files
authored
[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
1 parent 3531b26 commit 2b4064e

File tree

12 files changed

+171
-0
lines changed

12 files changed

+171
-0
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ module.exports = {
496496
'packages/react-devtools-shared/src/devtools/views/**/*.js',
497497
'packages/react-devtools-shared/src/hook.js',
498498
'packages/react-devtools-shared/src/backend/console.js',
499+
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
499500
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
500501
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
501502
],
@@ -504,6 +505,7 @@ module.exports = {
504505
__IS_FIREFOX__: 'readonly',
505506
__IS_EDGE__: 'readonly',
506507
__IS_NATIVE__: 'readonly',
508+
__IS_INTERNAL_MCP_BUILD__: 'readonly',
507509
__IS_INTERNAL_VERSION__: 'readonly',
508510
chrome: 'readonly',
509511
},

compiler/packages/react-mcp-server/src/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
2121
import assertExhaustive from './utils/assertExhaustive';
2222
import {convert} from 'html-to-text';
2323
import {measurePerformance} from './tools/runtimePerf';
24+
import {parseReactComponentTree} from './tools/componentTree';
2425

2526
function calculateMean(values: number[]): string {
2627
return values.length > 0
@@ -366,6 +367,45 @@ ${calculateMean(results.renderTime)}
366367
},
367368
);
368369

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+
369409
server.prompt('review-react-code', () => ({
370410
messages: [
371411
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ module.exports = {
7272
__IS_CHROME__: false,
7373
__IS_EDGE__: false,
7474
__IS_NATIVE__: true,
75+
__IS_INTERNAL_MCP_BUILD__: false,
7576
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
7677
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
7778
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,

packages/react-devtools-core/webpack.standalone.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ module.exports = {
9191
__IS_FIREFOX__: false,
9292
__IS_CHROME__: false,
9393
__IS_EDGE__: false,
94+
__IS_INTERNAL_MCP_BUILD__: false,
9495
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
9596
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
9697
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

packages/react-devtools-extensions/webpack.backend.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ module.exports = {
7878
__IS_FIREFOX__: IS_FIREFOX,
7979
__IS_EDGE__: IS_EDGE,
8080
__IS_NATIVE__: false,
81+
__IS_INTERNAL_MCP_BUILD__: false,
8182
}),
8283
new Webpack.SourceMapDevToolPlugin({
8384
filename: '[file].map',

packages/react-devtools-extensions/webpack.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
3333
const IS_EDGE = process.env.IS_EDGE === 'true';
3434
const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';
3535

36+
const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
37+
3638
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
3739

3840
const babelOptions = {
@@ -113,6 +115,7 @@ module.exports = {
113115
__IS_FIREFOX__: IS_FIREFOX,
114116
__IS_EDGE__: IS_EDGE,
115117
__IS_NATIVE__: false,
118+
__IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD,
116119
__IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
117120
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
118121
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,

packages/react-devtools-fusebox/webpack.config.frontend.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ module.exports = {
8686
__IS_CHROME__: false,
8787
__IS_FIREFOX__: false,
8888
__IS_EDGE__: false,
89+
__IS_INTERNAL_MCP_BUILD__: false,
8990
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`,
9091
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
9192
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

packages/react-devtools-inline/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ module.exports = {
7878
__IS_FIREFOX__: false,
7979
__IS_EDGE__: false,
8080
__IS_NATIVE__: false,
81+
__IS_INTERNAL_MCP_BUILD__: false,
8182
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
8283
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
8384
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5859,6 +5859,86 @@ export function attach(
58595859
return unresolvedSource;
58605860
}
58615861

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+
58625942
return {
58635943
cleanup,
58645944
clearErrorsAndWarnings,
@@ -5898,5 +5978,6 @@ export function attach(
58985978
storeAsGlobal,
58995979
updateComponentFilters,
59005980
getEnvironmentNames,
5981+
...internalMcpFunctions,
59015982
};
59025983
}

0 commit comments

Comments
 (0)