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.
- package/README.md +20 -11
- package/dist/bin/ios-hid +0 -0
- package/dist/client/contexts/McpContext/McpProvider.d.ts.map +1 -1
- package/dist/client/contexts/McpContext/McpProvider.js +6 -3
- package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
- package/dist/modules/alert/alert.d.ts.map +1 -1
- package/dist/modules/alert/alert.js +6 -5
- package/dist/modules/alert/alert.js.map +1 -1
- package/dist/modules/console/console.d.ts.map +1 -1
- package/dist/modules/console/console.js +18 -13
- package/dist/modules/console/console.js.map +1 -1
- package/dist/modules/device/device.d.ts.map +1 -1
- package/dist/modules/device/device.js +23 -27
- package/dist/modules/device/device.js.map +1 -1
- package/dist/modules/errors/errors.d.ts.map +1 -1
- package/dist/modules/errors/errors.js +92 -11
- package/dist/modules/errors/errors.js.map +1 -1
- package/dist/modules/errors/types.d.ts +8 -0
- package/dist/modules/errors/types.d.ts.map +1 -1
- package/dist/modules/fiberTree/fiberTree.d.ts +4 -0
- package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
- package/dist/modules/fiberTree/fiberTree.js +353 -114
- package/dist/modules/fiberTree/fiberTree.js.map +1 -1
- package/dist/modules/fiberTree/index.d.ts +1 -0
- package/dist/modules/fiberTree/index.d.ts.map +1 -1
- package/dist/modules/fiberTree/index.js +14 -1
- package/dist/modules/fiberTree/index.js.map +1 -1
- package/dist/modules/fiberTree/types.d.ts +40 -0
- package/dist/modules/fiberTree/types.d.ts.map +1 -1
- package/dist/modules/fiberTree/utils.d.ts +13 -0
- package/dist/modules/fiberTree/utils.d.ts.map +1 -1
- package/dist/modules/fiberTree/utils.js +195 -23
- package/dist/modules/fiberTree/utils.js.map +1 -1
- package/dist/modules/i18next/i18next.d.ts.map +1 -1
- package/dist/modules/i18next/i18next.js +30 -16
- package/dist/modules/i18next/i18next.js.map +1 -1
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +3 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/logBox/index.d.ts +2 -0
- package/dist/modules/logBox/index.d.ts.map +1 -0
- package/dist/modules/logBox/index.js +6 -0
- package/dist/modules/logBox/index.js.map +1 -0
- package/dist/modules/logBox/logBox.d.ts +3 -0
- package/dist/modules/logBox/logBox.d.ts.map +1 -0
- package/dist/modules/logBox/logBox.js +234 -0
- package/dist/modules/logBox/logBox.js.map +1 -0
- package/dist/modules/navigation/navigation.d.ts.map +1 -1
- package/dist/modules/navigation/navigation.js +130 -42
- package/dist/modules/navigation/navigation.js.map +1 -1
- package/dist/modules/network/network.d.ts.map +1 -1
- package/dist/modules/network/network.js +262 -67
- package/dist/modules/network/network.js.map +1 -1
- package/dist/modules/network/types.d.ts +35 -3
- package/dist/modules/network/types.d.ts.map +1 -1
- package/dist/modules/reactQuery/reactQuery.d.ts.map +1 -1
- package/dist/modules/reactQuery/reactQuery.js +25 -15
- package/dist/modules/reactQuery/reactQuery.js.map +1 -1
- package/dist/modules/storage/storage.d.ts.map +1 -1
- package/dist/modules/storage/storage.js +23 -16
- package/dist/modules/storage/storage.js.map +1 -1
- package/dist/server/host/hostModule.d.ts.map +1 -1
- package/dist/server/host/hostModule.js +19 -1
- package/dist/server/host/hostModule.js.map +1 -1
- package/dist/server/host/tools/capture.d.ts.map +1 -1
- package/dist/server/host/tools/capture.js +111 -28
- package/dist/server/host/tools/capture.js.map +1 -1
- package/dist/server/host/tools/devices.js +1 -1
- package/dist/server/host/tools/devices.js.map +1 -1
- package/dist/server/host/tools/input.d.ts +3 -0
- package/dist/server/host/tools/input.d.ts.map +1 -1
- package/dist/server/host/tools/input.js +197 -5
- package/dist/server/host/tools/input.js.map +1 -1
- package/dist/server/host/tools/lifecycle.d.ts.map +1 -1
- package/dist/server/host/tools/lifecycle.js +5 -4
- package/dist/server/host/tools/lifecycle.js.map +1 -1
- package/dist/server/host/tools/symbolicate.d.ts +3 -0
- package/dist/server/host/tools/symbolicate.d.ts.map +1 -0
- package/dist/server/host/tools/symbolicate.js +199 -0
- package/dist/server/host/tools/symbolicate.js.map +1 -0
- package/dist/server/host/tools/tapFiber.d.ts +3 -0
- package/dist/server/host/tools/tapFiber.d.ts.map +1 -0
- package/dist/server/host/tools/tapFiber.js +89 -0
- package/dist/server/host/tools/tapFiber.js.map +1 -0
- package/dist/server/host/types.d.ts +14 -0
- package/dist/server/host/types.d.ts.map +1 -1
- package/dist/server/mcpServer.d.ts +6 -0
- package/dist/server/mcpServer.d.ts.map +1 -1
- package/dist/server/mcpServer.js +440 -93
- package/dist/server/mcpServer.js.map +1 -1
- 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: '
|
|
11
|
+
description: '0-based index when several components match (default: 0).',
|
|
9
12
|
type: 'number',
|
|
10
13
|
},
|
|
11
|
-
mcpId: { description: 'data-mcp-id to
|
|
12
|
-
name: { description: 'Component name to
|
|
13
|
-
testID: { description: 'testID to
|
|
14
|
-
text: { description: '
|
|
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: '
|
|
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
|
|
220
|
+
description: `React fiber tree inspection and interaction.
|
|
82
221
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
232
|
+
STEP CRITERIA
|
|
233
|
+
name / mcpId / testID — strict 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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
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
|
|
321
|
+
args: { description: 'Arguments passed to the method.', type: 'array' },
|
|
158
322
|
method: {
|
|
159
|
-
description: 'Method name to call
|
|
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
|
|
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
|
|
344
|
+
depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
|
|
241
345
|
},
|
|
242
346
|
},
|
|
243
347
|
get_component: {
|
|
244
|
-
description: 'Find
|
|
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
|
|
283
|
-
mcpId: { description: 'data-mcp-id to
|
|
284
|
-
name: { description: 'Component name to
|
|
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.
|
|
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
|
|
290
|
-
text: { description: '
|
|
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
|
|
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:
|
|
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: '
|
|
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
|
|
445
|
+
depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
|
|
341
446
|
},
|
|
342
447
|
},
|
|
343
448
|
invoke: {
|
|
344
|
-
description:
|
|
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
|
|
475
|
+
description: 'Arguments passed to the callback.',
|
|
476
|
+
examples: [[true], ['text']],
|
|
371
477
|
type: 'array',
|
|
372
478
|
},
|
|
373
479
|
callback: {
|
|
374
|
-
description: '
|
|
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
|
};
|