[react-devtools-cdt-mcp] add DOM element component lookup (#36822)
Integrates newly added `getComponentByHostInstance` tool from `react-devtools-facade` into `react-devtools-cdt-mcp`. The API uses `uid` of the element from CDT MCP tree snapshot, but under the hood it gets converted to the DOM Element reference, which is exactly what we pass to `getComponentByHostInstance`.
Ruslan Lesiutin committed
Jun 30, 2026 at 22:45 UTC
2c8b73515961f005904a52dce943160991a0df13
3 files changed
+110
packages/react-devtools-cdt-mcp/README.md
+9
@@ -55,6 +55,15 @@ Detailed info for a single component.
55
is true, `hooks` (function, forwardRef, and memo components) is an array of
56
`{id, name, value, subHooks}`.
57
58
+### `react_get_component_by_dom_element`
59
+
60
+Detailed info for the React DOM component corresponding to a DOM element.
61
+
62
+- **Input:** `element` (object, required). This is an opaque page-side DOM
63
+ element reference. Chrome DevTools MCP clients pass this as
64
+ `{uid: string}`, using an element uid from the page snapshot.
65
+- **Output:** `{uid, type, name, key?, props?, hooks?}`.
66
+
67
### `react_find_components`
68
69
Find components by case-insensitive name substring.
packages/react-devtools-cdt-mcp/src/DevToolsCdtMcp.js
+39
@@ -46,6 +46,24 @@ function normalizeToolResult(result: mixed): mixed {
46
return result;
47
}
48
49
+function getComponentForDomElement(tools: Tools, element: mixed): mixed {
50
+ const result = tools.getComponentByHostInstance(element);
51
+ if (
52
+ result != null &&
53
+ typeof result === 'object' &&
54
+ typeof (result as any).error === 'string'
55
+ ) {
56
+ const error = (result as any).error;
57
+ if (error === 'Host instance is required') {
58
+ return {error: 'DOM element is required'};
59
+ }
60
+ if (error === 'Host instance is not managed by React') {
61
+ return {error: 'DOM element is not managed by React'};
62
+ }
63
+ }
64
+ return result;
65
+}
66
+
67
const TOOL_DEFINITIONS: Array<ToolDefinition> = [
68
{
69
name: 'react_get_component_tree',
@@ -97,6 +115,27 @@ const TOOL_DEFINITIONS: Array<ToolDefinition> = [
115
call: (tools, args) =>
116
tools.getComponentByUid(args.uid, args.includeHooks === true),
117
},
118
+ {
119
+ name: 'react_get_component_by_dom_element',
120
+ description:
121
+ 'Detailed info for one React DOM component by DOM element reference: ' +
122
+ '{uid, type, name, key?, props?, hooks?}. The element is an opaque ' +
123
+ 'page-side reference, such as the currently selected Chrome DevTools ' +
124
+ 'element.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ element: {
129
+ type: 'object',
130
+ 'x-mcp-type': 'HTMLElement',
131
+ description:
132
+ 'DOM element reference from the Chrome DevTools MCP page snapshot.',
133
+ },
134
+ },
135
+ required: ['element'],
136
+ },
137
+ call: (tools, args) => getComponentForDomElement(tools, args.element),
138
+ },
139
{
140
name: 'react_find_components',
141
description:
packages/react-devtools-cdt-mcp/src/__tests__/DevToolsCdtMcp-test.js
+62
@@ -10,6 +10,7 @@
10
const TOOL_NAMES = [
11
'react_get_component_tree',
12
'react_get_component_by_uid',
13
+ 'react_get_component_by_dom_element',
14
'react_find_components',
15
'react_get_component_source',
16
'react_get_owner_stack_trace',
@@ -122,6 +123,17 @@ describe('react-devtools-cdt-mcp', () => {
123
},
124
required: ['uid'],
125
});
126
+ expect(getTool('react_get_component_by_dom_element').inputSchema).toEqual({
127
+ type: 'object',
128
+ properties: {
129
+ element: {
130
+ type: 'object',
131
+ 'x-mcp-type': 'HTMLElement',
132
+ description: expect.any(String),
133
+ },
134
+ },
135
+ required: ['element'],
136
+ });
137
expect(getTool('react_find_components').inputSchema).toEqual({
138
type: 'object',
139
properties: {
@@ -308,6 +320,56 @@ describe('react-devtools-cdt-mcp', () => {
320
});
321
});
322
323
+ it('react_get_component_by_dom_element returns the DOM element component', () => {
324
+ function Wrapper({children}) {
325
+ return <section className="wrap">{children}</section>;
326
+ }
327
+ function App() {
328
+ return (
329
+ <Wrapper>
330
+ <button className="action">Run</button>
331
+ </Wrapper>
332
+ );
333
+ }
334
+
335
+ act(() => {
336
+ ReactDOMClient.createRoot(container).render(<App />);
337
+ });
338
+
339
+ const button = container.querySelector('button.action');
340
+ const tree = getTool('react_get_component_tree').execute({}).nodes;
341
+ const host = tree.find(n => n.name === 'button');
342
+ const wrapper = tree.find(n => n.name === 'Wrapper');
343
+ const result = getTool('react_get_component_by_dom_element').execute({
344
+ element: button,
345
+ });
346
+
347
+ expect(result.uid).toBe(host.uid);
348
+ expect(result.uid).not.toBe(wrapper.uid);
349
+ expect(result).toMatchObject({
350
+ type: 'host',
351
+ name: 'button',
352
+ props: {className: 'action'},
353
+ });
354
+ });
355
+
356
+ it('react_get_component_by_dom_element returns DOM-oriented errors', () => {
357
+ expect(getTool('react_get_component_by_dom_element').execute({})).toEqual({
358
+ error: 'DOM element is required',
359
+ });
360
+
361
+ act(() => {
362
+ ReactDOMClient.createRoot(container).render(<div className="host" />);
363
+ });
364
+
365
+ const unmanaged = document.createElement('span');
366
+ expect(
367
+ getTool('react_get_component_by_dom_element').execute({
368
+ element: unmanaged,
369
+ }),
370
+ ).toEqual({error: 'DOM element is not managed by React'});
371
+ });
372
+
373
it('returns tool errors as a raw payload', () => {
374
const result = getTool('react_get_component_by_uid').execute({
375
uid: 'r9999',