react-native-mcp-kit 2.3.1 → 3.0.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 +12 -7
- package/dist/babel/stripPlugin.d.ts.map +1 -1
- package/dist/babel/stripPlugin.js +18 -3
- package/dist/babel/stripPlugin.js.map +1 -1
- package/dist/babel/testIdPlugin.d.ts.map +1 -1
- package/dist/babel/testIdPlugin.js +466 -7
- package/dist/babel/testIdPlugin.js.map +1 -1
- 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 +0 -6
- package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
- package/dist/client/contexts/McpContext/types.d.ts +0 -2
- package/dist/client/contexts/McpContext/types.d.ts.map +1 -1
- package/dist/client/core/McpClient.d.ts +0 -2
- package/dist/client/core/McpClient.d.ts.map +1 -1
- package/dist/client/core/McpClient.js +1 -17
- package/dist/client/core/McpClient.js.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -3
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/fiberTree/fiberTree.d.ts +17 -0
- package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
- package/dist/modules/fiberTree/fiberTree.js +554 -2
- package/dist/modules/fiberTree/fiberTree.js.map +1 -1
- package/dist/modules/fiberTree/utils.d.ts +1 -0
- package/dist/modules/fiberTree/utils.d.ts.map +1 -1
- package/dist/modules/fiberTree/utils.js +7 -6
- package/dist/modules/fiberTree/utils.js.map +1 -1
- package/dist/server/bridge.d.ts +0 -1
- package/dist/server/bridge.d.ts.map +1 -1
- package/dist/server/bridge.js +0 -15
- package/dist/server/bridge.js.map +1 -1
- package/dist/server/canonicalize.d.ts +8 -0
- package/dist/server/canonicalize.d.ts.map +1 -0
- package/dist/server/canonicalize.js +23 -0
- package/dist/server/canonicalize.js.map +1 -0
- package/dist/server/host/tools/connectionStatus.d.ts +9 -0
- package/dist/server/host/tools/connectionStatus.d.ts.map +1 -0
- package/dist/server/host/tools/connectionStatus.js +39 -0
- package/dist/server/host/tools/connectionStatus.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/inputSchemaToZod.d.ts +19 -0
- package/dist/server/inputSchemaToZod.d.ts.map +1 -0
- package/dist/server/inputSchemaToZod.js +89 -0
- package/dist/server/inputSchemaToZod.js.map +1 -0
- package/dist/server/mcpServer.d.ts.map +1 -1
- package/dist/server/mcpServer.js +3 -86
- package/dist/server/mcpServer.js.map +1 -1
- package/dist/shared/protocol.d.ts +9 -10
- package/dist/shared/protocol.d.ts.map +1 -1
- package/dist/shared/protocol.js +9 -1
- package/dist/shared/protocol.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,6 +10,475 @@ const WAIT_TIMEOUT_DEFAULT = 10_000;
|
|
|
10
10
|
const WAIT_TIMEOUT_MAX = 60_000;
|
|
11
11
|
const WAIT_INTERVAL_DEFAULT = 300;
|
|
12
12
|
const WAIT_INTERVAL_MIN = 100;
|
|
13
|
+
// Parse a name pattern: `/regex/flags` → RegExp matcher; anything else →
|
|
14
|
+
// exact-string matcher. Same convention as log_box__ignore.
|
|
15
|
+
const parseNamePattern = (raw) => {
|
|
16
|
+
const m = raw.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
17
|
+
if (m && m[1] !== undefined) {
|
|
18
|
+
try {
|
|
19
|
+
const rx = new RegExp(m[1], m[2] ?? '');
|
|
20
|
+
return (n) => {
|
|
21
|
+
return rx.test(n);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return (n) => {
|
|
26
|
+
return n === raw;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return (n) => {
|
|
31
|
+
return n === raw;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const HOOK_DEFAULT_MAX_DEPTH = 3;
|
|
35
|
+
// Try to treat `current` as a React component / native view instance and
|
|
36
|
+
// collapse it to a compact identifier. Native views expose
|
|
37
|
+
// `_internalFiberInstanceHandleDEV` / `_reactInternals`; class instances
|
|
38
|
+
// expose `_reactInternals`. If we find a fiber, we surface its mcpId /
|
|
39
|
+
// testID / component name so the agent can follow up via `query` if needed.
|
|
40
|
+
// If it doesn't look like a component, return null to signal "serialize
|
|
41
|
+
// normally".
|
|
42
|
+
const resolveComponentRef = (current) => {
|
|
43
|
+
if (current === null || current === undefined)
|
|
44
|
+
return null;
|
|
45
|
+
if (typeof current !== 'object')
|
|
46
|
+
return null;
|
|
47
|
+
const obj = current;
|
|
48
|
+
// Pick up a fiber from the typical internal fields used by RN / React.
|
|
49
|
+
const fiber = obj._reactInternals ??
|
|
50
|
+
obj._reactInternalFiber ??
|
|
51
|
+
obj._internalFiberInstanceHandleDEV ??
|
|
52
|
+
obj._internalInstanceHandle;
|
|
53
|
+
const hasNativeTag = '_nativeTag' in obj;
|
|
54
|
+
if (!fiber && !hasNativeTag)
|
|
55
|
+
return null;
|
|
56
|
+
const out = { __componentRef: true };
|
|
57
|
+
if (fiber) {
|
|
58
|
+
const props = fiber.memoizedProps;
|
|
59
|
+
const mcpId = props?.['data-mcp-id'];
|
|
60
|
+
const testID = props?.testID;
|
|
61
|
+
if (mcpId)
|
|
62
|
+
out.mcpId = mcpId;
|
|
63
|
+
if (testID)
|
|
64
|
+
out.testID = testID;
|
|
65
|
+
const typeName = fiber.type?.displayName ??
|
|
66
|
+
fiber.type?.name;
|
|
67
|
+
if (typeName)
|
|
68
|
+
out.componentName = typeName;
|
|
69
|
+
}
|
|
70
|
+
if (hasNativeTag) {
|
|
71
|
+
out.nativeTag = obj._nativeTag;
|
|
72
|
+
const viewConfig = obj.viewConfig;
|
|
73
|
+
if (viewConfig?.uiViewClassName)
|
|
74
|
+
out.viewClass = viewConfig.uiViewClassName;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
// Recognise React's effect-record shape: `{ tag: number, create: function,
|
|
79
|
+
// deps: null | unknown[] }` with optional `inst` / `destroy` / `next`.
|
|
80
|
+
// useState / useReducer / useContext memoizedState values of this exact
|
|
81
|
+
// shape in real user code are astronomically unlikely, so we treat this as
|
|
82
|
+
// a reliable "definitely not a state slot" signal.
|
|
83
|
+
const looksLikeEffectRecord = (raw) => {
|
|
84
|
+
if (!raw || typeof raw !== 'object')
|
|
85
|
+
return false;
|
|
86
|
+
const r = raw;
|
|
87
|
+
return (typeof r.tag === 'number' &&
|
|
88
|
+
typeof r.create === 'function' &&
|
|
89
|
+
(r.deps === null || r.deps === undefined || Array.isArray(r.deps)));
|
|
90
|
+
};
|
|
91
|
+
// Recognise the useRef shape: `{ current: X }` with NO other keys. A useState
|
|
92
|
+
// value that is literally an object whose sole own-key is "current" is so
|
|
93
|
+
// improbable in real code that we treat it as a reliable "this slot is a
|
|
94
|
+
// ref, not a state" signal — lets State/Custom skip ref slots that leaked in
|
|
95
|
+
// through custom-hook internals.
|
|
96
|
+
const looksLikeRefShape = (raw) => {
|
|
97
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
98
|
+
return false;
|
|
99
|
+
const keys = Object.keys(raw);
|
|
100
|
+
return keys.length === 1 && keys[0] === 'current';
|
|
101
|
+
};
|
|
102
|
+
// Shape-verify a hook slot's memoizedState against its expected kind. When
|
|
103
|
+
// a custom hook internally uses multiple built-in hooks, our static metadata
|
|
104
|
+
// understates the number of slots — every subsequent pairing drifts. By
|
|
105
|
+
// requiring a structural match before consuming a metadata entry we can
|
|
106
|
+
// swallow "internal" slots and keep the rest aligned. Permissive kinds
|
|
107
|
+
// (State / Reducer / Context / Custom) reject only obvious mis-matches
|
|
108
|
+
// (currently: the effect-record shape).
|
|
109
|
+
const shapeMatchesKind = (raw, kind) => {
|
|
110
|
+
switch (kind) {
|
|
111
|
+
case 'Ref':
|
|
112
|
+
return !!raw && typeof raw === 'object' && 'current' in raw;
|
|
113
|
+
case 'Memo':
|
|
114
|
+
case 'Callback':
|
|
115
|
+
return Array.isArray(raw) && raw.length === 2 && (raw[1] === null || Array.isArray(raw[1]));
|
|
116
|
+
case 'Effect':
|
|
117
|
+
case 'LayoutEffect':
|
|
118
|
+
case 'InsertionEffect':
|
|
119
|
+
return looksLikeEffectRecord(raw);
|
|
120
|
+
case 'Transition':
|
|
121
|
+
return Array.isArray(raw) && raw.length === 2;
|
|
122
|
+
case 'State':
|
|
123
|
+
case 'Reducer':
|
|
124
|
+
case 'Context':
|
|
125
|
+
case 'Custom':
|
|
126
|
+
// Permissive but not blind — drop obvious effect-node and ref-shape
|
|
127
|
+
// slots so State/Custom metadata doesn't swallow internals of
|
|
128
|
+
// preceding custom hooks.
|
|
129
|
+
return !looksLikeEffectRecord(raw) && !looksLikeRefShape(raw);
|
|
130
|
+
default:
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const serializeHookValue = (raw, kind, maxDepth) => {
|
|
135
|
+
const walk = (v) => {
|
|
136
|
+
return (0, utils_1.serializeValue)(v, new WeakSet(), 0, maxDepth);
|
|
137
|
+
};
|
|
138
|
+
switch (kind) {
|
|
139
|
+
case 'Ref': {
|
|
140
|
+
if (!raw || typeof raw !== 'object' || !('current' in raw)) {
|
|
141
|
+
return walk(raw);
|
|
142
|
+
}
|
|
143
|
+
const current = raw.current;
|
|
144
|
+
const componentRef = resolveComponentRef(current);
|
|
145
|
+
if (componentRef) {
|
|
146
|
+
return { current: componentRef };
|
|
147
|
+
}
|
|
148
|
+
return { current: walk(current) };
|
|
149
|
+
}
|
|
150
|
+
case 'Memo':
|
|
151
|
+
case 'Callback':
|
|
152
|
+
if (Array.isArray(raw) && raw.length === 2) {
|
|
153
|
+
return { deps: raw[1], value: walk(raw[0]) };
|
|
154
|
+
}
|
|
155
|
+
return walk(raw);
|
|
156
|
+
case 'Effect':
|
|
157
|
+
case 'LayoutEffect':
|
|
158
|
+
case 'InsertionEffect': {
|
|
159
|
+
// React's hook slot for effects holds { tag, create, destroy, deps, next }.
|
|
160
|
+
// Only `deps` is safe and useful to surface.
|
|
161
|
+
const effect = raw;
|
|
162
|
+
return effect && typeof effect === 'object' && 'deps' in effect
|
|
163
|
+
? { deps: effect.deps ?? null }
|
|
164
|
+
: null;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return walk(raw);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Estimate how many slots in fiber.memoizedState a hook function consumes.
|
|
171
|
+
// Used by the slot-walker to advance past unannotated black-box library
|
|
172
|
+
// hooks (e.g. `useSelector` from react-redux) which our babel plugin couldn't
|
|
173
|
+
// expand statically — without this they'd consume only one fiber slot during
|
|
174
|
+
// alignment, drifting the rest of the metadata out of sync.
|
|
175
|
+
//
|
|
176
|
+
// Three cascading strategies, falling through on miss:
|
|
177
|
+
// 1. `fn.__mcp_hooks` recursive — accurate for any hook our plugin saw.
|
|
178
|
+
// Custom sub-entries recurse; built-in entries count as 1.
|
|
179
|
+
// 2. `fn.toString()` regex — counts `useXxx(` calls in the source. Works
|
|
180
|
+
// even after Metro bundling because hook references survive as
|
|
181
|
+
// property accesses (`(0, _react.useState)(...)`) — property names
|
|
182
|
+
// aren't mangled. Underestimates when nested customs themselves expand
|
|
183
|
+
// to multiple slots, but better than 1.
|
|
184
|
+
// 3. Default 1 — native functions, bound functions, or sources we can't
|
|
185
|
+
// parse. Same as the original behavior.
|
|
186
|
+
//
|
|
187
|
+
// Cached per-function via WeakMap so cost is paid once per hook fn per
|
|
188
|
+
// session.
|
|
189
|
+
const HOOK_SLOTS_CACHE = new WeakMap();
|
|
190
|
+
const HOOK_NAME_RE = /\buse[A-Z]\w*\s*\(/g;
|
|
191
|
+
const STRING_LITERAL_RE = /(['"`])(?:\\.|(?!\1).)*\1/g;
|
|
192
|
+
const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
193
|
+
const LINE_COMMENT_RE = /\/\/[^\n]*/g;
|
|
194
|
+
const countHookSlots = (fn, depth = 0, seen) => {
|
|
195
|
+
if (depth > 8)
|
|
196
|
+
return 1;
|
|
197
|
+
if (typeof fn !== 'function')
|
|
198
|
+
return 1;
|
|
199
|
+
const fnObj = fn;
|
|
200
|
+
const cached = HOOK_SLOTS_CACHE.get(fnObj);
|
|
201
|
+
if (cached !== undefined)
|
|
202
|
+
return cached;
|
|
203
|
+
const localSeen = seen ?? new WeakSet();
|
|
204
|
+
if (localSeen.has(fnObj))
|
|
205
|
+
return 1;
|
|
206
|
+
localSeen.add(fnObj);
|
|
207
|
+
// (1) Annotated metadata: recurse into sub-entries, summing their slot
|
|
208
|
+
// counts. Each built-in entry contributes 1; each Custom contributes
|
|
209
|
+
// however many its own fn does (recurse).
|
|
210
|
+
const annotated = fn.__mcp_hooks;
|
|
211
|
+
if (Array.isArray(annotated)) {
|
|
212
|
+
let total = 0;
|
|
213
|
+
for (const entry of annotated) {
|
|
214
|
+
if (entry && entry.kind === 'Custom' && typeof entry.fn === 'function') {
|
|
215
|
+
total += countHookSlots(entry.fn, depth + 1, localSeen);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
total += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const result = Math.max(total, 1);
|
|
222
|
+
HOOK_SLOTS_CACHE.set(fnObj, result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
// (2) toString-based parsing. Strip strings/comments first to avoid
|
|
226
|
+
// matching `'useState'` inside literals. Each `useXxx(` occurrence in
|
|
227
|
+
// remaining source counts as one hook call (≥ 1 slot). Custom hook calls
|
|
228
|
+
// we encounter here can't be resolved further (no scope binding from the
|
|
229
|
+
// outer function), so we bottom out at 1 slot per occurrence — still
|
|
230
|
+
// beats the original constant-1 fallback when the source has multiple
|
|
231
|
+
// hook calls (e.g. `useSyncExternalStoreWithSelector` internals).
|
|
232
|
+
let src;
|
|
233
|
+
try {
|
|
234
|
+
src = Function.prototype.toString.call(fn);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
HOOK_SLOTS_CACHE.set(fnObj, 1);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
if (!src || src.includes('[native code]')) {
|
|
241
|
+
HOOK_SLOTS_CACHE.set(fnObj, 1);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
const stripped = src
|
|
245
|
+
.replace(STRING_LITERAL_RE, '""')
|
|
246
|
+
.replace(BLOCK_COMMENT_RE, '')
|
|
247
|
+
.replace(LINE_COMMENT_RE, '');
|
|
248
|
+
HOOK_NAME_RE.lastIndex = 0;
|
|
249
|
+
const matches = stripped.match(HOOK_NAME_RE);
|
|
250
|
+
const result = matches ? matches.length : 1;
|
|
251
|
+
HOOK_SLOTS_CACHE.set(fnObj, result);
|
|
252
|
+
return result;
|
|
253
|
+
};
|
|
254
|
+
// Flatten a metadata array by recursively inlining custom-hook sub-metadata.
|
|
255
|
+
// Stops on cycles (hook references itself) and on Custom entries whose `fn`
|
|
256
|
+
// isn't annotated (library hooks that bypassed the babel plugin — usually
|
|
257
|
+
// pre-compiled node_modules). Such unannotated entries stay as single
|
|
258
|
+
// records and rely on shape-check for alignment.
|
|
259
|
+
//
|
|
260
|
+
// Custom entries with annotated `fn` produce TWO records: a parent (marked
|
|
261
|
+
// `expanded: true`, no slot consumption) followed by all flattened
|
|
262
|
+
// children. This keeps the call-site visible in the output — without it
|
|
263
|
+
// the agent would see e.g. `wrapperAnimStyle.areAnimationsActive` deep
|
|
264
|
+
// in `via:` but never the `wrapperAnimStyle = useAnimatedStyle(...)`
|
|
265
|
+
// invocation that owns those slots.
|
|
266
|
+
const flattenHookMeta = (meta, via = [], seen = new WeakSet(), maxDepth = Infinity) => {
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const entry of meta) {
|
|
269
|
+
const fn = entry.fn;
|
|
270
|
+
const sub = fn && typeof fn === 'function' && Array.isArray(fn.__mcp_hooks) ? fn.__mcp_hooks : undefined;
|
|
271
|
+
// Stop expanding once `via.length` would reach the cap — at that point
|
|
272
|
+
// the current entry is treated as a leaf (Custom record without
|
|
273
|
+
// children). The slot-walker still pairs it with one slot, so output
|
|
274
|
+
// stays internally consistent.
|
|
275
|
+
if (sub && !seen.has(fn) && via.length < maxDepth) {
|
|
276
|
+
seen.add(fn);
|
|
277
|
+
out.push({ ...entry, expanded: true, via });
|
|
278
|
+
out.push(...flattenHookMeta(sub, [...via, entry.name], seen, maxDepth));
|
|
279
|
+
seen.delete(fn);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
out.push({ ...entry, via });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
};
|
|
287
|
+
const flatHooksToTree = (flat) => {
|
|
288
|
+
const root = [];
|
|
289
|
+
// Stack of currently-open parents, indexed by their depth (= via.length
|
|
290
|
+
// of THEIR own entry, since their children sit at via.length + 1).
|
|
291
|
+
const parents = [];
|
|
292
|
+
for (const entry of flat) {
|
|
293
|
+
const depth = entry.via?.length ?? 0;
|
|
294
|
+
while (parents.length > depth)
|
|
295
|
+
parents.pop();
|
|
296
|
+
const node = { kind: entry.kind, name: entry.name };
|
|
297
|
+
if (entry.hook !== undefined)
|
|
298
|
+
node.hook = entry.hook;
|
|
299
|
+
if (entry.value !== undefined)
|
|
300
|
+
node.value = entry.value;
|
|
301
|
+
if (parents.length === 0) {
|
|
302
|
+
root.push(node);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const parent = parents[parents.length - 1];
|
|
306
|
+
if (parent) {
|
|
307
|
+
parent.children = parent.children ?? [];
|
|
308
|
+
parent.children.push(node);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
root.push(node);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (entry.expanded) {
|
|
315
|
+
// Push as the new active parent at this depth. Subsequent entries
|
|
316
|
+
// with via.length > depth become this node's descendants.
|
|
317
|
+
parents.push(node);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return root;
|
|
321
|
+
};
|
|
322
|
+
const extractHooks = (fiber, filter) => {
|
|
323
|
+
// React's wrapper machinery makes "where does metadata live" depend on
|
|
324
|
+
// the exact HOC chain. We try the most likely homes in order:
|
|
325
|
+
//
|
|
326
|
+
// 1. `fiber.type.__mcp_hooks` — bare components, FunctionDeclarations,
|
|
327
|
+
// and the outer memo fiber when the chain is just memo(fn).
|
|
328
|
+
// 2. `fiber.elementType.__mcp_hooks` — memo(fn) without compare.
|
|
329
|
+
// React converts the fiber to SimpleMemoComponent and rewrites
|
|
330
|
+
// `fiber.type` to the inner function (see `updateMemoComponent` in
|
|
331
|
+
// ReactFabric); our metadata sits on the outer memo wrapper, which
|
|
332
|
+
// survives only on `elementType`.
|
|
333
|
+
// 3. `fiber.type.render.__mcp_hooks` — forwardRef wrapper. React lays
|
|
334
|
+
// out memo(forwardRef(fn)) as three fibers (memo → forwardRef →
|
|
335
|
+
// function). When the user queries by displayName they tend to
|
|
336
|
+
// match the middle ForwardRef fiber (whose displayName resolves
|
|
337
|
+
// via `render.displayName`); fiber.type there is the forwardRef
|
|
338
|
+
// wrapper, which holds the inner fn at `.render`. The babel plugin
|
|
339
|
+
// put plain metadata on that fn via the FunctionDecl visitor.
|
|
340
|
+
// 4. `fiber.type.type.__mcp_hooks` — memo wrapper around forwardRef
|
|
341
|
+
// (or any non-function inner). Not the SimpleMemoComponent path,
|
|
342
|
+
// so `fiber.type` stays as the memo wrapper and `.type` is the
|
|
343
|
+
// inner forwardRef / class / etc. This catches getter installations
|
|
344
|
+
// on the wrapper layer too.
|
|
345
|
+
const candidates = [
|
|
346
|
+
fiber.type,
|
|
347
|
+
fiber.elementType,
|
|
348
|
+
fiber.type?.render,
|
|
349
|
+
fiber.type?.type,
|
|
350
|
+
];
|
|
351
|
+
let rawMeta;
|
|
352
|
+
for (const c of candidates) {
|
|
353
|
+
const m = c?.__mcp_hooks;
|
|
354
|
+
if (Array.isArray(m)) {
|
|
355
|
+
rawMeta = m;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!Array.isArray(rawMeta))
|
|
360
|
+
return null;
|
|
361
|
+
const meta = flattenHookMeta(rawMeta, [], new WeakSet(), filter.expansionDepth);
|
|
362
|
+
const out = [];
|
|
363
|
+
let state = fiber.memoizedState;
|
|
364
|
+
let metaIdx = 0;
|
|
365
|
+
// Walk the fiber's hook chain and pair each slot with the next metadata
|
|
366
|
+
// entry whose `kind` is shape-compatible with the slot. Strongly-shaped
|
|
367
|
+
// kinds (Ref / Memo / Callback / Effect) match only the corresponding
|
|
368
|
+
// React hook shape. Permissive kinds (State / Reducer / Context / Custom)
|
|
369
|
+
// reject obvious mismatches (effect-record, ref-shape) so they don't
|
|
370
|
+
// swallow slots belonging to preceding custom hooks.
|
|
371
|
+
//
|
|
372
|
+
// For Custom-leaf entries — typically black-box library hooks
|
|
373
|
+
// (`useSelector`, `useQuery`, etc.) where flattenHookMeta couldn't expand
|
|
374
|
+
// because `fn.__mcp_hooks` was missing — we estimate slot count via
|
|
375
|
+
// `countHookSlots` and advance the fiber chain by that many slots in one
|
|
376
|
+
// step. Without this, a hook that consumes 3 internal slots only
|
|
377
|
+
// advances by 1 in the walker, drifting all trailing entries off the end
|
|
378
|
+
// of the chain.
|
|
379
|
+
const emitEntry = (entry, rawValueSlot) => {
|
|
380
|
+
const { hook, kind, name, via } = entry;
|
|
381
|
+
const passesKind = !filter.kindsSet || filter.kindsSet.has(kind);
|
|
382
|
+
const passesName = !filter.nameMatchers ||
|
|
383
|
+
filter.nameMatchers.some((m) => {
|
|
384
|
+
return m(name);
|
|
385
|
+
});
|
|
386
|
+
if (!(passesKind && passesName))
|
|
387
|
+
return;
|
|
388
|
+
const record = { kind, name };
|
|
389
|
+
// Prefer the babel-emitted hook name; fall back to fn.name for entries
|
|
390
|
+
// produced by older bundles that predate the `hook` field.
|
|
391
|
+
const resolvedHook = hook ?? (typeof entry.fn === 'function' ? entry.fn.name : undefined);
|
|
392
|
+
if (resolvedHook)
|
|
393
|
+
record.hook = resolvedHook;
|
|
394
|
+
if (filter.withValues && rawValueSlot !== undefined) {
|
|
395
|
+
// Redaction guard: mask the value (but keep kind/name/hook visible)
|
|
396
|
+
// when the entry's name OR any ancestor in `via` matches a redact
|
|
397
|
+
// pattern. Catches both direct sensitive hooks (`password` State)
|
|
398
|
+
// and leaves nested under sensitive customs (a `value` field of
|
|
399
|
+
// `useCredentials()` won't slip through).
|
|
400
|
+
const isRedacted = matchesAnyRedactPattern(name, filter.redactPatterns) ||
|
|
401
|
+
(via?.some((v) => {
|
|
402
|
+
return matchesAnyRedactPattern(v, filter.redactPatterns);
|
|
403
|
+
}) ??
|
|
404
|
+
false);
|
|
405
|
+
if (isRedacted) {
|
|
406
|
+
record.value = REDACTED_VALUE;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
let value;
|
|
410
|
+
try {
|
|
411
|
+
value = serializeHookValue(rawValueSlot, kind, filter.maxDepth);
|
|
412
|
+
// Final cycle / non-serialisable check — the MCP bridge will
|
|
413
|
+
// stringify this for transport, so bail now rather than killing
|
|
414
|
+
// the whole response if one stray value carries a cycle past our
|
|
415
|
+
// WeakSet (e.g. Proxy, lazy getter, native-bridged object).
|
|
416
|
+
JSON.stringify(value);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
value = '[Unserialisable value]';
|
|
420
|
+
}
|
|
421
|
+
record.value = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (via && via.length > 0)
|
|
425
|
+
record.via = via;
|
|
426
|
+
if (entry.expanded)
|
|
427
|
+
record.expanded = true;
|
|
428
|
+
out.push(record);
|
|
429
|
+
};
|
|
430
|
+
const advanceState = (steps) => {
|
|
431
|
+
for (let i = 0; i < steps && state; i++)
|
|
432
|
+
state = state.next;
|
|
433
|
+
};
|
|
434
|
+
while (state && metaIdx < meta.length) {
|
|
435
|
+
const entry = meta[metaIdx];
|
|
436
|
+
if (!entry) {
|
|
437
|
+
metaIdx++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// Expanded parent (synthetic, marks the call-site of a recursively
|
|
441
|
+
// expanded custom hook). Emit without consuming any fiber slot — the
|
|
442
|
+
// children that follow are the real slot-bearing entries.
|
|
443
|
+
if (entry.expanded) {
|
|
444
|
+
emitEntry(entry, undefined);
|
|
445
|
+
metaIdx++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
// Custom-leaf with a known fn → recurse into source to estimate slot
|
|
449
|
+
// count, then consume that many slots at once. The first slot's value
|
|
450
|
+
// is exposed (best approximation of "this hook's value"); the rest are
|
|
451
|
+
// skipped silently as internals of the library hook.
|
|
452
|
+
if (entry.kind === 'Custom' && typeof entry.fn === 'function') {
|
|
453
|
+
const slots = countHookSlots(entry.fn);
|
|
454
|
+
if (slots > 1) {
|
|
455
|
+
emitEntry(entry, state.memoizedState);
|
|
456
|
+
advanceState(slots);
|
|
457
|
+
metaIdx++;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!shapeMatchesKind(state.memoizedState, entry.kind)) {
|
|
462
|
+
state = state.next;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
emitEntry(entry, state.memoizedState);
|
|
466
|
+
metaIdx++;
|
|
467
|
+
state = state.next;
|
|
468
|
+
}
|
|
469
|
+
// Any metadata entries left after the fiber chain ran dry didn't get a
|
|
470
|
+
// slot match. Emit them anyway (without value) so the agent at least
|
|
471
|
+
// sees the hook exists — this is strictly better than silently dropping,
|
|
472
|
+
// and helps debug alignment issues. Common cause: a preceding Custom
|
|
473
|
+
// hook consumed more slots than countHookSlots estimated.
|
|
474
|
+
while (metaIdx < meta.length) {
|
|
475
|
+
const entry = meta[metaIdx];
|
|
476
|
+
if (entry)
|
|
477
|
+
emitEntry(entry, undefined);
|
|
478
|
+
metaIdx++;
|
|
479
|
+
}
|
|
480
|
+
return filter.format === 'tree' ? flatHooksToTree(out) : out;
|
|
481
|
+
};
|
|
13
482
|
const FIND_SCHEMA = {
|
|
14
483
|
index: {
|
|
15
484
|
description: '0-based index when several components match (default: 0).',
|
|
@@ -25,6 +494,38 @@ const FIND_SCHEMA = {
|
|
|
25
494
|
type: 'string',
|
|
26
495
|
},
|
|
27
496
|
};
|
|
497
|
+
// Default redact patterns — applied to a hook's `name` AND every entry in
|
|
498
|
+
// its `via` chain so values stay masked even when nested under a sensitive
|
|
499
|
+
// custom hook (e.g. a leaf `value` inside a `useAuth()` expansion). Tuned
|
|
500
|
+
// to match real-world variable names without over-matching innocent ones:
|
|
501
|
+
// `Pin$` is anchored so it doesn't catch "Spinner"; broad terms like
|
|
502
|
+
// `auth` are deliberately omitted (would catch `isAuthenticated`).
|
|
503
|
+
const DEFAULT_REDACT_HOOK_NAMES = [
|
|
504
|
+
/password/i,
|
|
505
|
+
/token/i,
|
|
506
|
+
/jwt/i,
|
|
507
|
+
/secret/i,
|
|
508
|
+
/Pin$/,
|
|
509
|
+
/credential/i,
|
|
510
|
+
/apiKey/i,
|
|
511
|
+
/authorization/i,
|
|
512
|
+
];
|
|
513
|
+
const REDACTED_VALUE = '[redacted]';
|
|
514
|
+
const escapeRegExp = (s) => {
|
|
515
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
516
|
+
};
|
|
517
|
+
const compileRedactPatterns = (raw) => {
|
|
518
|
+
return raw.map((p) => {
|
|
519
|
+
return typeof p === 'string' ? new RegExp(escapeRegExp(p), 'i') : p;
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
const matchesAnyRedactPattern = (name, patterns) => {
|
|
523
|
+
for (const p of patterns) {
|
|
524
|
+
if (p.test(name))
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
};
|
|
28
529
|
const resolveScreenFiber = (runtime) => {
|
|
29
530
|
const nav = runtime.navigationRef;
|
|
30
531
|
if (!nav || typeof nav.getCurrentRoute !== 'function')
|
|
@@ -143,6 +644,14 @@ const fiberTreeModule = (options) => {
|
|
|
143
644
|
(0, utils_1.setRootRef)(options.rootRef);
|
|
144
645
|
}
|
|
145
646
|
const navigationRef = options?.navigationRef;
|
|
647
|
+
// Compile the redact pattern list once at module init. Precedence:
|
|
648
|
+
// - `redactHookNames` provided → use it verbatim (replace mode).
|
|
649
|
+
// - `additionalRedactHookNames` provided → defaults + user's.
|
|
650
|
+
// - Neither → defaults.
|
|
651
|
+
// Pass `redactHookNames: []` to disable redaction entirely.
|
|
652
|
+
const redactPatterns = compileRedactPatterns(options?.redactHookNames !== undefined
|
|
653
|
+
? options.redactHookNames
|
|
654
|
+
: [...DEFAULT_REDACT_HOOK_NAMES, ...(options?.additionalRedactHookNames ?? [])]);
|
|
146
655
|
// Root-version keyed cache for `runQueryChain`. When React commits, the
|
|
147
656
|
// HostRoot fiber swaps — so a mismatched pointer is proof the tree changed
|
|
148
657
|
// and the cached match set for the same steps is no longer valid.
|
|
@@ -250,13 +759,15 @@ STEP CRITERIA
|
|
|
250
759
|
index — pick N-th match from this step; otherwise all matches fan out into the next step.
|
|
251
760
|
|
|
252
761
|
SELECT (output fields)
|
|
253
|
-
Default ["mcpId", "name", "testID"] — props
|
|
762
|
+
Default ["mcpId", "name", "testID"] — props, bounds, hooks are opt-in.
|
|
254
763
|
bounds: { x, y, width, height, centerX, centerY } in PHYSICAL pixels,
|
|
255
764
|
top-left origin. null when the fiber has no mounted host view. centerX/
|
|
256
765
|
centerY feed straight into host__tap.
|
|
257
766
|
props: full serialized props (heavy). Pair with propsInclude:
|
|
258
767
|
["key1","key2"] to keep only the props you actually need and avoid
|
|
259
768
|
pulling large style maps, data arrays, or nested element trees.
|
|
769
|
+
hooks: the component's hooks. Each entry { kind, name, hook?, via?,
|
|
770
|
+
expanded?, value? }; configure via hooksInclude.
|
|
260
771
|
|
|
261
772
|
RESPONSE
|
|
262
773
|
{ matches: [...], total, truncated? } — total is the unrestricted match
|
|
@@ -508,6 +1019,23 @@ TIPS
|
|
|
508
1019
|
const propsInclude = Array.isArray(args.propsInclude)
|
|
509
1020
|
? new Set(args.propsInclude)
|
|
510
1021
|
: null;
|
|
1022
|
+
const hooksIncludeRaw = args.hooksInclude;
|
|
1023
|
+
const hookKindsSet = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.kinds)
|
|
1024
|
+
? new Set(hooksIncludeRaw.kinds)
|
|
1025
|
+
: null;
|
|
1026
|
+
const hookNameMatchers = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.names)
|
|
1027
|
+
? hooksIncludeRaw.names.map(parseNamePattern)
|
|
1028
|
+
: null;
|
|
1029
|
+
const hookWithValues = hooksIncludeRaw?.withValues === true;
|
|
1030
|
+
const hookMaxDepth = typeof hooksIncludeRaw?.maxDepthInValues === 'number' &&
|
|
1031
|
+
hooksIncludeRaw.maxDepthInValues >= 0
|
|
1032
|
+
? Math.min(Math.floor(hooksIncludeRaw.maxDepthInValues), 8)
|
|
1033
|
+
: HOOK_DEFAULT_MAX_DEPTH;
|
|
1034
|
+
const hookExpansionDepth = typeof hooksIncludeRaw?.expansionDepth === 'number' &&
|
|
1035
|
+
hooksIncludeRaw.expansionDepth >= 0
|
|
1036
|
+
? Math.floor(hooksIncludeRaw.expansionDepth)
|
|
1037
|
+
: Infinity;
|
|
1038
|
+
const hookFormat = hooksIncludeRaw?.format === 'tree' ? 'tree' : 'flat';
|
|
511
1039
|
const runtime = { navigationRef, root };
|
|
512
1040
|
const runOnce = async (useCache) => {
|
|
513
1041
|
const rawMatches = runCachedQuery(runtime, steps, useCache);
|
|
@@ -567,6 +1095,17 @@ TIPS
|
|
|
567
1095
|
if (fields.has('testID')) {
|
|
568
1096
|
result.testID = fiber.memoizedProps?.testID;
|
|
569
1097
|
}
|
|
1098
|
+
if (fields.has('hooks')) {
|
|
1099
|
+
result.hooks = extractHooks(fiber, {
|
|
1100
|
+
expansionDepth: hookExpansionDepth,
|
|
1101
|
+
format: hookFormat,
|
|
1102
|
+
kindsSet: hookKindsSet,
|
|
1103
|
+
maxDepth: hookMaxDepth,
|
|
1104
|
+
nameMatchers: hookNameMatchers,
|
|
1105
|
+
redactPatterns,
|
|
1106
|
+
withValues: hookWithValues,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
570
1109
|
return result;
|
|
571
1110
|
}));
|
|
572
1111
|
return truncated ? { matches, total, truncated: true } : { matches, total };
|
|
@@ -653,6 +1192,18 @@ TIPS
|
|
|
653
1192
|
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.',
|
|
654
1193
|
type: 'boolean',
|
|
655
1194
|
},
|
|
1195
|
+
hooksInclude: {
|
|
1196
|
+
description: 'When select includes "hooks": `kinds` (State | Reducer | Memo | Callback | Ref | Effect | LayoutEffect | InsertionEffect | Context | Transition | DeferredValue | Id | SyncExternalStore | ImperativeHandle | Custom) and `names` (exact or `/regex/flags`) filter the list. Each entry carries { kind, name, hook?, via?, expanded? } — `hook` is the source-level hook function (`useState`, `useAnimatedStyle`); `expanded: true` marks a parent custom-hook call whose sub-hooks follow. Pass `withValues: true` for resolved values. `maxDepthInValues` caps value recursion (default 3, max 8). `expansionDepth` caps how deep custom hooks recurse — 0 = no expansion (top-level only), 1 = one level, default Infinity. `format: "tree"` returns nested `children:` instead of flat `via:`.',
|
|
1197
|
+
examples: [
|
|
1198
|
+
{ kinds: ['State', 'Memo'] },
|
|
1199
|
+
{ names: ['count', 'scrollRef'], withValues: true },
|
|
1200
|
+
{ kinds: ['State'], names: ['/^is/'], withValues: true },
|
|
1201
|
+
{ expansionDepth: 0 },
|
|
1202
|
+
{ format: 'tree', withValues: true },
|
|
1203
|
+
{ expansionDepth: 1, format: 'tree' },
|
|
1204
|
+
],
|
|
1205
|
+
type: 'object',
|
|
1206
|
+
},
|
|
656
1207
|
limit: {
|
|
657
1208
|
description: `Max matches to return (default ${QUERY_LIMIT_DEFAULT}, max ${QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
|
|
658
1209
|
type: 'number',
|
|
@@ -670,10 +1221,11 @@ TIPS
|
|
|
670
1221
|
type: 'array',
|
|
671
1222
|
},
|
|
672
1223
|
select: {
|
|
673
|
-
description: `Output fields: mcpId, name, testID, props, bounds. Default ${JSON.stringify(QUERY_DEFAULT_FIELDS)}.`,
|
|
1224
|
+
description: `Output fields: mcpId, name, testID, props, bounds, hooks. Default ${JSON.stringify(QUERY_DEFAULT_FIELDS)}.`,
|
|
674
1225
|
examples: [
|
|
675
1226
|
['mcpId', 'name', 'bounds'],
|
|
676
1227
|
['mcpId', 'name', 'props'],
|
|
1228
|
+
['mcpId', 'hooks'],
|
|
677
1229
|
],
|
|
678
1230
|
type: 'array',
|
|
679
1231
|
},
|