@samitouri / QOS-React-1 / commits / 3508aee63d

[react-devtools] add parent stack tool (#36825)

This PR adds a new `getParentStack` tool to the Facade and an integration of it in `react-devtools-cdt-mcp`. Similarly to the owner stack tool, it receives a `uid` of a specific component and returns structural parent stack. For every node, it includes `uid`, `name`, `type`.

Ruslan Lesiutin committed Jul 2, 2026 at 19:49 UTC 3508aee63d59b7d4eb8e82171b78ef214a2487ca
7 files changed +323 -20
packages/react-devtools-cdt-mcp/README.md
+13 -2
@@ -89,13 +89,24 @@ Raw owner stack trace — the chain of JSX creation locations up to the root.
89 - **Input:** `uid` (string, required).
90 - **Output:** `{stack: string}` (DEV-only; empty in production).
91
92 +### `react_get_parent_stack`
93 +
94 +Rendered parent list — where a component is mounted in the rendered component
95 +tree.
96 +
97 +- **Input:** `uid` (string, required).
98 +- **Output:** an array of `{uid, name, type}`, ordered from immediate parent to
99 + root (empty for the root). This can include host DOM components and the root.
100 +
101 ### `react_get_owner_stack`
102
94 -Structured owner list — which components rendered this one.
103 +Structured owner list — which components created/rendered this element through
104 +JSX.
105
106 - **Input:** `uid` (string, required).
107 - **Output:** an array of `{uid, name, type}`, ordered from immediate owner to
98 - root ancestor (empty for a root component). DEV-only.
108 + root owner (empty for a root component). DEV-only. Owners are not structural
109 + parents; use `react_get_parent_stack` for mounted tree ancestry.
110
111 ### `react_start_profiling`
112
packages/react-devtools-cdt-mcp/e2e/run.flow.js
+88 -9
@@ -110,8 +110,10 @@ type OwnersStackResult = {
110 stack: string,
111 ...
112 };
113 -type Owner = {
113 +type ComponentBranchEntry = {
114 + uid: string,
115 name: string,
116 + type: string,
117 ...
118 };
119 type ErrorPayload = {
@@ -146,6 +148,7 @@ const TOOL_NAMES = [
148 'react_find_components',
149 'react_get_component_source',
150 'react_get_owner_stack_trace',
151 + 'react_get_parent_stack',
152 'react_get_owner_stack',
153 'react_start_profiling',
154 'react_stop_profiling',
@@ -665,10 +668,21 @@ function parseNamedObject(value: mixed, message: string): {name: string, ...} {
668 };
669 }
670
671 +function parseComponentBranchEntry(
672 + value: mixed,
673 + message: string
674 +): ComponentBranchEntry {
675 + const object = expectObject(value, message);
676 + return {
677 + uid: expectString(object.uid, `${message} uid`),
678 + name: expectString(object.name, `${message} name`),
679 + type: expectString(object.type, `${message} type`),
680 + };
681 +}
682 +
683 function parseComponentDetails(value: mixed): ComponentDetails {
684 const object = expectObject(value, 'Expected component details object');
670 - return {
671 - ...object,
685 + const details: ComponentDetails = {
686 name: expectString(object.name, 'Expected component details name'),
687 type: expectString(object.type, 'Expected component details type'),
688 hooks: expectArray(object.hooks, 'Expected component details hooks').map(
@@ -676,6 +690,7 @@ function parseComponentDetails(value: mixed): ComponentDetails {
690 parseNamedObject(hook, `Expected component hook ${index}`)
691 ),
692 };
693 + return details;
694 }
695
696 function parseComponentType(value: mixed): string {
@@ -775,9 +790,13 @@ function parseOwnersStack(value: mixed): OwnersStackResult {
790 };
791 }
792
778 -function parseOwnersBranch(value: mixed): Array<Owner> {
779 - return expectArray(value, 'Expected owners branch array').map(
780 - (owner, index) => parseNamedObject(owner, `Expected owner ${index}`)
793 +function parseComponentBranch(
794 + value: mixed,
795 + label: string
796 +): Array<ComponentBranchEntry> {
797 + return expectArray(value, `Expected ${label} branch array`).map(
798 + (entry, index) =>
799 + parseComponentBranchEntry(entry, `Expected ${label} ${index}`)
800 );
801 }
802
@@ -989,6 +1008,31 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
1008 node => node.name === 'Todo' && node.type === 'function',
1009 'Expected function component Todo'
1010 );
1011 + const todoList = findNode(
1012 + tree,
1013 + node => node.name === 'TodoList' && node.type === 'function',
1014 + 'Expected function component TodoList'
1015 + );
1016 + const todoListHost = findNode(
1017 + tree,
1018 + node => node.name === 'ul' && node.type === 'host',
1019 + 'Expected host ul for TodoList'
1020 + );
1021 + const mainNode = findNode(
1022 + tree,
1023 + node => node.name === 'main' && node.type === 'host',
1024 + 'Expected host main'
1025 + );
1026 + const app = findNode(
1027 + tree,
1028 + node => node.name === 'App' && node.type === 'function',
1029 + 'Expected function component App'
1030 + );
1031 + const root = findNode(
1032 + tree,
1033 + node => node.type === 'root',
1034 + 'Expected root node'
1035 + );
1036 const memoBox = findNode(
1037 tree,
1038 node => node.name.includes('MemoBox') && node.type === 'memo',
@@ -1069,7 +1113,7 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
1113 assert.strictEqual(domLookup.type, 'host');
1114 assert.strictEqual(domLookup.name, 'button');
1115
1072 - log('Checking source, owners, and error payloads...');
1116 + log('Checking source, parents, owners, and error payloads...');
1117 const source = parseSourceResult(
1118 await callTool('react_get_component_source', {
1119 uid: counter.uid,
@@ -1088,10 +1132,45 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
1132 );
1133 }
1134
1091 - const ownersBranch = parseOwnersBranch(
1135 + const parentsBranch = parseComponentBranch(
1136 + await callTool('react_get_parent_stack', {
1137 + uid: todo.uid,
1138 + }),
1139 + 'parents'
1140 + );
1141 + assert.deepStrictEqual(parentsBranch, [
1142 + {
1143 + uid: todoListHost.uid,
1144 + name: todoListHost.name,
1145 + type: todoListHost.type,
1146 + },
1147 + {
1148 + uid: todoList.uid,
1149 + name: todoList.name,
1150 + type: todoList.type,
1151 + },
1152 + {
1153 + uid: mainNode.uid,
1154 + name: mainNode.name,
1155 + type: mainNode.type,
1156 + },
1157 + {
1158 + uid: app.uid,
1159 + name: app.name,
1160 + type: app.type,
1161 + },
1162 + {
1163 + uid: root.uid,
1164 + name: root.name,
1165 + type: root.type,
1166 + },
1167 + ]);
1168 +
1169 + const ownersBranch = parseComponentBranch(
1170 await callTool('react_get_owner_stack', {
1171 uid: todo.uid,
1094 - })
1172 + }),
1173 + 'owners'
1174 );
1175 assert(
1176 ownersBranch.some(owner => owner.name === 'TodoList'),
packages/react-devtools-cdt-mcp/src/DevToolsCdtMcp.js
+22 -3
@@ -191,12 +191,31 @@ const TOOL_DEFINITIONS: Array<ToolDefinition> = [
191 },
192 call: (tools, args) => tools.getOwnerStackTrace(args.uid),
193 },
194 + {
195 + name: 'react_get_parent_stack',
196 + description:
197 + 'Rendered parent chain for a component, from immediate parent to root: ' +
198 + 'an array of {uid, name, type}. Parents describe where the node is ' +
199 + 'mounted in the rendered component tree and may include host DOM ' +
200 + 'components and the root. This differs from owners, which describe JSX ' +
201 + 'creation/render ownership.',
202 + inputSchema: {
203 + type: 'object',
204 + properties: {
205 + uid: {type: 'string', description: 'Component uid, e.g. "r5".'},
206 + },
207 + required: ['uid'],
208 + },
209 + call: (tools, args) => tools.getParentStack(args.uid),
210 + },
211 {
212 name: 'react_get_owner_stack',
213 description:
197 - 'Structured owner chain for a component, from immediate owner to root ' +
198 - 'ancestor: an array of {uid, name, type} (empty for a root ' +
199 - 'component). DEV-only.',
214 + 'JSX owner chain for a component, from immediate owner to root owner: ' +
215 + 'an array of {uid, name, type} (empty for a root component). Owners ' +
216 + 'describe which components created/rendered this element through JSX, ' +
217 + 'not where it is mounted in the rendered component tree. This DEV-only metadata ' +
218 + 'differs from structural parents.',
219 inputSchema: {
220 type: 'object',
221 properties: {
packages/react-devtools-cdt-mcp/src/__tests__/DevToolsCdtMcp-test.js
+48 -3
@@ -14,6 +14,7 @@ const TOOL_NAMES = [
14 'react_find_components',
15 'react_get_component_source',
16 'react_get_owner_stack_trace',
17 + 'react_get_parent_stack',
18 'react_get_owner_stack',
19 'react_start_profiling',
20 'react_stop_profiling',
@@ -105,6 +106,12 @@ describe('react-devtools-cdt-mcp', () => {
106 expect(tool.inputSchema.type).toBe('object');
107 expect(typeof tool.execute).toBe('function');
108 });
109 + expect(getTool('react_get_parent_stack').description).toEqual(
110 + expect.stringContaining('Rendered parent chain'),
111 + );
112 + expect(getTool('react_get_owner_stack').description).toEqual(
113 + expect.stringContaining('Owners describe'),
114 + );
115 });
116
117 it('declares JSON-Schema input with required params', () => {
@@ -144,6 +151,16 @@ describe('react-devtools-cdt-mcp', () => {
151 },
152 required: ['name'],
153 });
154 + expect(getTool('react_get_parent_stack').inputSchema).toEqual({
155 + type: 'object',
156 + properties: {uid: {type: 'string', description: expect.any(String)}},
157 + required: ['uid'],
158 + });
159 + expect(getTool('react_get_owner_stack').inputSchema).toEqual({
160 + type: 'object',
161 + properties: {uid: {type: 'string', description: expect.any(String)}},
162 + required: ['uid'],
163 + });
164 expect(getTool('react_start_profiling').inputSchema).toEqual({
165 type: 'object',
166 properties: {
@@ -174,8 +191,8 @@ describe('react-devtools-cdt-mcp', () => {
191 });
192
193 const result = getTool('react_get_component_tree').execute({});
177 - // Uids are assigned deterministically as fibers are first encountered. A
178 - // fiber's first child is assigned before the fiber itself, so App (the host
194 + // Uids are assigned deterministically as nodes are first encountered. A
195 + // node's first child is assigned before the node itself, so App (the host
196 // root's first child) is r0, the host root is r1, and the div is r2.
197 expect(result).toEqual({
198 nodes: [
@@ -320,6 +337,34 @@ describe('react-devtools-cdt-mcp', () => {
337 });
338 });
339
340 + it('react_get_parent_stack returns structural ancestors', () => {
341 + function Child() {
342 + return <span>leaf</span>;
343 + }
344 + function Owner() {
345 + return (
346 + <section>
347 + <Child />
348 + </section>
349 + );
350 + }
351 +
352 + act(() => {
353 + ReactDOMClient.createRoot(container).render(<Owner />);
354 + });
355 +
356 + const tree = getTool('react_get_component_tree').execute({}).nodes;
357 + const child = tree.find(n => n.name === 'Child');
358 +
359 + expect(getTool('react_get_parent_stack').execute({uid: child.uid})).toEqual(
360 + [
361 + {uid: 'r2', name: 'section', type: 'host'},
362 + {uid: 'r0', name: 'Owner', type: 'function'},
363 + {uid: 'r1', name: expect.any(String), type: 'root'},
364 + ],
365 + );
366 + });
367 +
368 it('react_get_component_by_dom_element returns the DOM element component', () => {
369 function Wrapper({children}) {
370 return <section className="wrap">{children}</section>;
@@ -428,7 +473,7 @@ describe('react-devtools-cdt-mcp', () => {
473 expect(overview).toHaveLength(1);
474 expect(overview[0].commit).toBe(0);
475
431 - // The commit report lists the fibers that rendered, sorted by actualDuration
476 + // The commit report lists the components that rendered, sorted by actualDuration
477 // descending, with uids assigned in commit-walk order: the host root r0
478 // (widest duration), then Counter r1, then the div r2. Durations are
479 // timing-dependent, so assert identity (uid/name/type) only.
packages/react-devtools-facade/src/DevToolsFacadeTools.js
+5
@@ -13,6 +13,7 @@ import type {
13 NodeInfo,
14 ComponentSource,
15 OwnersStack,
16 + ParentEntry,
17 OwnerEntry,
18 FindComponentsResult,
19 ToolError,
@@ -34,6 +35,8 @@ export type {
35 ComponentSource,
36 SourceLocation,
37 OwnersStack,
38 + ComponentBranchEntry,
39 + ParentEntry,
40 OwnerEntry,
41 FindComponentsResult,
42 ToolError,
@@ -68,6 +71,7 @@ export type Tools = {
71 ) => FindComponentsResult | ToolError,
72 getComponentSource: (uid: string) => ComponentSource | ToolError,
73 getOwnerStackTrace: (uid: string) => OwnersStack | ToolError,
74 + getParentStack: (uid: string) => Array<ParentEntry> | ToolError,
75 getOwnerStack: (uid: string) => Array<OwnerEntry> | ToolError,
76 startProfiling: (traceName?: string) => StartProfilingResult | ToolError,
77 stopProfiling: () => StopProfilingResult | ToolError,
@@ -102,6 +106,7 @@ export function createTools(facade: Facade): Tools {
106 findComponents: tree.findComponents,
107 getComponentSource: tree.getComponentSource,
108 getOwnerStackTrace: tree.getOwnerStackTrace,
109 + getParentStack: tree.getParentStack,
110 getOwnerStack: tree.getOwnerStack,
111 startProfiling: profiler.startProfiling,
112 stopProfiling: profiler.stopProfiling,
packages/react-devtools-facade/src/DevToolsFacadeTreeTools.js
+47 -3
@@ -66,7 +66,11 @@ export type ComponentSource = {source: SourceLocation | null};
66
67 export type OwnersStack = {stack: string};
68
69 -export type OwnerEntry = {uid: string, name: string, type: string};
69 +export type ComponentBranchEntry = {uid: string, name: string, type: string};
70 +
71 +export type ParentEntry = ComponentBranchEntry;
72 +
73 +export type OwnerEntry = ComponentBranchEntry;
74
75 export type FindComponentsResult = {
76 page: number,
@@ -94,6 +98,7 @@ export type TreeTools = {
98 ) => FindComponentsResult | ToolError,
99 getComponentSource: (uid: string) => ComponentSource | ToolError,
100 getOwnerStackTrace: (uid: string) => OwnersStack | ToolError,
101 + getParentStack: (uid: string) => Array<ParentEntry> | ToolError,
102 getOwnerStack: (uid: string) => Array<OwnerEntry> | ToolError,
103 // Shared with the profiler tools so component uids are consistent across all
104 // tools. Maps a fiber to its stable uid (assigning one on first encounter).
@@ -737,10 +742,48 @@ export function createTreeTools(
742 return {stack: stackString};
743 }
744
745 + /**
746 + * Returns the structural parent branch for this fiber — the path formed by
747 + * following Fiber.return pointers from this component to the host root.
748 + * Parents describe where a node is mounted in the rendered tree, so this
749 + * branch can include host DOM components and the host root.
750 + *
751 + * This differs from owners: owners describe which components created/rendered
752 + * an element through JSX and are DEV-only metadata. Parents are structural
753 + * runtime relationships and are available whenever the fiber tree exists.
754 + *
755 + * Returns an array of {uid, name, type}, ordered from immediate parent to
756 + * root ancestor. The host root has an empty parent branch.
757 + *
758 + * @param uid - The component uid (e.g. "r5").
759 + */
760 + function getParentStack(uid: string): Array<ParentEntry> | ToolError {
761 + const result = findFiberByUid(uid);
762 + if (result.error != null) {
763 + return {error: result.error};
764 + }
765 + const {internals} = result;
766 + const parents: Array<ParentEntry> = [];
767 + let parent = result.fiber.return;
768 + while (parent !== null) {
769 + parents.push({
770 + uid: getUid(parent),
771 + name: getDisplayName(internals, parent),
772 + type: getTypeTagForFiber(internals, parent),
773 + });
774 + parent = parent.return;
775 + }
776 + return parents;
777 + }
778 +
779 /**
780 * Returns the structured list of owner components — which components rendered
742 - * this component, ordered from immediate owner to root ancestor. Each entry
743 - * includes a uid for cross-referencing with other tools (e.g.
781 + * or created this element through JSX, ordered from immediate owner to root
782 + * ancestor. Owners describe creation/render ownership, not where a node is
783 + * mounted in the rendered tree. Use getParentStack for structural Fiber
784 + * parent ancestry, including host DOM parents and the host root.
785 + *
786 + * Each entry includes a uid for cross-referencing with other tools (e.g.
787 * getComponentByUid, getComponentSource, getComponentTree).
788 *
789 * Returns an array of {uid, name, type}, or an empty array if the component
@@ -787,6 +830,7 @@ export function createTreeTools(
830 findComponents,
831 getComponentSource,
832 getOwnerStackTrace,
833 + getParentStack,
834 getOwnerStack,
835 getUid,
836 };
packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js
+100
@@ -1035,6 +1035,106 @@ describe('react-devtools-facade', () => {
1035 });
1036 });
1037
1038 + describe('getParentStack', () => {
1039 + let getParentStack;
1040 + let getOwnerStack;
1041 + let getComponentTree;
1042 +
1043 + beforeEach(() => {
1044 + const tools = createTools(facade);
1045 + getParentStack = tools.getParentStack;
1046 + getOwnerStack = tools.getOwnerStack;
1047 + getComponentTree = tools.getComponentTree;
1048 + });
1049 +
1050 + it('returns structural parents from immediate parent to host root', () => {
1051 + function Child() {
1052 + return <span>leaf</span>;
1053 + }
1054 + function App() {
1055 + return (
1056 + <section>
1057 + <Child />
1058 + </section>
1059 + );
1060 + }
1061 +
1062 + act(() => {
1063 + ReactDOMClient.createRoot(container).render(<App />);
1064 + });
1065 +
1066 + const child = getComponentTree().find(n => n.name === 'Child');
1067 + expect(child).toBeDefined();
1068 +
1069 + const parents = getParentStack(child.uid);
1070 + expect(parents).toEqual([
1071 + {
1072 + uid: 'r2',
1073 + name: 'section',
1074 + type: 'host',
1075 + },
1076 + {
1077 + uid: 'r0',
1078 + name: 'App',
1079 + type: 'function',
1080 + },
1081 + {
1082 + uid: 'r1',
1083 + name: expect.any(String),
1084 + type: 'root',
1085 + },
1086 + ]);
1087 + });
1088 +
1089 + it('distinguishes structural parents from JSX owners', () => {
1090 + function Child() {
1091 + return <span>leaf</span>;
1092 + }
1093 + function App() {
1094 + return (
1095 + <section>
1096 + <Child />
1097 + </section>
1098 + );
1099 + }
1100 +
1101 + act(() => {
1102 + ReactDOMClient.createRoot(container).render(<App />);
1103 + });
1104 +
1105 + const child = getComponentTree().find(n => n.name === 'Child');
1106 + const parents = getParentStack(child.uid);
1107 + const owners = getOwnerStack(child.uid);
1108 +
1109 + expect(parents[0]).toMatchObject({
1110 + name: 'section',
1111 + type: 'host',
1112 + });
1113 + expect(owners[0]).toMatchObject({
1114 + name: 'App',
1115 + type: 'function',
1116 + });
1117 + });
1118 +
1119 + it('returns an empty array for the host root', () => {
1120 + function App() {
1121 + return <div>hello</div>;
1122 + }
1123 +
1124 + act(() => {
1125 + ReactDOMClient.createRoot(container).render(<App />);
1126 + });
1127 +
1128 + const root = getComponentTree().find(n => n.type === 'root');
1129 + expect(getParentStack(root.uid)).toEqual([]);
1130 + });
1131 +
1132 + it('returns error for non-existent uid', () => {
1133 + const result = getParentStack('r9999');
1134 + expect(result.error).toMatch(/Component not found/);
1135 + });
1136 + });
1137 +
1138 describe('getOwnerStack', () => {
1139 let getOwnerStack;
1140 let getComponentTree;