mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[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:
parent
3531b26729
commit
2b4064eb9b
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}"`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
1
scripts/flow/react-devtools.js
vendored
1
scripts/flow/react-devtools.js
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user