Activate a Pro license using an activation token from your dashboard. Use this if you signed up on the website and need to link your account to this MCP installation.

Activation token from your dashboard

Send a key event to an Android device/emulator.
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when sending Android key events (BACK, HOME, MENU, etc.)] Common keys: HOME, BACK, CALL, END_CALL, VOLUME_UP, VOLUME_DOWN, POWER, CAMERA, CLEAR, TAB, ENTER, DEL, MENU, SEARCH, MEDIA_PLAY_PAUSE, MEDIA_STOP, MEDIA_NEXT, MEDIA_PREVIOUS, MOVE_HOME, MOVE_END, APP_SWITCH, ESCAPE
PURPOSE: Dispatch Android system keys (BACK, HOME, MENU, ENTER, DEL, etc.) that aren't reachable via on-screen tap.
WHEN TO USE: Navigate back from a screen, submit a form with ENTER, dismiss the keyboard, or press hardware-style keys during a flow.

Android target: adb serial, emulator name, or RN device substring. Omit for first.

Key name (HOME, BACK, CALL, END_CALL, VOLUME_UP, VOLUME_DOWN, POWER, CAMERA, CLEAR, TAB, ENTER, DEL, MENU, SEARCH, MEDIA_PLAY_PAUSE, MEDIA_STOP, MEDIA_NEXT, MEDIA_PREVIOUS, MOVE_HOME, MOVE_END, APP_SWITCH, ESCAPE) or numeric keycode

Launch an app on an Android device/emulator by package name
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when launching an Android app by package name]
PURPOSE: Start an installed Android app by its package (and optional activity) so the next tool calls hit a running process.
WHEN TO USE: After a force-stop or install, or when the app isn't foregrounded before interaction.

Optional activity name to launch (e.g., .MainActivity). If not provided, launches the main activity.

Package name of the app (e.g., com.example.myapp)

List installed packages on an Android device/emulator
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when listing installed Android packages]
PURPOSE: Enumerate package names visible to adb so you can confirm installation or pick the right target for android_launch_app.
WHEN TO USE: Before android_launch_app when you don't know the exact package name, or to verify an install succeeded.

Optional filter to search packages by name (case-insensitive)

Long press at specific coordinates on an Android device/emulator screen
[PLATFORM FALLBACK — prefer `tap({ x, y, duration })` — it long-presses on both platforms and can resolve the target by testID/text/component unless you specifically need native-only behavior]
PURPOSE: Emit a sustained touch at raw pixel coordinates to trigger long-press handlers (context menus, drag starts, multi-select).
WHEN TO USE: Android-only coordinate holds with no React Native connection. Anything reachable through RN should use `tap({ duration })`, which also reports whether the element has an onLongPress handler.

Press duration in milliseconds (default: 1000)

X coordinate in pixels

Y coordinate in pixels

Take a screenshot from an Android device/emulator. Returns the image plus a screen-state summary: active route (name + navigation stack), overlay-grouped tappable elements (pressables behind an open sheet/modal are excluded), component names as JSX tags, labels, testIDs, and frames — all in ready-to-tap pixel coordinates. Prefer tap(text="...") when text is exact and unique; otherwise use tap(x, y) with coordinates from the list — this is the most reliable way to tap icons or visually-identified elements. Use component names for inspect_component/find_components.
PURPOSE: Snapshot what the user sees on Android AND get tap-ready pressables + a structured component map in one call.
WHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates on Android.
WORKFLOW: android_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> android_screenshot to verify.
LIMITATIONS: Requires adb in PATH and a running device/emulator. For non-RN surfaces (system dialogs, permission prompts), combine with tap(..., native=true).
GOOD: android_screenshot()
BAD: android_screenshot({ deviceId: "guess" }) with a made-up serial — run list_devices first.
SOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y).

Alias for `deviceId` — same accepted values. Provided for consistency with tap/get_screen_state/ios_screenshot, which all accept `device`. If both are given, `deviceId` wins.

Optional path to save the screenshot. If not provided, saves to temp directory.

Issue an HTTP request from inside the running app, as the logged-in user.
PURPOSE: Probe your backend through the app's real network stack, TLS trust and proxy config — without pasting credentials into the conversation.
WHY THIS EXISTS: hand-written fetch calls either dig the token out of redux or embed a JWT literal in the expression, which puts the credential in the transcript. auth="auto" resolves it in-app instead.
WHEN TO USE: reproduce a 4xx, check what an endpoint returns for an edge case, clean up test records the UI can't reach.
WORKFLOW: app_request({ method: "GET", url: "https://api.example.com/me" }) -> inspect status + body.
AUTH RESOLUTION (auth="auto", in order): explicit Authorization header -> redux (state.user.accessToken, state.auth.accessToken, state.auth.token) -> the Authorization header of the app's last captured request. That last step is source-agnostic, so it covers tokens kept outside redux — keychain, secure storage, an Apollo link — as long as the app has already made one authenticated call and the SDK captured it. Cookie auth needs none of this: the native cookie jar attaches cookies to any in-app request.
LIMITATIONS: needs a connected app. Pass an explicit Authorization only when every step above misses — it puts the credential in the transcript.
GOOD: app_request({ method: "DELETE", url: "https://api.example.com/address/17" })
BAD: embedding a bearer token in an execute_in_app expression — it lands in the transcript.

"auto" (default) resolves a bearer token in-app: redux, then the last captured request's Authorization header. "none" sends no Authorization header — the right choice for cookie-authenticated APIs.

Request body. An object is JSON-serialised for you; a string is sent verbatim (already-encoded JSON, urlencoded, raw). Sets Content-Type: application/json unless you override it.

Target device name (substring match). Omit for the default device.

Extra request headers. An explicit Authorization here wins over auth="auto".

Target size for the returned body in characters (default 25000). Oversized bodies are bounded structurally.

HTTP method.

Absolute URL to request.

Clear the log buffer.
PURPOSE: Empty the in-memory console buffer (and the SDK buffer if installed) so the next get_logs / search_logs only sees fresh entries.
WHEN TO USE: Before reproducing a bug so the resulting logs are isolated; between test iterations to avoid noise from earlier runs.
WORKFLOW: clear_logs -> reproduce the issue (tap / navigate / reload_app) -> get_logs or search_logs.
GOOD: clear_logs() right before tap(text="Submit")
BAD: clear_logs() AFTER the repro — you just deleted the evidence.

RN device name (substring). Omit for all devices; see get_apps.

Clear the network request buffer.
PURPOSE: Reset the captured request list to isolate new traffic from a specific user action.
WHEN TO USE: Right before reproducing a bug so the buffer contains only the relevant requests.
WORKFLOW: clear_network -> trigger action (tap, execute_in_app) -> get_network_requests / search_network.
LIMITATIONS: Irreversible — cleared requests cannot be recovered. Also clears the SDK's in-app buffer when SDK is present.
GOOD: clear_network() before a reproduction.
BAD: Using clear_network as a workaround for stale connections — use scan_metro / ensure_connection instead.

Connect to a Metro server on a specific port — the only way to reach a port scan_metro does not probe.
PURPOSE: Establish a CDP WebSocket connection to a Metro server on a known port.
WHEN TO USE: Metro is on a port outside 8081-8090. For any port in that range, scan_metro is strictly better — it probes them all and attaches every Bridgeless target in one call, where this connects to one port only.
SEE ALSO: scan_metro for auto-discovery; get_apps afterwards to confirm the device attached.

Metro server port (default: 8081)

Permanently delete your account and reset this MCP installation. Removes your server-side data and local cache. You will get a new installation ID on next restart. Requires confirm: 'DELETE' to proceed.

Must be exactly 'DELETE' to confirm account deletion

Disconnect from Metro servers and stop auto-reconnection. Without device param: disconnects ALL devices. With device param: disconnects only the matching device. Use this to remove stale connections or free the CDP slot for the built-in debugger. Log and network buffers are preserved. Reconnect later with scan_metro.
PURPOSE: Cleanly release CDP slots so another debugger (Flipper, React DevTools, Chrome) can attach, or nuke a stale connection the MCP keeps reviving.
WHEN TO USE: Before launching a native debugger, when connections keep flapping, or after a simulator/device restart left zombie targets.
WORKFLOW: disconnect_metro -> attach other debugger / restart app -> scan_metro to reconnect.
LIMITATIONS: Suppresses auto-reconnect until scan_metro or connect_metro is called again; buffers persist but won't receive new events.
GOOD: disconnect_metro(); disconnect_metro({ device: "iPhone" })
BAD: Using disconnect_metro to clear logs — use clear_logs instead; disconnect breaks capture.

Blur the currently focused TextInput, dismissing the on-screen keyboard.
PURPOSE: Close the keyboard when it's blocking content beneath the input, or move focus off an input before a tap that would otherwise be intercepted.
WHEN TO USE: After typing into a field and before tapping a button that is hidden by the keyboard. Or to verify a 'tap outside dismisses' UX is wired up.
PREREQUISITE: A TextInput must already have React focus. Tap the field first (e.g. tap({ testID: 'search' })).
LIMITATIONS: Requires Bridgeless/Fabric (RN new architecture). Returns 'no focused TextInput' if nothing is focused.

Optional device name (substring match). Uses default device if not specified.

Verify or establish a healthy connection to a React Native app. Use before running commands if connection may be stale, or after navigation/reload. This tool runs a health check and will auto-reconnect if needed.
PURPOSE: Health-check the existing CDP connection and transparently reconnect if it has gone stale, without rescanning all Metro ports.
WHEN TO USE: After a suspected disconnect (silent gaps, reload_app, app crash) or before long-running flows where a mid-flow drop would be costly. Cheaper than scan_metro when you already connected once this session.
WORKFLOW: scan_metro (once) -> ensure_connection(healthCheck=true) -> resume tool calls. Use forceRefresh=true if the first probe still looks dead.
GOOD: ensure_connection({ healthCheck: true })
BAD: ensure_connection() before scan_metro has ever run — call scan_metro first.

Force close existing connection and reconnect (default: false)

Run health check to verify page context is responsive (default: true)

Metro port (default: auto-detect)

Execute JavaScript code in the connected React Native app and return the result. Use this for inspecting app state, calling methods on exposed global objects, or running diagnostic code. Hermes compatible: 'global' is automatically polyfilled to 'globalThis', so both global.__REDUX_STORE__ and globalThis.__REDUX_STORE__ work.

RECOMMENDED WORKFLOW: 1) list_debug_globals to discover available objects, 2) inspect_global to see properties/methods, 3) execute_in_app to call methods or read values.

LIMITATIONS (Hermes engine):
- NO require() or import — only pre-existing globals are available
- Async: use `Promise.resolve(foo()).then(function(r){ return r; })` (resolved for you when awaitPromise:true). `async`/`await` syntax is engine-dependent — many Hermes builds reject it.
- Multi-statement input is auto-wrapped into an IIFE returning the last statement's value. If that can't yield a value (`if`/`for`/declaration), write the IIFE yourself with an explicit `return`.
- Non-ASCII in string literals (emoji, Arabic, CJK) is auto-escaped server-side. Write it as-is.

GOOD examples: `__DEV__`, `__APOLLO_CLIENT__.cache.extract()`, `__EXPO_ROUTER__.navigate('/settings')`
BAD examples: `await fetch(...)` (bare top-level await), `require('react-native')`
Pass timeoutMs (ms) for long-running expressions; capped at 120000. Auto-reconnect surfaces _meta.reconnected when a transport drop was self-healed.

Whether to await promises (default: true)

Collect a deferred promise result by handle. When a promise outlives its poll budget the result is kept in the app and its handle returned; pass it here to retrieve the settled value. Use instead of `expression`, not alongside it.

RN device name (substring). Omit for default; see get_apps.

REQUIRED unless `collect` is used. JavaScript expression to execute. Must be valid Hermes syntax — no require(), no `await`/`async` (use `Promise.resolve(foo()).then(function(r){ return r; })`), no unbalanced quotes. Multi-statement input is auto-wrapped into an IIFE returning the last statement's value. Use globals discovered via list_debug_globals — `globalThis.__rn__` exposes I18nManager, Dimensions, PixelRatio, Platform, NativeModules, StyleSheet, AppRegistry when populated, but check it for null before dereferencing.

Target size of the result in characters (default: 2000, 0 for unlimited). Oversized results are bounded structurally — arrays and objects are elided with count-preserving markers like "…+150 more" — not clipped mid-string.

Per-call timeout in milliseconds. Default: 10000. Hard cap: 120000 (values above are clamped with a warning surfaced in the response). A timeout here is a logical failure and does NOT trigger auto-reconnect.

Disable result truncation. Tip: Be cautious - Redux stores or large state can return 10KB+.

collect only: block up to this long (ms, capped at 120000) waiting for the handle to settle, instead of returning 'Still pending' immediately. Default 0 — one look, no wait.

Find components matching a name pattern. **TARGETED SEARCH**: Use after get_screen_layout or get_component_tree(structureOnly=true) to find specific components by pattern. Use includeLayout=true to get padding/margin/flex styles.
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Fast regex search over the entire fiber tree — including off-screen and wrapper components — to locate every instance of a component by name.
WHEN TO USE: You know roughly what the component is called (e.g., "Button", "Screen$") but not where it lives, or you need counts/paths before drilling in with inspect_component.
WORKFLOW: get_screen_layout (orient) -> find_components(pattern="...") -> inspect_component(componentName="...", index=N).
LIMITATIONS: Matches the React display name only; minified builds may return opaque names. Large result sets — use maxResults or a tighter pattern.
GOOD: find_components({ pattern: "Button" }); find_components({ pattern: "Screen$" })
BAD: find_components({ pattern: ".*" }) — floods the response; narrow the regex.
SOURCE: searching by name to find a file? If you can point at it on screen, inspect_at_point(x, y) returns the file and line directly.

Output format: 'json' or 'compact' (default, pipe-delimited rows — roughly 4.5x smaller than json)

Include layout styles (padding, margin, flex) for each matched component

Maximum number of results to return (default: 20)

Regex pattern to match component names (case-insensitive). Examples: 'Button', 'Screen$', 'List.*Item'

Show only last 3 path segments (default: true)

Return only component counts by name instead of full list (default: false)

Per-call timeout in milliseconds. Default: 5000. Hard cap: 120000.

Skip components inside hidden/inactive navigation scenes (unfocused Drawer/Tab destinations, inactive screens). Default false = search the entire fiber tree.

List currently connected React Native apps and their connection status. If no apps are connected, run scan_metro first to establish a connection.
PURPOSE: Enumerate active debug targets with device names, platforms, ports, and detected RN/Expo versions so you can target the right one.
WHEN TO USE: After scan_metro to confirm what connected, or before passing a device="..." filter to another tool.
WORKFLOW: scan_metro -> get_apps -> get_logs / ios_screenshot / tap (with device="..." if multiple).
LIMITATIONS: Only lists devices the MCP has successfully connected to — stale targets don't appear here, use get_connection_status for health details.
GOOD: get_apps()
BAD: Calling get_apps in a tight loop — the list doesn't change without a scan_metro or disconnect_metro.

Retrieve captured Metro bundling/compilation errors. These are errors that occur during the bundle build process (import resolution, syntax errors, transform errors) that prevent the app from loading. If no errors are captured but Metro is running without connected apps, automatically falls back to screenshot+OCR to capture the error from the device screen.
PURPOSE: Surface Metro's build-time failures (not runtime JS errors) that keep the app from booting or hot-reloading.
WHEN TO USE: App shows the red error screen, refuses to connect, or Fast Refresh stops working after an edit. Also use when get_logs is silent but the app is clearly broken.
WORKFLOW: get_bundle_status -> get_bundle_errors -> fix source -> get_bundle_errors({ clear: true }) -> reload_app.
LIMITATIONS: Captures errors Metro emits via its WebSocket; the screenshot+OCR fallback requires a booted simulator and the platform param.
GOOD: get_bundle_errors({ platform: "ios" }); get_bundle_errors({ clear: true }) after fixing, so the next read reflects only new errors.
BAD: Using get_bundle_errors to look for runtime TypeErrors — those live in get_logs, not the bundler.

If true, empty the bundle error buffer after reading it. Use once a bundling error is fixed so the next call reflects only fresh errors. The errors are still returned in this response.

Optional device target for screenshot fallback. Accepts an adb serial / iOS UDID, an emulator/simulator name, or a substring of the connected RN device name. Uses first available device if not specified.

Maximum number of errors to return (default: 10)

Platform for screenshot fallback when no errors are captured via CDP. Required to enable fallback.

Get the current Metro bundler status including build state and any recent bundling errors. Use this to check if there are compilation/bundling errors that prevent the app from loading.
PURPOSE: Snapshot Metro's current build state (idle / transforming / error) together with any captured errors — a fast "is the bundler healthy?" check.
WHEN TO USE: Before diving into runtime debugging — rules out compile-time failures that would make get_logs and tap pointless.
WORKFLOW: get_bundle_status -> if errors present: get_bundle_errors for detail -> fix -> get_bundle_errors({ clear: true }).
LIMITATIONS: Relies on Metro's WebSocket event stream; if Metro isn't running or the connection dropped, status may be stale.
GOOD: get_bundle_status() at the start of a debug session.
BAD: Polling every second — Metro events are push-based; just call once and act on the result.

Get the React component tree from the running app — the fiber hierarchy including providers, navigation wrappers, and internal components. For a screen overview with positions and text, use get_screen_layout instead. Returns compact names-only structure by default; pass structureOnly=false for the full detailed tree.
PURPOSE: Expose the entire fiber tree — including providers, navigators, and off-screen subtrees — when get_screen_layout's visible-only view isn't enough.
WHEN TO USE: Debugging context propagation, navigation wrappers, hidden modals, or when you need to understand the full React architecture.
WORKFLOW: get_component_tree() for overview -> find_components for targeted lookup -> inspect_component for props/state.
LIMITATIONS: The detailed tree (structureOnly=false) is very large and routinely exceeds response-size limits on real apps — reach for inspect_component on a specific node instead. Ignores non-React native views. Minified builds return display names that may be opaque.
GOOD: get_component_tree()
BAD: get_component_tree({ structureOnly: false, includeProps: true, includeStyles: true }) on a large app — prefer inspect_component for specific nodes.

RN device name (substring). Omit for default; see get_apps.

Output format: 'json' or 'compact' (default, indented tree — roughly 6x smaller than json). Ignored if structureOnly=true.

Hide internal RN components (RCTView, RNS*, Animated, etc.) for cleaner output (default: true)

Include component props (excluding children and style). Ignored if structureOnly=true.

Include layout styles (padding, margin, flex, etc.). Ignored if structureOnly=true.

Maximum tree depth (default: 5000)

Return ultra-compact structure with just component names (no props, styles, or paths). Default true — the detailed tree averages tens of thousands of tokens and is rarely what you want. Set false only when you specifically need props/styles/paths for the whole tree.

Get detailed connection health status including uptime, recent disconnects/reconnects, and connection gaps that may indicate missing data.
PURPOSE: Diagnose flaky CDP sessions — quantify uptime, count reconnects, and expose gaps where logs/network data could be missing.
WHEN TO USE: When logs look suspiciously empty, tools complain about disconnect/reconnect, or the app was suspended and resumed.
WORKFLOW: get_connection_status -> if unhealthy: disconnect_metro -> scan_metro to rebuild a clean session.
LIMITATIONS: Reports only MCP-side view; doesn't know why Metro dropped the socket (simulator sleep, app backgrounded, etc).
GOOD: get_connection_status() after noticing stale data.
BAD: Polling get_connection_status as a heartbeat — use ensure_connection(healthCheck=true) for a live probe.
Pass events=true to include the recent connection event log (connect/close/reconnect lifecycle) — useful when a target drops mid-session and you need to see why reconnect didn't recover it.

When events=true, filter the event log to a single appKey (format: '<port>-<deviceId>').

When events=true, show only the last N events. Default: 50.

Include the recent connection event log (lifecycle: connect, close, reconnect attempts/failures, stale-target, etc). Default: false.

Access the shared image buffer containing screenshots from all tools (ios_screenshot, android_screenshot, ocr_screenshot, tap verification). Returns metadata only by default — use id or groupId+frameIndex to retrieve actual image data. Tap burst verification stores frame groups here when burst=true is used.
PURPOSE: Retrieve prior screenshots — especially tap burst frames — without re-taking them, for visual diffing or reviewing transient UI states.
WHEN TO USE: After tap(burst=true) reports transientChangeDetected, or to compare before/after frames without another screenshot round-trip.
WORKFLOW: tap(burst=true) -> note verification.burstGroupId -> get_images(groupId, frameIndex=N) to inspect individual frames.
LIMITATIONS: Circular buffer (50 entries) — old images are evicted. Metadata is cheap; fetching image data is not — request specific ids, not bulk.
GOOD: get_images({ list: true }); get_images({ groupId: "burst-abc", frameIndex: 2 })
BAD: get_images() with no filter when buffer is full — floods context. Use list:true or last:N first.

Clear the buffer

Retrieve a specific frame from a group (requires groupId)

List frames in a group (metadata only), or combine with frameIndex to retrieve a specific frame

Retrieve a specific image by ID (returns image data)

Return the N most recent entries (metadata only)

List all entries and groups (metadata only, no image data)

Filter entries by source

Get your installation ID, license tier, and this month's usage against the free cap. Shows the Installation ID (needed to link Pro in the dashboard), current tier, cache validity, and calls used / remaining this month.

Get the full payload of a single log event — complete stack trace, backtrace, or oversized message.
PURPOSE: Expand one row from get_logs into its full text. A crash row collapses a 60-line backtrace; this returns all of it.
WHEN TO USE: After get_logs shows an event you need to read in full (a crash, an exception, a large payload).
WORKFLOW: get_logs -> copy the id (e.g. "n7") -> get_log_details(id="n7").
LIMITATIONS: Ids are valid until that device's buffer rolls over or clear_logs runs, not for the whole server session — call get_logs again to get fresh ones. Reads the buffer — it does not re-query the device.
GOOD: get_log_details({ id: "n7" })
BAD: Guessing ids — always take them from get_logs.

Event id from get_logs (e.g. "n7")

Max characters of payload (default: 4000, 0 for unlimited)

Disable truncation entirely

Retrieve console logs from connected React Native app. Tip: Use summary=true first for a quick overview (counts by level + last 5 messages), then fetch specific logs as needed.
PURPOSE: Pull captured console output (log/warn/error/info/debug) from the in-memory buffer, and optionally native device logs (crashes, ANRs) via source="native".
WHEN TO USE: Start of any log-driven investigation, verifying a code change picked up via Fast Refresh, or confirming a reported error actually fires.
WORKFLOW: scan_metro -> get_logs(summary=true) -> narrow with search_logs(text="...") or get_logs(level="error") -> clear_logs between reproductions.
LIMITATIONS: Circular buffer (~500 entries). Only captures logs emitted after the app connected; pre-connect logs are lost.
GOOD: get_logs({ summary: true }) then get_logs({ level: "error", maxLogs: 20 })
BAD: get_logs({ maxLogs: 500, verbose: true }) as a first call — floods context; start with summary=true.

RN device name (substring). Omit for all devices; see get_apps.

Filter by app run. 'current' = the live run only; a number targets a specific run; omit or 'all' for everything including pre-restart data (default).

Filter native/merged events by kind. Omit for all kinds.

Filter by log level (default: all)

Maximum number of logs to return (default: 50)

Max characters per message (default: 500, set to 0 for unlimited). Tip: Use lower values for overview, higher when debugging specific data structures.

Relevance floor for native events (default: warn), ordered debug < log < info < warn < error < fatal. Crashes and ANRs are always returned regardless. On iOS use 'log': os_log 'Default' — what a plain os_log() call emits, and most of what a simulator produces — maps to that tier, so 'info' excludes it. Lower to 'debug' to see native library loading.

Native acquisition window — ISO timestamp or a duration like "5m". Widens the device query; already-seen events are still deduped.

Which log stream to read. 'js' (default) = console output over CDP, instant. 'native' = device logs (Android logcat / iOS os_log) filtered to this app — surfaces crashes, ANRs and OOM kills that never reach JS; costs ~1-1.5s per device. 'all' = both, merged. When the JS buffer is empty and the app is disconnected, native crash events are consulted automatically.

Start from the first log line containing this text

Return summary statistics instead of full logs (count by level + last 5 messages). Use for quick overview.

Disable all truncation and return full messages. Tip: Use with lower maxLogs (e.g., 10) to avoid token overload when inspecting large objects.

Retrieve captured network requests from connected React Native app. Shows URL, method, status, and timing. Note: On Bridgeless targets (Expo SDK 52+) without the SDK, capture may miss early startup requests. Install execbro-sdk for full capture with headers and response bodies. Tip: Use summary=true first for stats overview.
PURPOSE: Inspect HTTP traffic the app made since connection — URLs, methods, status codes, and timings — to debug API, auth, and caching issues.
WHEN TO USE: User reports a failed login/load, slow screen, or wrong data. Confirm a request fired, check its status, and pivot to get_request_details for headers/body.
WORKFLOW: scan_metro -> reproduce action -> get_network_requests({ summary: true }) -> get_network_requests({ status: 500 }) or search_network -> get_request_details(id).
LIMITATIONS: Bridgeless targets without the SDK may miss pre-connect requests and response bodies — install execbro-sdk for full fidelity.
GOOD: get_network_requests({ summary: true }) then get_network_requests({ urlPattern: "/login", status: 401 })
BAD: get_network_requests({ maxRequests: 500 }) as the first call — start with summary=true.

Maximum number of requests to return (default: 50)

Filter by HTTP method (GET, POST, PUT, DELETE, etc.)

Filter by HTTP status code (e.g., 200, 401, 500)

Return statistics only (count, methods, domains, status codes). Use for quick overview.

Filter by URL pattern (case-insensitive substring match). Also matches GraphQL operation names (e.g. "GetCharacters"), since every GraphQL call shares one URL.

Pull-style probe: did the JS runtime accept a Fast Refresh (HMR) update since `since`? Returns lastUpdateAt, updateCount, and recentUpdates from a 32-entry ring buffer fed by a recorder around __ReactRefresh.performReactRefresh (preferred) or \$RefreshReg\$ (fallback).

PURPOSE: Confirm an edit landed in the running app without polling logs or screenshots. After editing TSX, wait ~2s then call with `since` = a Date.now() captured BEFORE the edit. updateCount > 0 means Fast Refresh accepted.

WHEN TO USE: After editing .tsx/.ts files (prefer over reload_app). To distinguish runtime acceptance (this) from Metro build state (get_bundle_status).

FILTER: `since` (epoch ms) keeps entries with at > since. `sincePath` (substring) matches modulePath — available on the \$RefreshReg\$ path but often omitted on performReactRefresh; if so the filter matches nothing — drop it.

LIMITATIONS: A full reload resets the buffer (next call reports `recorder just installed`). Edits to non-React utility files still increment. Requires the React 18+ refresh runtime.

SEE ALSO: get_bundle_status (did Metro compile?), get_bundle_errors (compile failures), reload_app (force full reload).

RN device name (substring). Omit for default; see get_apps.

Epoch ms; only count refresh entries with at > since. Capture Date.now() before your edit.

Substring matched against entry modulePath. Must not contain double quotes. Omit on builds where modulePath is unavailable.

Get full details of one network request: headers, body, status, timing.
PURPOSE: Drill into a single entry after get_network_requests or search_network returns a suspect id.
WORKFLOW: get id -> read the shape -> get_request_details({ requestId, query }) for the subtree you need.
SHAPE FIRST: a large JSON body returns as its structure — key paths kept, arrays and objects annotated with real sizes, leaves clipped. The first call already answers 'is there an errors array' and 'does this field exist'.
QUERY: dot-path into the JSON body, returned in full. Supports a.b.c, [0], [-1], [*], ["key.with.dots"]. Targets the response body, or the request body when there is none. No match returns the shape plus what IS there — retry from that.
SIDES: with a query, only the queried body is rendered (no headers, no other side) — include:"response"/"request"/"both" overrides. Credential headers are redacted to scheme + length; verbose:true prints them.
LIMITATIONS: Bodies need the execbro-sdk; CDP-only targets have no response body. Non-JSON is clipped at maxBodyLength, not projected.
GOOD: get_request_details({ requestId: "42", query: "data.approvals.single.basicInfo" }) | query: "errors[*].message"
BAD: verbose:true to find one field — that is the 40KB dump this avoids. Or guessing requestIds.

RN device name (substring). Omit for all devices; see get_apps.

Which side to render (headers + body). Defaults to the side `query` targets, or "both" when there is no query — so narrowing with query no longer re-dumps the request headers and the whole GraphQL query text on every call.

Byte target for each rendered body (default: 2000, 0 for unlimited). JSON is bounded structurally, so this trades depth and array width for size rather than cutting the text off.

Dot-path into the JSON body, returned in full: "data.items[0].id", "errors[*].message", "data[\"weird.key\"]". A leading $. is accepted. Omit to get the shape of the whole body. A path that matches nothing returns the shape plus what is actually there, not an error.

The request ID to get details for

Return bodies raw and unbounded, and print credential headers (Authorization, Cookie, …) in full instead of redacted. Prefer query — verbose on a large JSON body is the 40KB dump this tool exists to avoid.

Get a screen map showing visible components as an indented tree with actual screen positions. Uses measureInWindow for real coordinates and filters out off-screen components. Returns meaningful component names with text content and frame data (x,y width x height). Coordinates are delivered-screenshot pixels — the same space screenshots, get_screen_state and tap() use, so pass them through unchanged. Use extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.).
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Quickest textual map of what is actually on screen right now — component names, positions, and text — so you can plan taps and inspections without guessing.
WHEN TO USE: First step whenever the user asks "what's on screen", "why is X covering Y", or before tapping a visually ambiguous element.
WORKFLOW: get_screen_layout -> find_components(pattern="...") or inspect_component(componentName="...") -> tap(testID=...) -> get_screen_layout again to confirm.
LIMITATIONS: pass coordinates straight to tap(), which handles conversion — never multiply by devicePixelRatio yourself.
GOOD: get_screen_layout({ extended: true })
BAD: get_screen_layout({ summary: true }) when you actually need to pick a specific element — summary hides the tree.
SOURCE: file:line for an element? inspect_at_point(x, y).

Include layout styles (padding, margin, flex, backgroundColor, borderRadius, etc.) for each component. Default: false for compact output.

Return only component counts by name instead of full tree (default: false)

Per-call timeout in milliseconds. Default: 5000; bumped to 15000 when extended=true. Hard cap: 120000.

Screenshot-free snapshot of the current screen: active route + params, blocking overlays (sheets, modals, alerts), and every on-screen element merged top-to-bottom within reachability groups. Call after any tap or navigation to orient before the next action. Each line carries an (x, y) center + frame bounds (so anything is a tap(x, y) target), typed by a leading marker: 🔘 pressable (with component JSX tag, label, testID, onPress hint), 📝 text, 🖼 image (with src/alt). Elements covered by an open overlay are grouped under 🚫 Blocked — visible for context, but taps will NOT reach them until the overlay closes.

WHEN TO USE: After every tap/swipe that may navigate, and to read screen content (prices, labels, which image loaded) without a screenshot+OCR round-trip.
COORDINATES: delivered-screenshot pixels — the same space as ios_screenshot/android_screenshot, get_screen_layout, inspect_at_point, measure and tap(). Pass them through unchanged; never scale by devicePixelRatio yourself.
LIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection.
HISTORY: includeHistory=true appends the route trail (dwell + origin per screen).
PARAMS: route params listed by key; fullParams=true adds values.
SOURCE: this lists what is on screen, not where it lives in code — for the file:line that renders an element, call inspect_at_point(x, y).
SEE ALSO: get_screen_layout for the full component tree; this is a flat, tap-ready content list.

Emit the route params' values. Default false — only the param key names are listed, because the full blob is usually hundreds of characters of ids and image URLs.

Emit each text node's full string instead of the 80-char truncation. Default false.

Append the route trail — which screens the app has been on, most recent first, with dwell time and the route each was entered from. Recorded from connection time; an app restart shows an epoch divider. If no navigation listener could be attached the trail reports itself as sampled, meaning transitions between calls may be missing. Default false.

Return only route + overlays + pressables (the lean orientation snapshot), omitting on-screen text and images. Default false. Pressables include Switch/checkbox elements (onValueChange), each rendered with its current value as [switch:ON] / [switch:OFF] — tap those by testID or component instead of guessing an x from a screenshot.

Get recommended workflows and best practices for using the debugging tools. Call without parameters to see all available topics with short descriptions. Call with a topic parameter to get the full guide for that topic.

Topic to get the full guide for. Available topics: setup, inspect, layout, interact, logs, network, state, bundle, feedback. Omit to see the overview of all topics.

Write text into a React Native TextInput, or the OS's currently focused field, and verify it landed.
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Set a field's text and confirm, by reading the value back, that it holds exactly what you sent.
WHEN TO USE: Any text entry in a React Native app. Pass testID (or component/textMatch) and this focuses the field itself — no separate tap needed.
WORKFLOW: get_screen_state -> input_text({ testID, text }) -> read `verified`.
VERIFICATION: the write is read back and compared EXACTLY. A mismatch retries once, then fails with `sent` vs `landed`.
AMBIGUITY: several matching inputs -> the tool refuses and returns a numbered candidate list; pick one with `index`.
NATIVE SCREENS: with no React fiber tree at all (system dialog, native onboarding, non-RN app) this falls back automatically to typing into whatever the OS reports as focused — tap it first. native:true forces that path, and ignores testID/component/textMatch: it cannot target, only type into what already has focus.
LIMITATIONS: fields with no onChangeText fall back to the platform driver, which is US-keyboard only — non-ASCII fails there. The native path shares that limit.
GOOD: input_text({ testID: "new-topic-title", text: "Q3 budget", replace: true })
GOOD: input_text({ text: "1234", native: true }) — a system PIN prompt, no RN screen behind it.
BAD: input_text({ text: "..." }) with nothing focused — pass a target instead.

Target by React component name (case-insensitive substring), e.g. 'FormInput'. Use when there is no testID.

RN device name (substring match). Omit when one app is connected; see get_apps.

Zero-based choice when the target matches several inputs. The response's candidate list gives the indexes.

Skip React targeting and type directly into whichever field the OS reports as focused, via the platform accessibility tree. For system dialogs and non-RN screens. Ignores testID/component/textMatch/index. Auto-applied when no fiber tree is reachable at all, even without this flag.

Replace the field's contents instead of appending. Default false (append).

Target the input with this testID. Most reliable — the tool focuses it itself, no prior tap needed.

The text to write into the field.

Target by the field's visible label, placeholder, or current value (case-insensitive substring). NOTE: this picks WHICH field to write to; `text` is what gets written.

Inspect layout AND props at (x, y). Returns FRAME PER ANCESTOR (position/size in delivered-screenshot pixels, the same space as screenshots/get_screen_state/tap, for every ancestor that hit-tested the point) + the innermost component's PROPS (handlers as [Function], refs, custom props like onPress/data/testID). Pure JS hit-test via fiber + measureInWindow — no overlay toggled, zero visual side effect. Works on Paper and Fabric.
PURPOSE: Layout/props diagnosis — "where is each ancestor positioned, and what props does the touched component expose?"
WHEN TO USE: A button is clipped, hit area is wrong, animated frame is unexpected — or you need handler/ref/non-style props. Also preferred for tight loops (no overlay flicker).
WORKFLOW: screenshot or get_screen_state → take the coordinate as-is → inspect_at_point(x, y).
LIMITATIONS: Style is the node's own style object, not the merged cascade. `frame` is the element's own box; `hitFrame` (when present) is the innermost node actually under the point.
SOURCE: also returns `source: {file, line, column}` for the component at the point, plus the owner chain as `Source ancestors` (set source=false to skip in tight loops).

RN device name (substring). Omit for default; see get_apps.

Include position/dimensions (frame) in the output (default: true)

Include component props in the output (default: true)

Resolve the component's source file and line via Metro symbolication. Default true. Set false to skip in tight loops.

X coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion.

Y coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion.

Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go.
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Reveal a mounted component's live props, hook state, and (optionally) child subtree so you can reason about why it renders the way it does.
WHEN TO USE: User asks "why is this button disabled", "what props does X receive", or you need to confirm state changed after a tap.
WORKFLOW: get_screen_layout or find_components -> inspect_component(componentName="Foo") -> tap or execute_in_app to change state -> inspect_component again.
LIMITATIONS: Requires the component to be currently mounted in the fiber tree. Name matching is exact; use find_components for fuzzy/regex lookup.
GOOD: inspect_component({ componentName: "SneakerCard", index: 0 })
BAD: inspect_component({ componentName: "Card" }) when many Card instances exist — pass index or narrow via find_components.

How many levels deep to show children (default: 1 = direct children only, 2+ = nested tree)

Name of the component to inspect (e.g., 'Button', 'HomeScreen', 'FlatList')

Include children component tree

Include component state/hooks (default: true)

Include flattened style on each child entry (only with includeChildren). Use when debugging 'why isn't X style applying' or cascade-like inheritance on nested elements (e.g., textAlign on an inner Text, flex on a wrapper View). Resolves StyleSheet IDs and array/conditional styles to a single object. Default: false.

If multiple instances exist, which one to inspect (0-based index, default: 0)

Show only last 3 path segments (default: true)

Simplify hooks output by hiding effects and reducing depth (default: true)

Per-call timeout in milliseconds. Default: 5000. Hard cap: 120000.

Inspect a global object (or a dotted path into one) to see its properties, types, and whether they are callable functions. Use this BEFORE calling methods on unfamiliar objects to avoid errors.
PURPOSE: Surface the shape of a global (Apollo client, Redux store, Expo Router, SDK-registered store, etc.) — keys, types, and which members are callable — without executing arbitrary code.
WHEN TO USE: After list_debug_globals identifies a promising global and before you try execute_in_app on it.
WORKFLOW: list_debug_globals -> inspect_global(objectName="__APOLLO_CLIENT__") -> execute_in_app("__APOLLO_CLIENT__.cache.extract()").
DOTTED PATHS: Pass dotted paths to drill into the SDK surface, e.g. inspect_global({ objectName: "__RN_AI_DEVTOOLS__.stores.redux" }) or "__RN_AI_DEVTOOLS__.custom.mmkv". Only identifier paths are accepted — for arbitrary expressions, use execute_in_app.
LIMITATIONS: Only reads one level deep; nested objects show as a 100-char JSON preview — re-inspect the child path. Returns an error object (not a throw) when the path doesn't resolve.
WIDE OBJECTS: a global with hundreds of keys is bounded structurally rather than dumped; use query to pull one entry in full.
GOOD: inspect_global({ objectName: "__APOLLO_CLIENT__" }) | inspect_global({ objectName: "__RN_AI_DEVTOOLS__.stores.redux", query: "cache" })
BAD: inspect_global({ objectName: "store.getState()" }) — call expressions aren't supported; use execute_in_app.

Byte target for the rendered listing (default: 25000, 0 for unlimited).

Identifier or dotted path of the global to inspect (e.g., '__APOLLO_CLIENT__', '__RN_AI_DEVTOOLS__.stores.redux', '__RN_AI_DEVTOOLS__.custom.mmkv')

Dot-path into the property listing, returned in full (e.g. "cache" or "stores.redux"). A path that matches nothing returns the listing plus what is actually there, not an error.

Boot an iOS simulator by UDID.
PURPOSE: Bring a specific simulator online so you can install/launch an app in it.
WHEN TO USE: At session start when no simulator is running, or after switching between device models.

[PLATFORM-SPECIFIC — no cross-platform equivalent; use when booting an iOS simulator] Use list_devices to find available simulators.

UDID of the simulator to boot (from list_devices)

Press a hardware button on an iOS simulator.
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when pressing iOS hardware buttons (HOME, LOCK, SIRI, APPLE_PAY)] Requires an iOS UI driver: AXe (recommended: brew install cameroncooke/axe/axe) or IDB (brew install idb-companion).
PURPOSE: Trigger iOS hardware buttons (HOME, LOCK, SIDE_BUTTON, SIRI, APPLE_PAY) that aren't reachable via on-screen tap.
WHEN TO USE: Send the app to background (HOME), lock the simulator (LOCK), or exercise Siri/Apple Pay flows.

Hardware button to press: HOME, LOCK, SIDE_BUTTON, SIRI, or APPLE_PAY

Optional button press duration in seconds

iOS target: UDID, simulator name, or RN device substring. Omit for booted.

Launch an app on an iOS simulator by bundle ID
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when launching an iOS app by bundle ID]
PURPOSE: Start an installed iOS app by its bundle ID so the next tool calls hit a running process.
WHEN TO USE: After ios_terminate_app or an install, or when the app isn't foregrounded before interaction.

Bundle ID of the app (e.g., com.example.myapp)

Open a URL in the iOS simulator (opens in default handler or Safari).
PURPOSE: Drive an iOS simulator into a deep link or universal link entry point so you can exercise routing from an external entry.
WHEN TO USE: Testing deep-link handlers, universal link routing, OAuth/SSO callback URLs, or any flow that enters the app via a URL.
WORKFLOW: ios_boot_simulator -> ios_launch_app (or have the app running) -> ios_open_url -> ios_screenshot / get_screen_layout to verify the target screen rendered.
GOOD: ios_open_url(url="myapp://product/42") to land directly on a product screen.
BAD: ios_open_url(url="...") used as a substitute for in-app navigation when the user would normally tap — prefer `tap` for normal interaction flows.

[PLATFORM-SPECIFIC — no cross-platform equivalent; use when testing iOS deep links or universal links]

URL to open (e.g., https://example.com or myapp://path)

Take a screenshot from an iOS simulator. Returns the image plus a screen-state summary: active route (name + navigation stack), overlay-grouped tappable elements (pressables behind an open sheet/modal are excluded), component names as JSX tags, labels, testIDs, and frames — all in ready-to-tap pixel coordinates. Prefer tap(text="...") when text is exact and unique; otherwise use tap(x, y) with coordinates from the list — this is the most reliable way to tap icons or visually-identified elements. Use component names for inspect_component/find_components.
PURPOSE: Snapshot what the user sees on iOS AND get tap-ready pressables + a structured component map in one call.
WHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates.
WORKFLOW: ios_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> ios_screenshot to verify.
LIMITATIONS: Requires a booted iOS simulator (simctl). For physical devices or system dialogs without RN, combine with tap(..., native=true).
GOOD: ios_screenshot()
BAD: ios_screenshot({ udid: "guess" }) with a made-up UDID — run list_devices first.
SOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y) — it returns the absolute file and line.

Alias for `udid` — same accepted values. Provided for consistency with tap/get_screen_layout/get_screen_state, which all use `device`. If both are given, `udid` wins.

Optional path to save the screenshot. If not provided, saves to temp directory.

Terminate a running app on an iOS simulator
[PLATFORM-SPECIFIC — no cross-platform equivalent; use when force-terminating an iOS app]
PURPOSE: Force-kill an iOS app process so the next launch starts from a cold state.
WHEN TO USE: To reset app state fully (beyond what reload_app does), or before reinstalling a new build.

Bundle ID of the app to terminate

List globally available debugging objects in the connected app (Apollo, Redux, React DevTools, etc.).
PURPOSE: Enumerate the app's globalThis.* surface so you know which stores, clients, and debug hooks you can drill into.
WHEN TO USE: Start of a state-debugging session, or when you don't know whether the app exposes a Redux/Apollo/Zustand handle.
WORKFLOW: list_debug_globals -> inspect_global(objectName="...") -> execute_in_app for reads/mutations.
SDK INTEGRATION: When init({ stores, navigation, custom }) from execbro-sdk (formerly react-native-ai-devtools-sdk) was called, the response includes an sdk.paths array of dotted paths (e.g. __RN_AI_DEVTOOLS__.stores.redux). Pass them to inspect_global or execute_in_app.
RN NAMESPACE: The rn field reports globalThis.__rn__ — a curated set of seven RN modules (I18nManager, PixelRatio, Platform, StyleSheet, AppRegistry, NativeModules, Dimensions) populated by SDK exposeRnGlobals() or the executor's fallback bootstrap. Use paths like __rn__.Platform.OS. rn=null → bootstrap not yet run; keys=[] → ran but no match.
OUTPUT: { sdk: {...}|null, rn: {keys, hint}|null, categories: {...} }
LIMITATIONS: Only sees variables explicitly assigned to a global. Module-scoped state is invisible — expose it first or use the SDK.

RN device name (substring). Omit for default; see get_apps.

List every iOS simulator, Android emulator, and connected physical device on the host machine, in one structured response.
PURPOSE: Single discovery entry point. Returns booted+shutdown iOS sims (from simctl), running+stopped Android emulators (from `emulator -list-avds` cross-referenced with `adb devices`), and attached physical devices. Each row is enriched with `rnConnected` when an RN app from get_apps matches the same identifier.
WHEN TO USE: Before tap/swipe to pick a device, when a tool reports an ambiguous-device error, or to check whether a simulator is booted before targeting it.
WORKS WITHOUT RN: No Metro connection required. Safe to call before scan_metro.
WORKFLOW: list_devices -> tap({ device: '<udid-or-serial-or-name>', ... })
SEE ALSO: get_apps for RN-specific connection details (RN version, JS engine, network capture mode).

Force re-query of simctl/adb/emulator instead of returning cached results (5s TTL).

logbox

Interact with React Native's LogBox overlay (dev mode only). Actions: "dismiss" clears all entries and returns their content. "push" displays a message in the LogBox error banner (visible to the developer watching the device). "ignore" adds patterns to suppress future LogBox entries for this session. "detect" reads current LogBox state without modifying it. Only works in __DEV__ mode — LogBox does not exist in production builds.
PURPOSE: Control RN's on-device red/yellow overlay — clear it when it blocks UI, suppress noisy repeats, or push a message back to the developer.
WHEN TO USE: Screenshot/tap reports LogBox is obstructing the screen, an error banner prevents interaction, or you want to surface info to the dev watching the simulator.
WORKFLOW: logbox(action="detect") -> if present: logbox(action="dismiss") to read + clear -> continue UI work. Use action="ignore" with patterns to stop repeat noise.
LIMITATIONS: Dev-only — no effect in production builds. "push" at level="warning" won't show a banner unless LogBox is already open.
GOOD: logbox({ action: "dismiss" }); logbox({ action: "ignore", patterns: ["[APOLLO]"] })
BAD: Spamming logbox(action="push") for every tool step — annoys the developer.

Action to perform: "dismiss", "push", "ignore", or "detect"

When true, opens the full-screen LogBox detail view instead of the minimized bottom banner. Useful for important messages with clickable URLs (default: false)

LogBox level for push (default: "error"). Only "error" shows a visible bottom banner; "warning" is stored but not visually shown unless LogBox is already open

Message to push into LogBox (required when action="push")

Patterns to ignore (required when action="ignore"), e.g. ["[APOLLO]", "deprecated"]

Additional info shown in the call stack area when expanded=true (default: "MCP Server"). Use for context like "License Check", "Usage Limit", etc.

Where to push the message (default: "logbox"). "logbox" shows on device screen, "metro" outputs to Metro terminal via console.log

measure

Get on-screen geometry {x, y, width, height} for a named React component instance. Calls measureInWindow on the matched fiber (or its nearest host descendant for composite components). Coordinates are delivered-screenshot pixels, the same space as screenshots, get_screen_layout, get_screen_state, inspect_at_point and tap().
PURPOSE: One-shot, name-based component measurement — avoids hand-rolling fiber walks and Promise-wrapping measureInWindow callbacks in execute_in_app.
WHEN TO USE: You already know the component's display name (from get_screen_layout or find_components) and just need its current bounds — e.g. to verify a layout change, compute a tap target, or compare against design specs.
WORKFLOW: find_components(pattern="...") -> measure(componentName="...", index=N) -> tap(x, y) at the center, or inspect_at_point at the center to verify identity.
LIMITATIONS: Returns post-layout on-screen geometry only — for static style use find_components({ includeLayout: true }). For point-based lookup use inspect_at_point. Off-screen fibers may return zeros; that's the truth, not an error. Composites with multiple host descendants return the first host descendant's bounds.
GOOD: measure({ componentName: "SneakerCard", index: 0 })
BAD: measure({ componentName: "View" }) — too generic; narrow with find_components first.
SEE ALSO: inspect_at_point for point-based variant; find_components({ includeLayout: true }) for static style.

Exact React display name to match (same matcher as inspect_component).

0-based index when multiple instances match (default: 0).

navigate

Navigate the app's router directly, and verify it actually moved.
PURPOSE: Jump to a screen without tapping through the UI, with a settled before/after route check.
WHY THIS EXISTS: hand-written router calls report success whenever nothing throws. A path sent to a React Navigation ref changes nothing and warns only in LogBox, so a no-op reads as a success.
DESTINATIONS ARE NOT INTERCHANGEABLE: Expo Router takes paths ("/event-details?id=1"); React Navigation takes route names ("TarotNav"). The response reports which router resolved. Unknown React Navigation names are rejected before dispatch, with nearest-match suggestions.
WORKFLOW: navigate({ to: "/settings" }) -> check changed -> get_screen_state.
LIMITATIONS: push/replace are unavailable on a React Navigation root ref (stack-scoped). changed=false means it settled without moving; indeterminate=true means no settled reading.
GOOD: navigate({ to: "TarotNav" }); navigate({ action: "back" })
BAD: navigate({ to: "/TarotNav" }) on React Navigation — that is a path, not a route name.

Default "navigate".

Target device name (substring match).

Route params, passed as the second navigate argument.

Include the app's registered route names in the response.

Destination: a path for Expo Router, a route name for React Navigation. Required unless action is "back" or "reset".

Simulate offline or slow network for the running app.
PURPOSE: Reach the offline / timeout branches through the app's real code.
WHEN TO USE: 'what does this screen do with no network', 'is there a loading state', 'does the retry banner appear'.
WORKFLOW: network_condition({mode:"offline"}) -> reproduce -> network_condition({mode:"normal"}).
OFFLINE also patches NetInfo when installed, because many apps gate their offline UI on useNetInfo() rather than on a failed request. The result reports netInfo as patched / reads-patched-only / not-installed. Request failure works regardless.
LIMITATIONS: JS-originated HTTP only. Does not change the device's real connectivity. Leaves network_mock rules untouched.
GOOD: network_condition({mode:"slow", latencyMs:3000})
BAD: forgetting network_condition({mode:"normal"}) afterwards — it survives reload_app.

RN device name (substring). Omit for all devices; see get_apps.

Delay per request in slow mode.

offline fails every request; slow delays them; normal clears.

Intercept the app's HTTP requests and replace or modify the response.
PURPOSE: Drive the app down an error path through its REAL code — the request builder, the error branch, the retry, the toast. redux_dispatch writes the post-failure state directly and skips all of it.
WHEN TO USE: 'what does this screen do on a 500', 'what if this field is null', 'does the retry work'.
WORKFLOW: network_mock({action:"add", url:"/orders", status:500}) -> reproduce -> get_network_requests (rows show [MOCK m1]) -> network_mock({action:"clear"}).
MODES: replace returns a canned response; tamper fetches the real one and mutates it (set/remove take dotted paths).
MATCHING: url is a substring by default; wrap it in slashes for a regex ("/\\/orders\\/\\d+$/"). First matching rule wins, so add specific rules before broad ones.
LIMITATIONS: JS-originated HTTP only — native-module traffic (native SDKs, <Image> loading) is not intercepted. Rules are per-device and survive reload_app; clear them when done.
GOOD: network_mock({action:"add", url:"/orders", mode:"tamper", remove:["data.email"]})
BAD: leaving a rule active and then debugging why the app 'always fails' — check network_mock({action:"list"}) hit counts first.

What to do.

Response body (replace mode).

tamper: replace the whole body.

Delay before delivering.

Response headers (replace mode).

Rule id, for action="remove".

Restrict to one HTTP method.

replace = canned response; tamper = mutate the real one.

Fail the request instead of responding.

tamper: dotted paths to delete.

tamper: dotted path -> value, e.g. {"data.user.email": null}.

Response status. In tamper mode, overrides the real status.

Fire at most N times, then pass through. Use times:1 to test retry logic.

URL substring, or /regex/ when slash-wrapped.

Re-issue a request the app already made, optionally with changes.
PURPOSE: Retry a captured call without driving the UI back to the screen that made it — and vary one field at a time to find what the backend actually rejects.
WHEN TO USE: A request 4xx'd and you want to know whether it was the body, a header, or the endpoint. Or a flaky call you want to run again.
WORKFLOW: get_network_requests -> copy the id -> network_replay({requestId:"js-x1-7"}) -> network_replay({requestId:"js-x1-7", body:"{...}"}).
GOES THROUGH THE APP: same network stack, TLS trust, proxy and credentials as the original — cookies are attached by the native cookie jar, so a cookie-authenticated call replays as the logged-in user with no token handling. An active network_mock rule will intercept the replay too; the response says so when it does.
LIMITATIONS: ids come from get_network_requests and expire when the buffer rolls over or clear_network runs. Headers replace wholesale, they do not merge.
GOOD: network_replay({requestId:"js-x1-7", body:"{\"qty\":99}"})
BAD: guessing a requestId — read one from get_network_requests first.

Default "none": the captured headers already carry the original Authorization. Use "auto" to resolve a fresh token instead.

Replace the captured request body. Sent verbatim, already-encoded.

Target device name (substring match). Omit for the default device.

Replace the captured headers entirely (not merged).

Target size for the returned body in characters (default 25000).

Override the captured HTTP method.

Id of a captured request, from get_network_requests or search_network.

Override the captured URL.

RECOMMENDED: Use this tool FIRST when you need to find and tap UI elements. Takes a screenshot and extracts all visible text with tap-ready coordinates using OCR. ADVANTAGES over accessibility trees: (1) Works on ANY visible text regardless of accessibility labels, (2) Returns ready-to-use tapX/tapY coordinates - no conversion needed, (3) Faster than parsing accessibility hierarchies, (4) Works consistently across iOS and Android. USE THIS FOR: Finding buttons, labels, menu items, tab bars, or any text you need to tap. Simply find the text in the results and use its tapX/tapY with the tap command.
PURPOSE: Visually locate text on screen and return coordinates safe to pass straight into tap.
WHEN TO USE: Non-RN surfaces, third-party WebViews, accessibility-poor screens, or when fiber/testID strategies have failed.
WORKFLOW: ocr_screenshot(platform="ios") -> scan results for the label -> tap(x=tapX, y=tapY) -> ios_screenshot to verify.
LIMITATIONS: OCR accuracy degrades on very small or stylized text; icons with no label won't appear — use tap(component=...) instead.
GOOD: ocr_screenshot({ platform: "ios" })
BAD: ocr_screenshot used just to view the screen — plain ios_screenshot / android_screenshot is cheaper when you don't need OCR text.
SOURCE: for RN screens, inspect_at_point(x, y) returns the file and line that render an element — no OCR needed.

Optional device ID (Android) or UDID (iOS). Uses first available device if not specified.

Platform to capture screenshot from

pinch

Pinch-to-zoom using REAL two-finger touch events, with pixel-diff verification. ANDROID EMULATOR ONLY (iOS in progress).
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Zoom a map, image gallery, photo viewer, or any zoomable surface. pinch({ direction: "out" }) zooms in at screen centre; "in" zooms out. Pass x/y to zoom around a specific point.
HOW IT WORKS: Two independent contacts sent through the emulator's multi-touch bridge as real kernel touch events. It works below the app, so it drives React Native, native views, WebViews — anything on screen.
VERIFICATION: verify=true (default) returns `verification.meaningful` — false means nothing zoomed (not zoomable, already at a zoom limit, or the focal point missed).
WORKFLOW: pinch({ direction: "out" }) -> read verification.meaningful. Take x/y from get_screen_state or a screenshot; no conversion needed.
IF A GESTURE DOES NOTHING: lower `span` — the contacts may be landing on surrounding UI (a top bar, a bottom sheet) rather than the zoomable surface. direction="in" already defaults to a reduced span for this reason.
LIMITATIONS: Physical Android devices and iOS have no multi-touch channel and return an explicit error, never a partial result. Success means real fingers moved — this never fakes zoom by calling app code.

Axis the two fingers sit on, in degrees. 0 = horizontal (default), 90 = vertical.

Capture rapid sequential frames after the pinch to catch transient feedback (rubber-band snap-back at a zoom limit) even when the final state is unchanged. Frames land in the image buffer; retrieve with get_images(groupId=verification.burstGroupId).

Target device. Accepts (a) an Android adb serial like 'emulator-5554', (b) the emulator name (substring match), or (c) a connected RN app's deviceName (substring match against get_apps output). Omit when exactly one device is available. Call list_devices to enumerate.

"out" spreads the fingers apart and zooms IN (default). "in" brings them together and zooms OUT.

Gesture duration in milliseconds (default: 300).

How far the fingers travel, as a ratio between their start and end separation. Default 3. Large values are split automatically into several chained gestures.

Return the post-pinch image bytes in the response. Default true. Set to false to drop the PNG bytes — verification still runs (set verify=false to skip that too).

How much of the screen the gesture occupies, as a fraction 0-1. Defaults to 1 for direction="out" and 0.5 for direction="in", because a pinch-in starts with the fingers far apart and at span 1 they land at the screen extremes, where a top bar or bottom sheet takes the gesture. Raise it to zoom out further per gesture; lower it if a gesture still lands on surrounding UI. Does not change the zoom ratio — that is `scale`.

Compare before/after screenshots to detect whether the pinch produced a visual change. Set false to skip. When skipped, the response contains `verification: { skipped: true, skippedReason }`.

Focal point X in screenshot pixels — the point the zoom centres on. Default: screen centre.

Focal point Y in screenshot pixels — the point the zoom centres on. Default: screen centre.

Dispatch a Redux action to the store bound to the app's <Provider>, triggering useSelector subscribers and React re-renders. Resolves the live store by walking the React fiber tree on each call (no SDK setup needed; works even if no store was registered with init()).
PURPOSE: Drive state-controlled UI (loaders, modals, toasts, error overlays) without exercising the real flow (network, OTP, etc.).
WHY THIS EXISTS: __RN_AI_DEVTOOLS__.stores.redux often holds a different store reference than the one passed to <Provider>, so dispatching through it updates state but does NOT notify react-redux subscribers. This tool dispatches through the actual Provider store, so views re-render.
WHEN TO USE: Verify state-driven UI by seeding redux state directly. Example: dispatch app/setIsLoading: true, then ios_screenshot to confirm the loader rendered.
WORKFLOW: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } }) -> ios_screenshot -> redux_dispatch({ action: { type: 'app/setIsLoading', payload: false } }).
BATCH: action accepts an array — dispatched in order, one round trip.
LIMITATIONS: Requires React DevTools hook (dev mode). Action must be plain JSON-serializable (no thunks/functions). If the app has multiple <Provider> roots, pass storeIndex (default 0).
GOOD: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } })
BAD: redux_dispatch({ action: () => ... }) — actions must be plain objects; for thunks use execute_in_app to call your action creator.

Plain JSON-serializable Redux action object, e.g. { type: 'app/setIsLoading', payload: true }. Pass an ARRAY to dispatch several in order in one round trip — restoring a 17-field settings slice is one call, not 17.

RN device name (substring). Omit for default; see get_apps.

Optional dotted path into the post-dispatch state to return for verification (e.g. 'app' or 'auth.user'). Omit to skip returning state — keeps the response small. Use redux_get_state for ad-hoc reads.

Index of the Provider store to dispatch to when the app has multiple <Provider> roots (default: 0).

Read state from the Redux store bound to the app's <Provider>, resolved live via the fiber tree (same store redux_dispatch targets).
PURPOSE: Inspect the current app state without relying on __RN_AI_DEVTOOLS__.stores.redux (which may point at a different store instance than the Provider).
WHEN TO USE: Verify state shape before/after redux_dispatch, or check what slice keys exist before crafting an action.
WORKFLOW: redux_get_state() -> craft action -> redux_dispatch -> redux_get_state({ path: 'app' }) to confirm.
SHAPE FIRST: a large state comes back as its structure — every key path kept, arrays and objects annotated with real sizes, leaves clipped. Read it, then narrow.
NARROWING, TWO WAYS: path drills IN THE APP, so the rest of the state never crosses the wire — prefer it when you know the slice. query runs here and adds [0], [-1], [*] and quoted keys. They compose: path picks the slice, query picks the field inside it.
LIMITATIONS: Requires React DevTools hook (dev mode). State must be JSON-serializable; non-serializable values are replaced with an error marker.
GOOD: redux_get_state({ path: 'app' }) | redux_get_state({ path: 'cart', query: 'items[*].sku' })
BAD: redux_get_state({ path: 'app.isLoading.0' }) when isLoading is a boolean — path traversal returns undefined.

Byte target for the rendered state (default: 25000, 0 for unlimited). Bounded structurally, so size is traded for depth and array width rather than cutting the text off.

Optional dotted path into state (e.g. 'app' or 'auth.user'), resolved in-app so only that slice crosses the wire. Omit for the full state.

Dot-path into the state that came back, returned in full: "items[0].sku", "orders[*].status", "byId[\"a.b\"]". Applies after path. A path that matches nothing returns the shape plus what is actually there, not an error.

Index of the Provider store to read from when the app has multiple <Provider> roots (default: 0).

Reload the React Native app (triggers JavaScript bundle reload like pressing 'r' in Metro).
PURPOSE: Force a full JS bundle reload when Fast Refresh isn't enough — clears in-memory state and re-runs the bundle from scratch.
WHEN TO USE (only these cases): (1) native code, app.json, Info.plist, Podfile, or a native module changed; (2) Fast Refresh visibly failed (red-screen or stale render confirmed via screenshot after a few seconds); (3) the app is in a broken state; (4) you need to reset app state completely; (5) the user explicitly asks.
AVOID: reloading reflexively after JS/TS/TSX/style edits — Fast Refresh applies those in 1-2s. A reload discards navigation stack, context, hooks state, BLE/WebSocket connections, paired devices, and auth sessions, which can force re-pairing or re-login and break your verification loop.
WORKFLOW: screenshot → wait 2s for Fast Refresh → if still stale, reload_app. Auto-connects to Metro if no connection exists (with or without a device argument) — no need to call scan_metro first. After reload, wait a few seconds before running other tools.
SEE ALSO: get_refresh_status (did Fast Refresh accept?), get_bundle_status (did Metro compile?).

Target device name, substring match against the name shown by get_apps (a simulator UDID or adb serial also works). OMIT THIS unless several devices are connected — passing a name copied from list_devices that isn't attached to Metro is the most common cause of failure.

Scan for running Metro bundler servers and automatically connect to any found React Native apps. This is typically the FIRST tool to call when starting a debugging session - it establishes the connection needed for other tools like get_logs, list_debug_globals, execute_in_app, and reload_app.
PURPOSE: Discover Metro on ports 8081-8090 and auto-connect all React Native debugger targets it advertises.
WHEN TO USE: At the start of any session, or after the user restarts Metro / boots a new simulator.
WORKFLOW: scan_metro -> get_apps -> get_logs / ios_screenshot / tap.
GOOD: scan_metro()
BAD: scan_metro() called repeatedly in a loop — use ensure_connection to re-verify an existing connection.

End port for scanning (default: 8090). Metro allocates upward from 8081, so ten ports covers several apps side by side; widen only if your bundler is pinned elsewhere.

Start port for scanning (default: 8081)

Search console logs for text (case-insensitive).
PURPOSE: Find log lines matching a substring across the connected app's console buffer.
WHEN TO USE: User reports a known error/warning, or wants to trace a specific event (e.g., "redux", "auth failed"). For unfocused exploration, prefer get_logs.
WORKFLOW: scan_metro -> search_logs(text="...") -> if empty, get_logs to verify buffer populated.
LIMITATIONS: Only matches text captured AFTER the app connected; won't find pre-connect logs.
GOOD: search_logs({ text: "TypeError" })
BAD: search_logs({ text: "" })  (use get_logs for a raw dump)

RN device name (substring). Omit for all devices; see get_apps.

Max characters per message (default: 500, set to 0 for unlimited)

Maximum number of results to return (default: 50)

Text to search for in log messages

Disable all truncation and return full messages

Search network requests by URL pattern (case-insensitive).
PURPOSE: Filter the network buffer to requests whose URL matches a substring — fast way to find a specific endpoint in a noisy app.
WHEN TO USE: You know part of the URL (e.g., "/graphql", "users", a domain) and want matching requests across all devices.
WORKFLOW: search_network(urlPattern="/api/") -> get_request_details(requestId="...") for full headers/body.
LIMITATIONS: Matches URL only; for method/status/body filtering use get_network_requests. Bodies are only present when the SDK is installed.
GOOD: search_network({ urlPattern: "/graphql" })
BAD: search_network({ urlPattern: "" }) — empty pattern matches everything; use get_network_requests instead.

URL pattern to search for. Also matches GraphQL operation names (e.g. "GetCharacters") — use the operation name to find one GraphQL call among many sharing the same endpoint.

Report feedback about the ExecBro MCP tools THEMSELVES — a tool (tap, get_screen_layout, get_logs, etc.) that behaved incorrectly, was confusing, was missing, or could work better. This is EXCLUSIVELY about your experience operating ExecBro's debugging tools. It is NOT for bugs in the user's app under test, and NOT for the feature or task you were working on in this session — keep that out of the report entirely. Auto-collects environment info. Returns a pre-filled GitHub issue URL and formatted issue body. Ask the user to open the URL and paste the body to submit.

What about ExecBro's tools went wrong or could be better: which tool, what you expected it to do, what it actually did. Do NOT describe the app feature or task you were debugging — only the tool's behavior.

Short summary of the ExecBro tooling issue (becomes the GitHub issue title)

Type, scoped to ExecBro tooling: "bug" = an ExecBro tool malfunctioned, "feature_request" = a missing ExecBro capability, "feedback" = general notes on using the ExecBro tools

Which ExecBro tools were in use when the issue surfaced (e.g. "tap → get_screen_layout retry loop"). Name the tools and the debugging step — not the user's app goal.

swipe

Swipe gesture that auto-routes to the correct platform (iOS or Android), with pixel-diff verification.
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Single unified swipe entry point. Easiest form: swipe({ direction: "up" }) scrolls to reveal more content ("down"/"left"/"right" also work; bare swipe() defaults to "up"). Optional distance in screenshot pixels (default 33% of axis). For precise control, pass all four coordinates (startX/startY/endX/endY) — they take precedence over direction.
WHEN TO USE: Scrolling lists, paging carousels, pull-to-refresh, dismissing sheets, opening drawers. Especially useful in virtualized lists (FlatList/SectionList) where off-screen items aren't mounted in the fiber tree.
VERIFICATION: verify=true (default) returns `verification.meaningful`. When false, `warning` names the cause — already at top/end, not scrollable, wrong axis, no scroll view there, or (no RN connection) that it could not inspect the screen. burst=true catches transient feedback like overscroll bounce.
SAFETY: Android direction swipes stay clear of the system bars; `foregroundLost` appears if the app left the foreground anyway.
WORKFLOW: swipe({ direction: "up" }) -> read response.verification.meaningful.
LIMITATIONS: iOS needs AXe (brew install cameroncooke/axe/axe) or IDB. Pass `device` to target a specific device — call list_devices for the inventory.

Capture rapid sequential frames after the swipe to detect transient feedback (overscroll bounce, fling-then-snap-back) even when the final state is unchanged. Frames are stored in the image buffer; use get_images(groupId=verification.burstGroupId) to retrieve them.

iOS only — touch step size between events (driver-dependent default). Ignored on Android.

Target device. Accepts (a) an iOS simulator UDID, (b) an Android adb serial like 'emulator-5554', (c) the iOS simulator or Android emulator/device name (substring match), or (d) a connected RN app's deviceName (substring match against get_apps output). Omit when exactly one device is available. Call list_devices to enumerate.

Shorthand for a centered scroll gesture (content-scroll semantics): "up" reveals content below (finger moves bottom→top), "down" reveals content above, "left"/"right" page horizontally. A bare swipe() with no params defaults to "up". Ignored when all four explicit coordinates are provided.

Travel length in screenshot pixels for the direction shorthand. Default: 33% of the relevant screen axis.

Swipe duration in milliseconds (default: 300 on Android; iOS uses driver default if omitted)

Ending X coordinate in screenshot pixels (explicit-coordinate mode)

Ending Y coordinate in screenshot pixels (explicit-coordinate mode)

Return the post-swipe image bytes in the response. Default true. Set to false to drop the PNG bytes — verification still runs (set verify=false to skip that too).

Starting X coordinate in screenshot pixels (explicit-coordinate mode)

Starting Y coordinate in screenshot pixels (explicit-coordinate mode)

Compare before/after screenshots to detect whether the swipe produced a visual change. Set false to skip. When skipped, the response contains `verification: { skipped: true, skippedReason }` so callers can tell apart "ran clean" from "never ran".

tap

Tap a UI element. Automatically tries multiple strategies: fiber tree (React), accessibility tree (native), and OCR (visual).
[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]
PURPOSE: Single unified tap entry point — resolves text/testID/component/coordinates into a real touch event on the correct device.
WHEN TO USE: Any time you need to press a button, focus an input, open a menu, or verify a handler fires. Prefer testID, then text, then component, then (x,y) from a screenshot's pressables list.
WORKFLOW: ios_screenshot or android_screenshot -> tap(testID="...") | tap(text="...") | tap(x, y) -> screenshot again to verify. Use burst=true when meaningful=false but visual feedback looks transient.
LIMITATIONS: iOS needs AXe (brew install cameroncooke/axe/axe) or IDB for accessibility/coordinate taps. Non-ASCII text skips fiber (Hermes); prefer testID. Pass `device` to target a specific simulator/emulator when multiple are available — call list_devices for the inventory.
GOOD: tap({ testID: "login-btn" }); tap({ text: "Submit" }); tap({ x: 300, y: 600 }); tap({ x: 300, y: 600, native: true, device: "emulator-5554" })
LONG PRESS: tap({ testID: "row-3", duration: 800 }) holds the touch.
BAD: tap({ text: "" }) or tap({ x: 0, y: 0 }) — missing a target. tap({ text: "Submit" }) without first screenshotting an ambiguous screen.
SOURCE: need the file:line that renders an element? inspect_at_point(x, y).

Enable burst screenshot capture for enhanced verification. Captures 4 rapid screenshots (~150ms intervals) after the tap to detect transient visual feedback (press animations, highlights, ripples) that may settle before a standard after-screenshot. Results are stored in the image buffer (use get_images to inspect individual frames). Default: false.

Component name match (case-insensitive substring, e.g. 'Button', 'MenuItem').

Hold the touch for this many milliseconds instead of releasing it immediately — a long press. Use for context menus, drag starts, multi-select. React Native fires onLongPress at 500ms, so 800 is a safe default and anything under 500 will not trigger it. Omit for a normal tap. The response carries `longPress.handlerFound`: true/false when the fiber strategy inspected the element, null when the strategy that resolved it (accessibility, OCR, coordinates) cannot see handlers.

Zero-based index when multiple elements match (default: 0).

Max parent levels to traverse when searching by component name (default: 15). Increase if your component is deeply wrapped (e.g. inside multiple HOCs/animation wrappers).

When true, tap coordinates directly via ADB/simctl without requiring a React Native connection. Useful for interacting with native UI, system dialogs, or non-RN apps. Requires x/y coordinates.

Return post-tap image bytes in the response. Default true. Set to false to drop the PNG bytes — verification still runs (set verify=false to skip that too). Combine with verify=true to get the meaningful/changeRate signal without paying the ~1MB-per-tap bandwidth cost.

"auto" (default) tries fiber -> accessibility -> OCR. Set explicitly to skip strategies you know will fail.

Exact match on the element's testID prop. Also resolves Switch/checkbox elements (onValueChange), which have no onPress — the response then carries `switch.before/after/changed`, read back from the element after the gesture. Read it: a pixel diff looks identical for a correct toggle and one that flipped the neighbouring row.

Visible text to match (case-insensitive substring). ASCII only for fiber strategy; OCR handles non-ASCII.

Run before/after screenshot diff to detect if the tap had a meaningful visual effect. Default: true for coordinate/accessibility/ocr strategies, false for fiber. Independent of `screenshot` — verify can run with screenshot=false (the diff is computed internally; image bytes are dropped). When skipped, the response contains `verification: { skipped: true, skippedReason }` so callers can tell apart "ran clean" from "never ran".

X coordinate in pixels (from screenshot). Must provide both x and y.

Y coordinate in pixels (from screenshot). Must provide both x and y.