@samitouri / QOS-React / commits / 2c8b735159

[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',