react-native-mcp-kit 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +20 -11
  2. package/dist/bin/ios-hid +0 -0
  3. package/dist/client/contexts/McpContext/McpProvider.d.ts.map +1 -1
  4. package/dist/client/contexts/McpContext/McpProvider.js +6 -3
  5. package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
  6. package/dist/modules/alert/alert.d.ts.map +1 -1
  7. package/dist/modules/alert/alert.js +6 -5
  8. package/dist/modules/alert/alert.js.map +1 -1
  9. package/dist/modules/console/console.d.ts.map +1 -1
  10. package/dist/modules/console/console.js +18 -13
  11. package/dist/modules/console/console.js.map +1 -1
  12. package/dist/modules/device/device.d.ts.map +1 -1
  13. package/dist/modules/device/device.js +23 -27
  14. package/dist/modules/device/device.js.map +1 -1
  15. package/dist/modules/errors/errors.d.ts.map +1 -1
  16. package/dist/modules/errors/errors.js +92 -11
  17. package/dist/modules/errors/errors.js.map +1 -1
  18. package/dist/modules/errors/types.d.ts +8 -0
  19. package/dist/modules/errors/types.d.ts.map +1 -1
  20. package/dist/modules/fiberTree/fiberTree.d.ts +4 -0
  21. package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
  22. package/dist/modules/fiberTree/fiberTree.js +353 -114
  23. package/dist/modules/fiberTree/fiberTree.js.map +1 -1
  24. package/dist/modules/fiberTree/index.d.ts +1 -0
  25. package/dist/modules/fiberTree/index.d.ts.map +1 -1
  26. package/dist/modules/fiberTree/index.js +14 -1
  27. package/dist/modules/fiberTree/index.js.map +1 -1
  28. package/dist/modules/fiberTree/types.d.ts +40 -0
  29. package/dist/modules/fiberTree/types.d.ts.map +1 -1
  30. package/dist/modules/fiberTree/utils.d.ts +13 -0
  31. package/dist/modules/fiberTree/utils.d.ts.map +1 -1
  32. package/dist/modules/fiberTree/utils.js +195 -23
  33. package/dist/modules/fiberTree/utils.js.map +1 -1
  34. package/dist/modules/i18next/i18next.d.ts.map +1 -1
  35. package/dist/modules/i18next/i18next.js +30 -16
  36. package/dist/modules/i18next/i18next.js.map +1 -1
  37. package/dist/modules/index.d.ts +1 -0
  38. package/dist/modules/index.d.ts.map +1 -1
  39. package/dist/modules/index.js +3 -1
  40. package/dist/modules/index.js.map +1 -1
  41. package/dist/modules/logBox/index.d.ts +2 -0
  42. package/dist/modules/logBox/index.d.ts.map +1 -0
  43. package/dist/modules/logBox/index.js +6 -0
  44. package/dist/modules/logBox/index.js.map +1 -0
  45. package/dist/modules/logBox/logBox.d.ts +3 -0
  46. package/dist/modules/logBox/logBox.d.ts.map +1 -0
  47. package/dist/modules/logBox/logBox.js +234 -0
  48. package/dist/modules/logBox/logBox.js.map +1 -0
  49. package/dist/modules/navigation/navigation.d.ts.map +1 -1
  50. package/dist/modules/navigation/navigation.js +130 -42
  51. package/dist/modules/navigation/navigation.js.map +1 -1
  52. package/dist/modules/network/network.d.ts.map +1 -1
  53. package/dist/modules/network/network.js +262 -67
  54. package/dist/modules/network/network.js.map +1 -1
  55. package/dist/modules/network/types.d.ts +35 -3
  56. package/dist/modules/network/types.d.ts.map +1 -1
  57. package/dist/modules/reactQuery/reactQuery.d.ts.map +1 -1
  58. package/dist/modules/reactQuery/reactQuery.js +25 -15
  59. package/dist/modules/reactQuery/reactQuery.js.map +1 -1
  60. package/dist/modules/storage/storage.d.ts.map +1 -1
  61. package/dist/modules/storage/storage.js +23 -16
  62. package/dist/modules/storage/storage.js.map +1 -1
  63. package/dist/server/host/hostModule.d.ts.map +1 -1
  64. package/dist/server/host/hostModule.js +19 -1
  65. package/dist/server/host/hostModule.js.map +1 -1
  66. package/dist/server/host/tools/capture.d.ts.map +1 -1
  67. package/dist/server/host/tools/capture.js +111 -28
  68. package/dist/server/host/tools/capture.js.map +1 -1
  69. package/dist/server/host/tools/devices.js +1 -1
  70. package/dist/server/host/tools/devices.js.map +1 -1
  71. package/dist/server/host/tools/input.d.ts +3 -0
  72. package/dist/server/host/tools/input.d.ts.map +1 -1
  73. package/dist/server/host/tools/input.js +197 -5
  74. package/dist/server/host/tools/input.js.map +1 -1
  75. package/dist/server/host/tools/lifecycle.d.ts.map +1 -1
  76. package/dist/server/host/tools/lifecycle.js +5 -4
  77. package/dist/server/host/tools/lifecycle.js.map +1 -1
  78. package/dist/server/host/tools/symbolicate.d.ts +3 -0
  79. package/dist/server/host/tools/symbolicate.d.ts.map +1 -0
  80. package/dist/server/host/tools/symbolicate.js +199 -0
  81. package/dist/server/host/tools/symbolicate.js.map +1 -0
  82. package/dist/server/host/tools/tapFiber.d.ts +3 -0
  83. package/dist/server/host/tools/tapFiber.d.ts.map +1 -0
  84. package/dist/server/host/tools/tapFiber.js +89 -0
  85. package/dist/server/host/tools/tapFiber.js.map +1 -0
  86. package/dist/server/host/types.d.ts +14 -0
  87. package/dist/server/host/types.d.ts.map +1 -1
  88. package/dist/server/mcpServer.d.ts +6 -0
  89. package/dist/server/mcpServer.d.ts.map +1 -1
  90. package/dist/server/mcpServer.js +440 -93
  91. package/dist/server/mcpServer.js.map +1 -1
  92. package/package.json +1 -1
@@ -3,24 +3,163 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fiberTreeModule = void 0;
4
4
  const utils_1 = require("./utils");
5
5
  const DEFAULT_DEPTH = 10;
6
+ const QUERY_LIMIT_DEFAULT = 50;
7
+ const QUERY_LIMIT_MAX = 500;
8
+ const QUERY_DEFAULT_FIELDS = ['mcpId', 'name', 'testID'];
6
9
  const FIND_SCHEMA = {
7
10
  index: {
8
- description: 'Zero-based index when multiple components match (default: 0, i.e. first match)',
11
+ description: '0-based index when several components match (default: 0).',
9
12
  type: 'number',
10
13
  },
11
- mcpId: { description: 'data-mcp-id to search for', type: 'string' },
12
- name: { description: 'Component name to search for', type: 'string' },
13
- testID: { description: 'testID to search for', type: 'string' },
14
- text: { description: 'Text content to search for', type: 'string' },
14
+ mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
15
+ name: { description: 'Component name to match.', type: 'string' },
16
+ testID: { description: 'testID to match.', type: 'string' },
17
+ text: { description: 'Rendered text substring (not prop values).', type: 'string' },
15
18
  within: {
16
- description: 'Search within a parent component path. Use "/" for nesting, ":N" for index. E.g. "Checkbox/Pressable", "Button:1/View"',
19
+ description: 'Parent component path. "/" nests, ":N" picks index.',
20
+ examples: ['LoginForm', 'Button:1/Pressable', 'TabBar/TabBarItem:2'],
17
21
  type: 'string',
18
22
  },
19
23
  };
24
+ const resolveScreenFiber = (runtime) => {
25
+ const nav = runtime.navigationRef;
26
+ if (!nav || typeof nav.getCurrentRoute !== 'function')
27
+ return null;
28
+ const route = nav.getCurrentRoute();
29
+ const key = route && typeof route.key === 'string' ? route.key : undefined;
30
+ if (!key)
31
+ return null;
32
+ return (0, utils_1.findScreenFiberByRouteKey)(runtime.root, key);
33
+ };
34
+ const collectByScope = (fiber, scope, runtime) => {
35
+ switch (scope) {
36
+ case 'self':
37
+ return [fiber];
38
+ case 'parent':
39
+ return fiber.return ? [fiber.return] : [];
40
+ case 'ancestors':
41
+ return (0, utils_1.getAncestors)(fiber);
42
+ case 'children':
43
+ return (0, utils_1.getDirectChildren)(fiber);
44
+ case 'siblings':
45
+ return (0, utils_1.getSiblings)(fiber);
46
+ case 'nearest_host': {
47
+ const host = (0, utils_1.findHostFiber)(fiber);
48
+ return host ? [host] : [];
49
+ }
50
+ case 'screen': {
51
+ const screen = resolveScreenFiber(runtime);
52
+ if (!screen)
53
+ return [];
54
+ return (0, utils_1.findAllByQuery)(screen, {}).filter((f) => {
55
+ return f !== screen;
56
+ });
57
+ }
58
+ case 'descendants':
59
+ default:
60
+ return (0, utils_1.findAllByQuery)(fiber, {}).filter((f) => {
61
+ return f !== fiber;
62
+ });
63
+ }
64
+ };
65
+ const runQueryChain = (runtime, steps) => {
66
+ let current = [runtime.root];
67
+ for (const step of steps) {
68
+ const scope = step.scope ?? 'descendants';
69
+ const seen = new Set();
70
+ const collected = [];
71
+ for (const fiber of current) {
72
+ for (const candidate of collectByScope(fiber, scope, runtime)) {
73
+ if (!seen.has(candidate)) {
74
+ seen.add(candidate);
75
+ collected.push(candidate);
76
+ }
77
+ }
78
+ }
79
+ const filtered = collected.filter((f) => {
80
+ return (0, utils_1.matchesQuery)(f, step);
81
+ });
82
+ if (typeof step.index === 'number') {
83
+ const picked = filtered[step.index];
84
+ current = picked ? [picked] : [];
85
+ }
86
+ else {
87
+ current = filtered;
88
+ }
89
+ if (current.length === 0)
90
+ return [];
91
+ }
92
+ return current;
93
+ };
94
+ // Keep only fibers whose ancestor chain contains no other match. Removes
95
+ // wrapper cascades (PressableView → Pressable → View → RCTView) while keeping
96
+ // independent siblings with overlapping bounds (e.g. absolute-positioned
97
+ // overlays). Preserves original DFS order.
98
+ const dedupAncestors = (matches) => {
99
+ if (matches.length < 2)
100
+ return matches;
101
+ const matchSet = new Set(matches);
102
+ return matches.filter((fiber) => {
103
+ let p = fiber.return;
104
+ while (p) {
105
+ if (matchSet.has(p))
106
+ return false;
107
+ p = p.return;
108
+ }
109
+ return true;
110
+ });
111
+ };
112
+ // Window dimensions → physical-pixel bounds rectangle for `onlyVisible` filter.
113
+ const getVisibleRect = () => {
114
+ try {
115
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
116
+ const RN = require('react-native');
117
+ const { Dimensions, PixelRatio } = RN;
118
+ const window = Dimensions?.get?.('window');
119
+ const ratio = PixelRatio?.get?.() ?? 1;
120
+ if (!window || !Number.isFinite(window.width) || !Number.isFinite(window.height))
121
+ return null;
122
+ return {
123
+ height: window.height * ratio,
124
+ width: window.width * ratio,
125
+ };
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ };
131
+ const intersectsRect = (bounds, rect) => {
132
+ return (bounds.x + bounds.width > 0 &&
133
+ bounds.y + bounds.height > 0 &&
134
+ bounds.x < rect.width &&
135
+ bounds.y < rect.height);
136
+ };
20
137
  const fiberTreeModule = (options) => {
21
138
  if (options?.rootRef) {
22
139
  (0, utils_1.setRootRef)(options.rootRef);
23
140
  }
141
+ const navigationRef = options?.navigationRef;
142
+ // Root-version keyed cache for `runQueryChain`. When React commits, the
143
+ // HostRoot fiber swaps — so a mismatched pointer is proof the tree changed
144
+ // and the cached match set for the same steps is no longer valid.
145
+ // Enabled by default (cache: true); `cache: false` bypasses lookup + write.
146
+ let cacheRoot = null;
147
+ const cacheEntries = new Map();
148
+ const runCachedQuery = (runtime, steps, useCache) => {
149
+ if (!useCache)
150
+ return runQueryChain(runtime, steps);
151
+ if (cacheRoot !== runtime.root) {
152
+ cacheRoot = runtime.root;
153
+ cacheEntries.clear();
154
+ }
155
+ const key = JSON.stringify(steps);
156
+ const hit = cacheEntries.get(key);
157
+ if (hit)
158
+ return hit;
159
+ const result = runQueryChain(runtime, steps);
160
+ cacheEntries.set(key, result);
161
+ return result;
162
+ };
24
163
  const findInRoot = (root, segment) => {
25
164
  if (!root)
26
165
  return null;
@@ -78,44 +217,69 @@ const fiberTreeModule = (options) => {
78
217
  return null;
79
218
  };
80
219
  return {
81
- description: `React component tree inspection and interaction.
220
+ description: `React fiber tree inspection and interaction.
82
221
 
83
- ## Finding components
84
- - find_all with hasProps: ["onPress"] find all pressable elements
85
- - find_all with hasProps: ["onChangeText"] — find all text inputs
86
- - find_all with name: "Button" find by component name
87
- - get_component with mcpId: "Button:screens/Login:42" find by stable ID
222
+ SCOPES (query steps)
223
+ descendants (default) / children / parent / ancestors / siblings / self
224
+ / screen / nearest_host.
225
+ · screen descendants of the currently focused React Navigation
226
+ screen fiber. Available when the library was initialized with a
227
+ navigationRef. Lets a first step skip "find current screen first".
228
+ · nearest_host — walks down to the first mounted HOST_COMPONENT
229
+ fiber. Useful before call_ref (focus/blur/measure) which require
230
+ a host instance.
88
231
 
89
- ## Interacting
90
- - invoke with callback: "onPress"press a button
91
- - invoke with callback: "onChangeText", args: ["text"] type into input
92
- - invoke with callback: "onPress", args: [true] toggle checkbox
93
- - call_ref with method: "focus" focus an input
232
+ STEP CRITERIA
233
+ name / mcpId / testIDstrict equality.
234
+ text substring match in RENDERED text only (not prop values).
235
+ hasProps array of prop names that must exist.
236
+ props map of prop matcher:
237
+ · primitive → strict equality.
238
+ · { contains: "X" } / { regex: "Y" } → match via String(value); primitives only by default.
239
+ · add deep: true → also JSON-serialize objects/arrays and match inside.
240
+ any — array of sub-criteria; OR semantics.
241
+ Example: { any: [{ name: "Pressable" }, { name: "TouchableOpacity" }] }.
242
+ not — nested criteria; excludes fibers that match the inner query.
243
+ Composes with the others: { hasProps: ["onPress"], not: { testID: "loading" } }.
244
+ Accepts an array for multi-pattern exclusion:
245
+ { not: [{ name: "Pressable" }, { testID: "loading" }] }.
246
+ index — pick N-th match from this step; otherwise all matches fan out into the next step.
94
247
 
95
- ## Scoping with "within"
96
- - within: "LoginForm" — search only inside LoginForm
97
- - within: "Button:1/Pressable" nested path with index
248
+ SELECT (output fields)
249
+ Default ["mcpId", "name", "testID"] props and bounds are opt-in.
250
+ bounds: { x, y, width, height, centerX, centerY } in PHYSICAL pixels,
251
+ top-left origin. null when the fiber has no mounted host view. centerX/
252
+ centerY feed straight into host__tap.
253
+ props: full serialized props (heavy). Pair with propsInclude:
254
+ ["key1","key2"] to keep only the props you actually need and avoid
255
+ pulling large style maps, data arrays, or nested element trees.
98
256
 
99
- ## Coordinates (host__tap targets)
100
- - Pass \`select: ["mcpId", "name", "bounds"]\` to find_all to get tap coordinates without heavy props.
101
- - bounds = {x, y, width, height, centerX, centerY} in PHYSICAL PIXELS.
102
- - Use bounds.centerX/centerY directly with host__tap no scaling needed.
103
- - bounds is null only when the component has no host view (unmounted, virtualized off-screen).
257
+ RESPONSE
258
+ { matches: [...], total, truncated? } total is the unrestricted match
259
+ count; when the result exceeds limit (default 50, max 500) truncated:
260
+ true is added and matches contains the first limit items in DFS order.
261
+ Narrow the query rather than cranking limit.
104
262
 
105
- ## Saving tokens with select
106
- - \`select\` controls which fields appear in each result: mcpId, name, testID, props, bounds.
107
- - Default (no select): all fields except bounds. Include "bounds" for tap coordinates.
108
- - Omit "props" to cut response size ~90% when you only need names/IDs.
263
+ By default wrapper cascades are deduped: a fiber is hidden when any of
264
+ its ancestors is also a match, so PressableView Pressable View
265
+ RCTView collapses to the topmost PressableView. Independent siblings
266
+ are kept. Pass dedup: false to see every layer.
109
267
 
110
- ## Tips
111
- - mcpId is stable across renders (format: ComponentName:file:line)
112
- - Use find_all first to discover available components, then invoke or host__tap them
113
- - host__tap with bounds.centerX/centerY tests the real OS gesture pipeline; invoke bypasses it and calls the prop directly (faster, immune to overlay/gesture-arbitration issues, but doesn't exercise touch handlers)
114
- - Use screenshot after interactions to verify results`,
268
+ TIPS
269
+ mcpId format "ComponentName:file:line" — stable across renders.
270
+ Use query to locate, then invoke (bypasses gesture pipeline) or host__tap
271
+ with bounds (real OS touch) to act. For one-shot real taps, tap_fiber
272
+ collapses both steps into a single call.
273
+ When stepping up via scope: "ancestors", prefer filtering by name (or
274
+ testID/mcpId) over guessing an index — ancestors count is brittle and
275
+ varies across RN versions.
276
+ \`text\` matches RENDERED text only — Text children content, not prop
277
+ values. To match "placeholder: Search" use \`props: { placeholder:
278
+ { contains: "Search" } }\`.`,
115
279
  name: 'fiber_tree',
116
280
  tools: {
117
281
  call_ref: {
118
- description: 'Call a method on the native ref/instance of a component (e.g. focus, blur, measure). Use get_ref_methods first to see available methods.',
282
+ description: "Call a method on a component's native ref (focus, blur, measure, …). Use get_ref_methods first to see what's available.",
119
283
  handler: (args) => {
120
284
  const rootError = requireRoot();
121
285
  if (rootError)
@@ -154,76 +318,16 @@ const fiberTreeModule = (options) => {
154
318
  },
155
319
  inputSchema: {
156
320
  ...FIND_SCHEMA,
157
- args: { description: 'Arguments to pass to the method as array', type: 'array' },
321
+ args: { description: 'Arguments passed to the method.', type: 'array' },
158
322
  method: {
159
- description: 'Method name to call (e.g. "focus", "blur", "measure")',
323
+ description: 'Method name to call.',
324
+ examples: ['focus', 'blur', 'measure'],
160
325
  type: 'string',
161
326
  },
162
327
  },
163
328
  },
164
- find_all: {
165
- description: 'Find all components matching a query (by testID, name, text, or props presence). Supports within for scoped search. Use select to control which fields are returned — e.g. select: ["mcpId", "name", "bounds"] for tap targeting without heavy props.',
166
- handler: async (args) => {
167
- const rootError = requireRoot();
168
- if (rootError)
169
- return rootError;
170
- let root = (0, utils_1.getFiberRoot)();
171
- if (args.within) {
172
- const path = args.within.split('/');
173
- for (const segment of path) {
174
- root = findInRoot(root, segment);
175
- if (!root)
176
- return { error: `Parent "${args.within}" not found` };
177
- }
178
- }
179
- const query = {
180
- hasProps: args.hasProps,
181
- mcpId: args.mcpId,
182
- name: args.name,
183
- testID: args.testID,
184
- text: args.text,
185
- };
186
- const defaultFields = ['mcpId', 'name', 'props', 'testID'];
187
- const fields = new Set(Array.isArray(args.select) ? args.select : defaultFields);
188
- const fibers = (0, utils_1.findAllByQuery)(root, query);
189
- return Promise.all(fibers.map(async (fiber) => {
190
- const result = {};
191
- if (fields.has('bounds')) {
192
- result.bounds = await (0, utils_1.measureFiber)(fiber);
193
- }
194
- if (fields.has('mcpId')) {
195
- result.mcpId = fiber.memoizedProps?.['data-mcp-id'];
196
- }
197
- if (fields.has('name')) {
198
- result.name = (0, utils_1.getComponentName)(fiber);
199
- }
200
- if (fields.has('props')) {
201
- result.props = (0, utils_1.serializeProps)(fiber.memoizedProps);
202
- }
203
- if (fields.has('testID')) {
204
- result.testID = fiber.memoizedProps?.testID;
205
- }
206
- return result;
207
- }));
208
- },
209
- inputSchema: {
210
- hasProps: {
211
- description: 'Filter by props presence (array of prop names)',
212
- type: 'array',
213
- },
214
- mcpId: { description: 'data-mcp-id to match', type: 'string' },
215
- name: { description: 'Component name to match', type: 'string' },
216
- select: {
217
- description: 'Fields to include in each result. Available: mcpId, name, testID, props, bounds. Default (when omitted): all except bounds. Include "bounds" for physical-pixel tap coordinates. Omit "props" to save tokens.',
218
- type: 'array',
219
- },
220
- testID: { description: 'testID to match', type: 'string' },
221
- text: { description: 'Text content to match (substring)', type: 'string' },
222
- within: FIND_SCHEMA.within,
223
- },
224
- },
225
329
  get_children: {
226
- description: 'Get children of a component found by testID, name, or text',
330
+ description: 'Get the children subtree of a single component.',
227
331
  handler: (args) => {
228
332
  const rootError = requireRoot();
229
333
  if (rootError)
@@ -237,11 +341,11 @@ const fiberTreeModule = (options) => {
237
341
  },
238
342
  inputSchema: {
239
343
  ...FIND_SCHEMA,
240
- depth: { description: 'Max depth to traverse (default: 10)', type: 'number' },
344
+ depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
241
345
  },
242
346
  },
243
347
  get_component: {
244
- description: 'Find a component by testID, name, or text and return its details. Use select to control root-level fields — e.g. select: ["name", "bounds"] to include tap coordinates without props.',
348
+ description: 'Find one component and return its details with children subtree (deep inspection). Use `query` for a flat list of matches.',
245
349
  handler: async (args) => {
246
350
  const rootError = requireRoot();
247
351
  if (rootError)
@@ -279,19 +383,20 @@ const fiberTreeModule = (options) => {
279
383
  return serialized;
280
384
  },
281
385
  inputSchema: {
282
- depth: { description: 'Max depth to traverse children (default: 3)', type: 'number' },
283
- mcpId: { description: 'data-mcp-id to search for', type: 'string' },
284
- name: { description: 'Component name to search for', type: 'string' },
386
+ depth: { description: 'Max child traversal depth (default: 10).', type: 'number' },
387
+ mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
388
+ name: { description: 'Component name to match.', type: 'string' },
285
389
  select: {
286
- description: 'Fields to include on root node. Available: name, props, bounds. Include "bounds" for tap coordinates. Omit "props" to save tokens. Children are always included.',
390
+ description: 'Fields to include on the root node. Available: name, props, bounds. Children are always included.',
391
+ examples: [['name', 'bounds']],
287
392
  type: 'array',
288
393
  },
289
- testID: { description: 'testID to search for', type: 'string' },
290
- text: { description: 'Text content to search for', type: 'string' },
394
+ testID: { description: 'testID to match.', type: 'string' },
395
+ text: { description: 'Rendered text substring.', type: 'string' },
291
396
  },
292
397
  },
293
398
  get_props: {
294
- description: 'Get all props of a component found by testID, name, or text',
399
+ description: 'Get all props of one component.',
295
400
  handler: (args) => {
296
401
  const rootError = requireRoot();
297
402
  if (rootError)
@@ -307,7 +412,7 @@ const fiberTreeModule = (options) => {
307
412
  inputSchema: FIND_SCHEMA,
308
413
  },
309
414
  get_ref_methods: {
310
- description: 'List available methods on the native ref/instance of a component',
415
+ description: "List available methods on a component's native ref.",
311
416
  handler: (args) => {
312
417
  const rootError = requireRoot();
313
418
  if (rootError)
@@ -327,7 +432,7 @@ const fiberTreeModule = (options) => {
327
432
  inputSchema: FIND_SCHEMA,
328
433
  },
329
434
  get_tree: {
330
- description: 'Get the React component tree. Returns component names, types, props, and testIDs.',
435
+ description: 'Dump the full React component tree from the root fiber.',
331
436
  handler: (args) => {
332
437
  const rootError = requireRoot();
333
438
  if (rootError)
@@ -337,11 +442,11 @@ const fiberTreeModule = (options) => {
337
442
  return (0, utils_1.serializeFiber)(root, depth);
338
443
  },
339
444
  inputSchema: {
340
- depth: { description: 'Max depth to traverse (default: 3)', type: 'number' },
445
+ depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
341
446
  },
342
447
  },
343
448
  invoke: {
344
- description: 'Call any callback prop on a component found by testID, name, or text. Use this to simulate press, scroll, text input, or any other interaction. Bypasses the OS gesture pipeline faster and immune to overlay/gesture-arbitration issues, but does not exercise touch handlers. Prefer host__tap (with fiber_tree bounds) when you want to test the real user touch path.',
449
+ description: "Call a prop's callback function directly from JS. For simulating a user tap, prefer host__tap_fiber it runs the real OS gesture pipeline so Pressable feedback, gesture responders, and hit-test behave as under a real finger. invoke still works for any callback when you specifically want the JS-only path (component off-screen, skipping the gesture recognizer, or driving a non-gesture prop), but it is not the default for user-behavior simulation.",
345
450
  handler: (args) => {
346
451
  const rootError = requireRoot();
347
452
  if (rootError)
@@ -367,15 +472,149 @@ const fiberTreeModule = (options) => {
367
472
  inputSchema: {
368
473
  ...FIND_SCHEMA,
369
474
  args: {
370
- description: 'Arguments to pass to the callback as array (e.g. [true] or ["text"])',
475
+ description: 'Arguments passed to the callback.',
476
+ examples: [[true], ['text']],
371
477
  type: 'array',
372
478
  },
373
479
  callback: {
374
- description: 'Name of the callback prop to call (e.g. "onPress", "onChangeText")',
480
+ description: 'Callback prop name.',
481
+ examples: ['onSkip', 'onUpdate', 'onCompleted'],
375
482
  type: 'string',
376
483
  },
377
484
  },
378
485
  },
486
+ query: {
487
+ description: 'Chain-based fiber search. Each step narrows the result set via `scope` + criteria; multiple matches fan out into the next step. Returns { matches, total, truncated? }. See the module description for scope, criteria, select and response reference.',
488
+ handler: async (args) => {
489
+ const rootError = requireRoot();
490
+ if (rootError)
491
+ return rootError;
492
+ const root = (0, utils_1.getFiberRoot)();
493
+ const steps = args.steps;
494
+ if (!Array.isArray(steps) || steps.length === 0) {
495
+ return { error: 'query requires a non-empty `steps` array' };
496
+ }
497
+ const limit = typeof args.limit === 'number' && args.limit > 0
498
+ ? Math.min(Math.floor(args.limit), QUERY_LIMIT_MAX)
499
+ : QUERY_LIMIT_DEFAULT;
500
+ const dedup = args.dedup !== false;
501
+ const useCache = args.cache !== false;
502
+ const onlyVisible = args.onlyVisible === true;
503
+ const runtime = { navigationRef, root };
504
+ const rawMatches = runCachedQuery(runtime, steps, useCache);
505
+ let all = dedup ? dedupAncestors(rawMatches) : rawMatches;
506
+ let visibleRect = null;
507
+ const boundsCache = new Map();
508
+ const measure = async (fiber) => {
509
+ if (boundsCache.has(fiber))
510
+ return boundsCache.get(fiber) ?? null;
511
+ const b = await (0, utils_1.measureFiber)(fiber);
512
+ boundsCache.set(fiber, b);
513
+ return b;
514
+ };
515
+ if (onlyVisible) {
516
+ visibleRect = getVisibleRect();
517
+ if (visibleRect) {
518
+ const rect = visibleRect;
519
+ const measured = await Promise.all(all.map(async (fiber) => {
520
+ return { bounds: await measure(fiber), fiber };
521
+ }));
522
+ all = measured
523
+ .filter(({ bounds }) => {
524
+ return bounds && intersectsRect(bounds, rect);
525
+ })
526
+ .map(({ fiber }) => {
527
+ return fiber;
528
+ });
529
+ }
530
+ }
531
+ const total = all.length;
532
+ const truncated = total > limit;
533
+ const picked = truncated ? all.slice(0, limit) : all;
534
+ const fields = new Set(Array.isArray(args.select) ? args.select : QUERY_DEFAULT_FIELDS);
535
+ const propsInclude = Array.isArray(args.propsInclude)
536
+ ? new Set(args.propsInclude)
537
+ : null;
538
+ const matches = await Promise.all(picked.map(async (fiber) => {
539
+ const result = {};
540
+ if (fields.has('bounds')) {
541
+ result.bounds = await measure(fiber);
542
+ }
543
+ if (fields.has('mcpId')) {
544
+ result.mcpId = fiber.memoizedProps?.['data-mcp-id'];
545
+ }
546
+ if (fields.has('name')) {
547
+ result.name = (0, utils_1.getComponentName)(fiber);
548
+ }
549
+ if (fields.has('props')) {
550
+ const full = (0, utils_1.serializeProps)(fiber.memoizedProps);
551
+ if (propsInclude) {
552
+ const filtered = {};
553
+ for (const key of propsInclude) {
554
+ if (key in full)
555
+ filtered[key] = full[key];
556
+ }
557
+ result.props = filtered;
558
+ }
559
+ else {
560
+ result.props = full;
561
+ }
562
+ }
563
+ if (fields.has('testID')) {
564
+ result.testID = fiber.memoizedProps?.testID;
565
+ }
566
+ return result;
567
+ }));
568
+ const response = { matches, total };
569
+ if (truncated)
570
+ response.truncated = true;
571
+ return response;
572
+ },
573
+ inputSchema: {
574
+ cache: {
575
+ description: 'Reuse the match set when the React tree has not committed since the previous identical steps — detected via fiber root pointer equality. Default true; pass false to force a fresh traversal.',
576
+ type: 'boolean',
577
+ },
578
+ dedup: {
579
+ description: 'Drop wrapper cascades — a fiber is removed when any of its ancestors is also in the match set (PressableView → Pressable → View → RCTView collapses to the topmost). Independent siblings with overlapping bounds are kept. Default true; pass false to keep every match.',
580
+ type: 'boolean',
581
+ },
582
+ limit: {
583
+ description: `Max matches to return (default ${QUERY_LIMIT_DEFAULT}, max ${QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
584
+ type: 'number',
585
+ },
586
+ onlyVisible: {
587
+ description: 'Drop matches whose measured bounds do not intersect the current window rectangle (physical pixels). Also drops fibers with no measurable host view — usually virtualized or unmounted. Halves results on long lists.',
588
+ type: 'boolean',
589
+ },
590
+ propsInclude: {
591
+ description: 'When select includes "props", keep only these prop names. Unknown keys are silently dropped. Omit for full serialization.',
592
+ examples: [
593
+ ['placeholder', 'value'],
594
+ ['title', 'disabled'],
595
+ ],
596
+ type: 'array',
597
+ },
598
+ select: {
599
+ description: `Output fields: mcpId, name, testID, props, bounds. Default ${JSON.stringify(QUERY_DEFAULT_FIELDS)}.`,
600
+ examples: [
601
+ ['mcpId', 'name', 'bounds'],
602
+ ['mcpId', 'name', 'props'],
603
+ ],
604
+ type: 'array',
605
+ },
606
+ steps: {
607
+ description: 'Ordered steps: [{ scope?, name?, mcpId?, testID?, text?, hasProps?, props?, index? }]. See module description for full semantics.',
608
+ examples: [
609
+ [{ hasProps: ['onPress'] }],
610
+ [{ name: 'HomeScreen' }, { name: 'ProductCard' }],
611
+ [{ testID: 'favorite-icon' }, { index: 0, name: 'ProductCard', scope: 'ancestors' }],
612
+ [{ props: { placeholder: { contains: 'Search' } } }],
613
+ ],
614
+ type: 'array',
615
+ },
616
+ },
617
+ },
379
618
  },
380
619
  };
381
620
  };