Public log

execbro

ExecBro: React Native AI debugger and devtools MCP server. Gives AI agents eyes and hands in your running app (logs, network, state inspection, tap, screenshot).

npm · fingerprint f882e22f89f5e9510e20cc1d · repository · RSS

67tools
3recorded versions
45dtracked
changed 2d ago

What changed

Changed 2026-10-15 11:24 UTC

3 schema or annotation changes with the description left byte-identical.

--- pinned/navigate
+++ observed/navigate
  {
    "description": "Navigate the app's router directly, and verify it actually moved.\nPURPOSE: Jump to a screen without tapping through the UI, with a settled before/after route check.\nWHY 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.\nDESTINATIONS 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.\nWORKFLOW: navigate({ to: \"/settings\" }) -> check changed -> get_screen_state.\nLIMITATIONS: 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.\nDISCOVERY: navigate({ routeTable: true }) with no destination just LISTS the registered route names — call it first when you do not know them.\nNATIVE ANDROID: on an app with execbro-sdk-android, drives its Jetpack Navigation NavController. to is a route pattern (\"settings\", \"detail/{id}\") and params fill its placeholders.\nGOOD: navigate({ to: \"TarotNav\" }); navigate({ action: \"back\" }); navigate({ routeTable: true })\nBAD: navigate({ to: \"/TarotNav\" }) on React Navigation — that is a path, not a route name.",
    "execution": {
+       "screen": {
+         "description": "Alias for `to`.",
+         "type": "string"
+       },
        "to": {
          "description": "Destination: a path for Expo Router, a route name for React Navigation. Required unless action is \"back\" or \"reset\".",
          "type": "string"
--- pinned/search_logs
+++ observed/search_logs
  {
    "description": "Search console logs for text (case-insensitive).\nPURPOSE: Find log lines matching a substring across the connected app's console buffer.\nWHEN 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.\nWORKFLOW: scan_metro -> search_logs(text=\"...\") -> if empty, get_logs to verify buffer populated.\nLIMITATIONS: Only matches text captured AFTER the app connected; won't find pre-connect logs.\nGOOD: search_logs({ text: \"TypeError\" })\nBAD: search_logs({ text: \"\" })  (use get_logs for a raw dump)\n",
    "execution": {
+       "query": {
+         "description": "Alias for `text`.",
+         "type": "string"
+       },
        "text": {
          "description": "Text to search for in log messages",
          "type": "string"
-     "required": [
-       "text"
-     ],
      "type": "object"
    },
    "name": "search_logs"
--- pinned/search_network
+++ observed/search_network
  {
    "description": "Search network requests by URL pattern (case-insensitive).\nPURPOSE: Filter the network buffer to requests whose URL matches a substring — fast way to find a specific endpoint in a noisy app.\nWHEN TO USE: You know part of the URL (e.g., \"/graphql\", \"users\", a domain) and want matching requests across all devices.\nWORKFLOW: search_network(urlPattern=\"/api/\") -> get_request_details(requestId=\"...\") for full headers/body.\nLIMITATIONS: Matches URL only; for method/status/body filtering use get_network_requests. Bodies are only present when the SDK is installed.\nGOOD: search_network({ urlPattern: \"/graphql\" })\nBAD: search_network({ urlPattern: \"\" }) — empty pattern matches everything; use get_network_requests instead.\n",
    "execution": {
+       "query": {
+         "description": "Alias for `urlPattern`.",
+         "type": "string"
+       },
        "urlPattern": {
          "description": "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.",
          "type": "string"
-     "required": [
-       "urlPattern"
-     ],
      "type": "object"
    },
    "name": "search_network"

Changed 2026-10-05 14:24 UTC

18 descriptions rewritten, 3 schema or annotation changes with the description left byte-identical, 8 tools added, 1 tool removed.

--- pinned/android_screenshot
+++ observed/android_screenshot
  {
-   "description": "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.\nPURPOSE: Snapshot what the user sees on Android AND get tap-ready pressables + a structured component map in one call.\nWHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates on Android.\nWORKFLOW: android_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> android_screenshot to verify.\nLIMITATIONS: Requires adb in PATH and a running device/emulator. For non-RN surfaces (system dialogs, permission prompts), combine with tap(..., native=true).\nGOOD: android_screenshot()\nBAD: android_screenshot({ deviceId: \"guess\" }) with a made-up serial — run list_devices first.\nSOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y).\n",
+   "description": "PURPOSE: Legacy Android-only alias of screenshot, kept so older prompts keep working. Prefer screenshot({ device }): same capture and pressables summary, on every platform.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/app_request
+++ observed/app_request
  {
-   "description": "Issue an HTTP request from inside the running app, as the logged-in user.\nPURPOSE: Probe your backend through the app's real network stack, TLS trust and proxy config — without pasting credentials into the conversation.\nWHY 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.\nWHEN TO USE: reproduce a 4xx, check what an endpoint returns for an edge case, clean up test records the UI can't reach.\nWORKFLOW: app_request({ method: \"GET\", url: \"https://api.example.com/me\" }) -> inspect status + body.\nAUTH 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.\nLIMITATIONS: needs a connected app. Pass an explicit Authorization only when every step above misses — it puts the credential in the transcript.\nGOOD: app_request({ method: \"DELETE\", url: \"https://api.example.com/address/17\" })\nBAD: embedding a bearer token in an execute_in_app expression — it lands in the transcript.",
+   "description": "Issue an HTTP request from inside the running app, as the logged-in user.\nPURPOSE: Probe your backend through the app's real network stack, TLS trust and proxy config — without pasting credentials into the conversation.\nWHEN TO USE: reproduce a 4xx, check what an endpoint returns for an edge case, clean up test records the UI can't reach.\nWORKFLOW: app_request({ method: \"GET\", url: \"https://api.example.com/me\" }) -> inspect status + body.\nAUTH 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.\nLIMITATIONS: needs a connected app. Pass an explicit Authorization only when every step above misses — it puts the credential in the transcript.\nGOES THROUGH THE APP: same TLS trust, proxy, cookie jar and credentials, so an active network_mock rule intercepts it too. For a clean request from your own machine — to tell a server bug from a client one — use http_request.\nGOOD: app_request({ method: \"DELETE\", url: \"https://api.example.com/address/17\" })\nBAD: embedding a bearer token in an execute_in_app expression — it lands in the transcript.",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/connect_metro
+++ observed/connect_metro
  {
-   "description": "Connect to a Metro server on a specific port — the only way to reach a port scan_metro does not probe.\nPURPOSE: Establish a CDP WebSocket connection to a Metro server on a known port.\nWHEN 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.\nSEE ALSO: scan_metro for auto-discovery; get_apps afterwards to confirm the device attached.",
+   "description": "Connect to a debug port directly: a Metro server outside 8081-8090, or a Chromium/Electron target (e.g. port 9222, which scan_metro lists but does not auto-connect). Also connects a native Android app with execbro-sdk-android by package name.\nPURPOSE: Establish a CDP WebSocket connection to the targets on one known port.\nWHEN TO USE: Metro is on a port outside 8081-8090, or you want to attach a Chromium target scan_metro listed. For Metro in that range, scan_metro is strictly better.\nNATIVE ANDROID: connect_metro({ device: \"com.example.app\" }) connects a native app with execbro-sdk-android directly by package, for devices where scan_metro cannot list sockets.\nGOOD: connect_metro({ port: 9222, device: \"FluentTalk\" })\nSEE ALSO: scan_metro for auto-discovery; get_apps afterwards to confirm the device attached.",
    "execution": {
      "taskSupport": "forbidden"
    },
+       "device": {
+         "description": "Connect only targets whose name contains this (case-insensitive). Omit on a Metro port to connect every target; required on a port with several Chromium targets (each connect injects a network interceptor into the page).",
+         "type": "string"
+       },
        "port": {
          "default": 8081,
-         "description": "Metro server port (default: 8081)",
+         "description": "Debug port (default: 8081)",
          "type": "number"
        }
      },
--- pinned/dismiss_keyboard
+++ observed/dismiss_keyboard
  {
-   "description": "Blur the currently focused TextInput, dismissing the on-screen keyboard.\nPURPOSE: 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.\nWHEN 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.\nPREREQUISITE: A TextInput must already have React focus. Tap the field first (e.g. tap({ testID: 'search' })).\nLIMITATIONS: Requires Bridgeless/Fabric (RN new architecture). Returns 'no focused TextInput' if nothing is focused.",
+   "description": "Blur the currently focused TextInput, dismissing the on-screen keyboard.\nPURPOSE: 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.\nWHEN 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.\nPREREQUISITE: A TextInput must already have React focus. Tap the field first (e.g. tap({ testID: 'search' })).\nLIMITATIONS: Requires Bridgeless/Fabric (RN new architecture). With nothing focused it reports the keyboard already down; if the keyboard is up anyway, a native field owns it and only a platform dismiss will close it.",
    "execution": {
      "taskSupport": "forbidden"
    },

+ tool added: electron_launch_app

--- pinned/ensure_connection
+++ observed/ensure_connection
  {
-   "description": "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.\nPURPOSE: Health-check the existing CDP connection and transparently reconnect if it has gone stale, without rescanning all Metro ports.\nWHEN 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.\nWORKFLOW: scan_metro (once) -> ensure_connection(healthCheck=true) -> resume tool calls. Use forceRefresh=true if the first probe still looks dead.\nGOOD: ensure_connection({ healthCheck: true })\nBAD: ensure_connection() before scan_metro has ever run — call scan_metro first.\n",
+   "description": "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.\nPURPOSE: Health-check the existing CDP connection and transparently reconnect if it has gone stale, without rescanning all Metro ports.\nWHEN 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.\nWORKFLOW: scan_metro (once) -> ensure_connection(healthCheck=true) -> resume tool calls. Use forceRefresh=true if the first probe still looks dead.\nCOLD LAUNCH: right after ios_launch_app / android_launch_app, or when scan_metro says Metro is running but no app has attached, use ensure_connection({ waitMs: 60000 }): it returns as soon as the app's JS runtime attaches. Waits only when nothing is connected yet.\nGOOD: ensure_connection({ healthCheck: true })\nBAD: ensure_connection() before scan_metro has ever run — call scan_metro first.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
+       },
+       "waitMs": {
+         "default": 0,
+         "description": "When Metro is up but no app has attached yet (cold launch, bundle still loading), keep polling up to this long and return as soon as one attaches. 60000 covers most cold bundles. Default: 0 (no wait).",
+         "maximum": 300000,
+         "minimum": 0,
+         "type": "number"
        }
      },
      "type": "object"
--- pinned/execute_in_app
+++ observed/execute_in_app
  {
    "description": "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.\n\nRECOMMENDED 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.\n\nLIMITATIONS (Hermes engine):\n- NO require() or import — only pre-existing globals are available\n- 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.\n- 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`.\n- Non-ASCII in string literals (emoji, Arabic, CJK) is auto-escaped server-side. Write it as-is.\n\nGOOD examples: `__DEV__`, `__APOLLO_CLIENT__.cache.extract()`, `__EXPO_ROUTER__.navigate('/settings')`\nBAD examples: `await fetch(...)` (bare top-level await), `require('react-native')`\nPass timeoutMs (ms) for long-running expressions; capped at 120000. Auto-reconnect surfaces _meta.reconnected when a transport drop was self-healed.\n",
    "execution": {
+       "code": {
+         "description": "Alias for `expression`.",
+         "type": "string"
+       },
        "collect": {
          "description": "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.",
          "type": "string"
+       "script": {
+         "description": "Alias for `expression`.",
+         "type": "string"
+       },
        "timeoutMs": {
          "description": "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.",
          "type": "number"
--- pinned/get_apps
+++ observed/get_apps
  {
-   "description": "List currently connected React Native apps and their connection status. If no apps are connected, run scan_metro first to establish a connection.\nPURPOSE: Enumerate active debug targets with device names, platforms, ports, and detected RN/Expo versions so you can target the right one.\nWHEN TO USE: After scan_metro to confirm what connected, or before passing a device=\"...\" filter to another tool.\nWORKFLOW: scan_metro -> get_apps -> get_logs / ios_screenshot / tap (with device=\"...\" if multiple).\nLIMITATIONS: Only lists devices the MCP has successfully connected to — stale targets don't appear here, use get_connection_status for health details.\nGOOD: get_apps()\nBAD: Calling get_apps in a tight loop — the list doesn't change without a scan_metro or disconnect_metro.\n",
+   "description": "List currently connected React Native apps and their connection status. If no apps are connected, run scan_metro first to establish a connection.\nPURPOSE: Enumerate active debug targets with device names, platforms, ports, and detected RN/Expo versions so you can target the right one.\nWHEN TO USE: After scan_metro to confirm what connected, or before passing a device=\"...\" filter to another tool.\nWORKFLOW: scan_metro -> get_apps -> get_logs / screenshot / tap (with device=\"...\" if multiple).\nLIMITATIONS: Only lists devices the MCP has successfully connected to — stale targets don't appear here, use get_connection_status for health details.\nGOOD: get_apps()\nBAD: Calling get_apps in a tight loop — the list doesn't change without a scan_metro or disconnect_metro.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/get_images
+++ observed/get_images
  {
-   "description": "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.\nPURPOSE: Retrieve prior screenshots — especially tap burst frames — without re-taking them, for visual diffing or reviewing transient UI states.\nWHEN TO USE: After tap(burst=true) reports transientChangeDetected, or to compare before/after frames without another screenshot round-trip.\nWORKFLOW: tap(burst=true) -> note verification.burstGroupId -> get_images(groupId, frameIndex=N) to inspect individual frames.\nLIMITATIONS: Circular buffer (50 entries) — old images are evicted. Metadata is cheap; fetching image data is not — request specific ids, not bulk.\nGOOD: get_images({ list: true }); get_images({ groupId: \"burst-abc\", frameIndex: 2 })\nBAD: get_images() with no filter when buffer is full — floods context. Use list:true or last:N first.\n",
+   "description": "Access the shared image buffer containing screenshots from all tools (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.\nPURPOSE: Retrieve prior screenshots — especially tap burst frames — without re-taking them, for visual diffing or reviewing transient UI states.\nWHEN TO USE: After tap(burst=true) reports transientChangeDetected, or to compare before/after frames without another screenshot round-trip.\nWORKFLOW: tap(burst=true) -> note verification.burstGroupId -> get_images(groupId, frameIndex=N) to inspect individual frames.\nLIMITATIONS: Circular buffer (50 entries) — old images are evicted. Metadata is cheap; fetching image data is not — request specific ids, not bulk.\nGOOD: get_images({ list: true }); get_images({ groupId: \"burst-abc\", frameIndex: 2 })\nBAD: get_images() with no filter when buffer is full — floods context. Use list:true or last:N first.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/get_request_details
+++ observed/get_request_details
  {
    "description": "Get full details of one network request: headers, body, status, timing.\nPURPOSE: Drill into a single entry after get_network_requests or search_network returns a suspect id.\nWORKFLOW: get id -> read the shape -> get_request_details({ requestId, query }) for the subtree you need.\nSHAPE 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'.\nQUERY: 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.\nSIDES: 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.\nLIMITATIONS: Bodies need the execbro-sdk; CDP-only targets have no response body. Non-JSON is clipped at maxBodyLength, not projected.\nGOOD: get_request_details({ requestId: \"42\", query: \"data.approvals.single.basicInfo\" }) | query: \"errors[*].message\"\nBAD: verbose:true to find one field — that is the 40KB dump this avoids. Or guessing requestIds.\n",
    "execution": {
-         "description": "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.",
+         "description": "Return bodies raw and unbounded. Does NOT reveal secrets — credential headers, tokens in bodies and tokens in URLs stay redacted regardless; that is deliberate and only EXECBRO_REDACT=off lifts it. Prefer query — verbose on a large JSON body is the 40KB dump this tool exists to avoid.",
          "type": "boolean"
        }
      },
--- pinned/get_screen_state
+++ observed/get_screen_state
  {
-   "description": "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.\n\nWHEN 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.\nCOORDINATES: 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.\nLIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection.\nHISTORY: includeHistory=true appends the route trail (dwell + origin per screen).\nPARAMS: route params listed by key; fullParams=true adds values.\nSOURCE: 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).\nSEE ALSO: get_screen_layout for the full component tree; this is a flat, tap-ready content list.",
+   "description": "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.\n\nWHEN 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.\nCOORDINATES: delivered-screenshot pixels — the same space as screenshot, get_screen_layout, inspect_at_point, measure and tap(). Pass them through unchanged; never scale by devicePixelRatio yourself.\nLIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection.\nHISTORY: includeHistory=true appends the route trail (dwell + origin per screen).\nPARAMS: route params listed by key; fullParams=true adds values.\nSOURCE: 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).\nSEE ALSO: get_screen_layout for the full component tree; this is a flat, tap-ready content list.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "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.",
+         "description": "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. On chromium (Electron / Chrome) the route is the page URL, its query string the params, and elements come from the DOM.",
          "type": "boolean"
        },
        "fullText": {
-         "description": "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.",
+         "description": "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. Not recorded on chromium targets.",
          "type": "boolean"
        },
        "pressablesOnly": {

+ tool added: handle_dialog

+ tool added: http_request

--- pinned/input_text
+++ observed/input_text
  {
    "description": "Write text into a React Native TextInput, or the OS's currently focused field, and verify it landed.\n[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]\nPURPOSE: Set a field's text and confirm, by reading the value back, that it holds exactly what you sent.\nWHEN 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.\nWORKFLOW: get_screen_state -> input_text({ testID, text }) -> read `verified`.\nVERIFICATION: the write is read back and compared EXACTLY. A mismatch retries once, then fails with `sent` vs `landed`.\nAMBIGUITY: several matching inputs -> the tool refuses and returns a numbered candidate list; pick one with `index`.\nNATIVE 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.\nLIMITATIONS: 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.\nGOOD: input_text({ testID: \"new-topic-title\", text: \"Q3 budget\", replace: true })\nGOOD: input_text({ text: \"1234\", native: true }) — a system PIN prompt, no RN screen behind it.\nBAD: input_text({ text: \"...\" }) with nothing focused — pass a target instead.\n",
    "execution": {
-         "description": "Target the input with this testID. Most reliable — the tool focuses it itself, no prior tap needed.",
+         "description": "Target the input with this testID. Most reliable — the tool focuses it itself, no prior tap needed. On a chromium (Electron / Chrome) target it matches data-testid, data-test-id or id, and inputs, textareas and contenteditable elements are searched; the text is written with CDP Input.insertText and read back.",
          "type": "string"
        },
        "text": {
--- pinned/inspect_global
+++ observed/inspect_global
  {
-   "description": "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.\nPURPOSE: 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.\nWHEN TO USE: After list_debug_globals identifies a promising global and before you try execute_in_app on it.\nWORKFLOW: list_debug_globals -> inspect_global(objectName=\"__APOLLO_CLIENT__\") -> execute_in_app(\"__APOLLO_CLIENT__.cache.extract()\").\nDOTTED 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.\nLIMITATIONS: 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.\nWIDE OBJECTS: a global with hundreds of keys is bounded structurally rather than dumped; use query to pull one entry in full.\nGOOD: inspect_global({ objectName: \"__APOLLO_CLIENT__\" }) | inspect_global({ objectName: \"__RN_AI_DEVTOOLS__.stores.redux\", query: \"cache\" })\nBAD: inspect_global({ objectName: \"store.getState()\" }) — call expressions aren't supported; use execute_in_app.\n",
+   "description": "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.\nPURPOSE: 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.\nWHEN TO USE: After list_debug_globals identifies a promising global and before you try execute_in_app on it.\nWORKFLOW: list_debug_globals -> inspect_global(objectName=\"__APOLLO_CLIENT__\") -> execute_in_app(\"__APOLLO_CLIENT__.cache.extract()\").\nDOTTED 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.\nLIMITATIONS: 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.\nWIDE OBJECTS: a global with hundreds of keys is bounded structurally rather than dumped; use query to pull one entry in full.\nGOOD: inspect_global({ objectName: \"__APOLLO_CLIENT__\" }) | inspect_global({ objectName: \"__RN_AI_DEVTOOLS__.stores.redux\", query: \"cache\" })\nBAD: inspect_global({ objectName: \"store.getState()\" }) — call expressions aren't supported; use execute_in_app.\nNATIVE: pass a registered state name.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/ios_open_url
+++ observed/ios_open_url
  {
-   "description": "Open a URL in the iOS simulator (opens in default handler or Safari).\nPURPOSE: Drive an iOS simulator into a deep link or universal link entry point so you can exercise routing from an external entry.\nWHEN TO USE: Testing deep-link handlers, universal link routing, OAuth/SSO callback URLs, or any flow that enters the app via a URL.\nWORKFLOW: 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.\nGOOD: ios_open_url(url=\"myapp://product/42\") to land directly on a product screen.\nBAD: ios_open_url(url=\"...\") used as a substitute for in-app navigation when the user would normally tap — prefer `tap` for normal interaction flows.\n\n[PLATFORM-SPECIFIC — no cross-platform equivalent; use when testing iOS deep links or universal links]",
+   "description": "Open a URL in the iOS simulator (opens in default handler or Safari).\nPURPOSE: Drive an iOS simulator into a deep link or universal link entry point so you can exercise routing from an external entry.\nWHEN TO USE: Testing deep-link handlers, universal link routing, OAuth/SSO callback URLs, or any flow that enters the app via a URL.\nWORKFLOW: ios_boot_simulator -> ios_launch_app (or have the app running) -> ios_open_url -> screenshot / get_screen_layout to verify the target screen rendered.\nGOOD: ios_open_url(url=\"myapp://product/42\") to land directly on a product screen.\nBAD: ios_open_url(url=\"...\") used as a substitute for in-app navigation when the user would normally tap — prefer `tap` for normal interaction flows.\n\n[PLATFORM-SPECIFIC — no cross-platform equivalent; use when testing iOS deep links or universal links]",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/ios_screenshot
+++ observed/ios_screenshot
  {
-   "description": "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.\nPURPOSE: Snapshot what the user sees on iOS AND get tap-ready pressables + a structured component map in one call.\nWHEN TO USE: Any visual verification, before/after comparison, or as the starting point for tapping UI by coordinates.\nWORKFLOW: ios_screenshot -> pick element from pressables -> tap(x, y) or tap(testID=...) -> ios_screenshot to verify.\nLIMITATIONS: Requires a booted iOS simulator (simctl). For physical devices or system dialogs without RN, combine with tap(..., native=true).\nGOOD: ios_screenshot()\nBAD: ios_screenshot({ udid: \"guess\" }) with a made-up UDID — run list_devices first.\nSOURCE: to jump from a pixel to the code that renders it, call inspect_at_point(x, y) — it returns the absolute file and line.\n",
+   "description": "PURPOSE: Legacy iOS-only alias of screenshot, kept so older prompts keep working. Prefer screenshot({ device }): same capture and pressables summary, on every platform.\n",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/list_debug_globals
+++ observed/list_debug_globals
  {
-   "description": "List globally available debugging objects in the connected app (Apollo, Redux, React DevTools, etc.).\nPURPOSE: Enumerate the app's globalThis.* surface so you know which stores, clients, and debug hooks you can drill into.\nWHEN TO USE: Start of a state-debugging session, or when you don't know whether the app exposes a Redux/Apollo/Zustand handle.\nWORKFLOW: list_debug_globals -> inspect_global(objectName=\"...\") -> execute_in_app for reads/mutations.\nSDK 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.\nRN 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.\nOUTPUT: { sdk: {...}|null, rn: {keys, hint}|null, categories: {...} }\nLIMITATIONS: Only sees variables explicitly assigned to a global. Module-scoped state is invisible — expose it first or use the SDK.\n",
+   "description": "List globally available debugging objects in the connected app (Apollo, Redux, React DevTools, etc.).\nPURPOSE: Enumerate the app's globalThis.* surface so you know which stores, clients, and debug hooks you can drill into.\nWHEN TO USE: Start of a state-debugging session, or when you don't know whether the app exposes a Redux/Apollo/Zustand handle.\nWORKFLOW: list_debug_globals -> inspect_global(objectName=\"...\") -> execute_in_app for reads/mutations.\nSDK 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.\nRN 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.\nOUTPUT: { sdk: {...}|null, rn: {keys, hint}|null, categories: {...} }\nLIMITATIONS: Only sees variables explicitly assigned to a global. Module-scoped state is invisible — expose it first or use the SDK.\nNATIVE ANDROID: on an app with execbro-sdk-android, lists the state and commands the app registered.\n",
    "execution": {
      "taskSupport": "forbidden"
    },

+ tool added: list_secrets

--- pinned/navigate
+++ observed/navigate
  {
-   "description": "Navigate the app's router directly, and verify it actually moved.\nPURPOSE: Jump to a screen without tapping through the UI, with a settled before/after route check.\nWHY 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.\nDESTINATIONS 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.\nWORKFLOW: navigate({ to: \"/settings\" }) -> check changed -> get_screen_state.\nLIMITATIONS: 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.\nGOOD: navigate({ to: \"TarotNav\" }); navigate({ action: \"back\" })\nBAD: navigate({ to: \"/TarotNav\" }) on React Navigation — that is a path, not a route name.",
+   "description": "Navigate the app's router directly, and verify it actually moved.\nPURPOSE: Jump to a screen without tapping through the UI, with a settled before/after route check.\nWHY 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.\nDESTINATIONS 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.\nWORKFLOW: navigate({ to: \"/settings\" }) -> check changed -> get_screen_state.\nLIMITATIONS: 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.\nDISCOVERY: navigate({ routeTable: true }) with no destination just LISTS the registered route names — call it first when you do not know them.\nNATIVE ANDROID: on an app with execbro-sdk-android, drives its Jetpack Navigation NavController. to is a route pattern (\"settings\", \"detail/{id}\") and params fill its placeholders.\nGOOD: navigate({ to: \"TarotNav\" }); navigate({ action: \"back\" }); navigate({ routeTable: true })\nBAD: navigate({ to: \"/TarotNav\" }) on React Navigation — that is a path, not a route name.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "Include the app's registered route names in the response.",
+         "description": "Include the app's registered route names in the response. Pass it alone, with no `to`, to just list them.",
          "type": "boolean"
        },
        "to": {
--- pinned/network_mock
+++ observed/network_mock
  {
-   "description": "Intercept the app's HTTP requests and replace or modify the response.\nPURPOSE: 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.\nWHEN TO USE: 'what does this screen do on a 500', 'what if this field is null', 'does the retry work'.\nWORKFLOW: network_mock({action:\"add\", url:\"/orders\", status:500}) -> reproduce -> get_network_requests (rows show [MOCK m1]) -> network_mock({action:\"clear\"}).\nMODES: replace returns a canned response; tamper fetches the real one and mutates it (set/remove take dotted paths).\nMATCHING: 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.\nLIMITATIONS: 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.\nGOOD: network_mock({action:\"add\", url:\"/orders\", mode:\"tamper\", remove:[\"data.email\"]})\nBAD: leaving a rule active and then debugging why the app 'always fails' — check network_mock({action:\"list\"}) hit counts first.",
+   "description": "Intercept the app's HTTP requests and replace or modify the response.\nPURPOSE: 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.\nWHEN TO USE: 'what does this screen do on a 500', 'what if this field is null', 'does the retry work'.\nWORKFLOW: network_mock({action:\"add\", url:\"/orders\", status:500}) -> reproduce -> get_network_requests (rows show [MOCK m1]) -> network_mock({action:\"clear\"}).\nMODES: replace returns a canned response; tamper fetches the real one and mutates it (set/remove take dotted paths).\nMATCHING: 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.\nLIMITATIONS: 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 (a bare clear covers every device).\nGOOD: network_mock({action:\"add\", url:\"/orders\", mode:\"tamper\", remove:[\"data.email\"]})\nBAD: leaving a rule active and then debugging why the app 'always fails' — check network_mock({action:\"list\"}) hit counts first.",
    "execution": {
      "taskSupport": "forbidden"
    },

+ tool added: press_key

--- pinned/redux_dispatch
+++ observed/redux_dispatch
  {
-   "description": "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()).\nPURPOSE: Drive state-controlled UI (loaders, modals, toasts, error overlays) without exercising the real flow (network, OTP, etc.).\nWHY 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.\nWHEN TO USE: Verify state-driven UI by seeding redux state directly. Example: dispatch app/setIsLoading: true, then ios_screenshot to confirm the loader rendered.\nWORKFLOW: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } }) -> ios_screenshot -> redux_dispatch({ action: { type: 'app/setIsLoading', payload: false } }).\nBATCH: action accepts an array — dispatched in order, one round trip.\nLIMITATIONS: 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).\nGOOD: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } })\nBAD: redux_dispatch({ action: () => ... }) — actions must be plain objects; for thunks use execute_in_app to call your action creator.",
+   "description": "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()).\nPURPOSE: Drive state-controlled UI (loaders, modals, toasts, error overlays) without exercising the real flow (network, OTP, etc.).\nWHY 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.\nWHEN TO USE: Verify state-driven UI by seeding redux state directly. Example: dispatch app/setIsLoading: true, then screenshot to confirm the loader rendered.\nWORKFLOW: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } }) -> screenshot -> redux_dispatch({ action: { type: 'app/setIsLoading', payload: false } }).\nBATCH: action accepts an array — dispatched in order, one round trip.\nLIMITATIONS: 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).\nGOOD: redux_dispatch({ action: { type: 'app/setIsLoading', payload: true } })\nBAD: redux_dispatch({ action: () => ... }) — actions must be plain objects; for thunks use execute_in_app to call your action creator.",
    "execution": {
      "taskSupport": "forbidden"
    },

+ tool added: run_app_command

--- pinned/scan_metro
+++ observed/scan_metro
  {
-   "description": "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.\nPURPOSE: Discover Metro on ports 8081-8090 and auto-connect all React Native debugger targets it advertises.\nWHEN TO USE: At the start of any session, or after the user restarts Metro / boots a new simulator.\nWORKFLOW: scan_metro -> get_apps -> get_logs / ios_screenshot / tap.\nGOOD: scan_metro()\nBAD: scan_metro() called repeatedly in a loop — use ensure_connection to re-verify an existing connection.\n",
+   "description": "Discover and connect to debuggable apps: React Native via Metro, Chromium/Electron via CDP, and native Android apps that include execbro-sdk-android. 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.\nPURPOSE: Discover Metro on ports 8081-8090 and auto-connect every React Native debugger target it advertises. Also probes Chromium debug ports: 9222 is listed but not auto-connected (attach with connect_metro), ports in EXECBRO_CHROMIUM_PORTS auto-connect.\nNATIVE ANDROID: also finds native apps that include execbro-sdk-android on every online adb device and connects them (runtime native).\nWHEN TO USE: At the start of any session, or after the user restarts Metro / boots a new simulator.\nWORKFLOW: scan_metro -> get_apps -> get_logs / screenshot / tap.\nGOOD: scan_metro()\nBAD: scan_metro() called repeatedly in a loop — use ensure_connection to re-verify an existing connection.\n",
    "execution": {
      "taskSupport": "forbidden"
    },

+ tool added: screenshot

--- pinned/swipe
+++ observed/swipe
  {
-   "description": "Swipe gesture that auto-routes to the correct platform (iOS or Android), with pixel-diff verification.\n[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]\nPURPOSE: 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.\nWHEN 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.\nVERIFICATION: 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.\nSAFETY: Android direction swipes stay clear of the system bars; `foregroundLost` appears if the app left the foreground anyway.\nWORKFLOW: swipe({ direction: \"up\" }) -> read response.verification.meaningful.\nLIMITATIONS: iOS needs AXe (brew install cameroncooke/axe/axe) or IDB. Pass `device` to target a specific device — call list_devices for the inventory.\n",
+   "description": "Swipe gesture that auto-routes to the correct platform (iOS or Android), with pixel-diff verification.\n[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]\nPURPOSE: 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.\nWHEN 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.\nVERIFICATION: 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.\nSAFETY: Android direction swipes stay clear of the system bars; `foregroundLost` appears if the app left the foreground anyway.\nWORKFLOW: swipe({ direction: \"up\" }) -> read response.verification.meaningful.\nCHROMIUM: a direction is a mouse wheel (reports `scrolled`), four coordinates a drag.\nLIMITATIONS: iOS needs AXe (brew install cameroncooke/axe/axe) or IDB. Pass `device` to pick one (list_devices).\n",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "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.",
+         "description": "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. On chromium it is one mouse-wheel event (up = deltaY positive) at the viewport centre, or at startX/startY when given, since the browser scrolls whatever is under the pointer; the response's `scrolled` and `container` come from the scroll container's own offsets, and a no-op names why (already at top/end, wrong axis, nothing scrollable there).",
          "enum": [
            "up",
            "down",
-         "description": "Swipe duration in milliseconds (default: 300 on Android; iOS uses driver default if omitted)",
+         "description": "Swipe duration in milliseconds (default: 300 on Android; iOS uses driver default if omitted). On chromium: the drag duration, default 300; unused by the wheel.",
          "type": "number"
        },
        "endX": {
-         "description": "Ending Y coordinate in screenshot pixels (explicit-coordinate mode)",
+         "description": "Ending Y coordinate in screenshot pixels (explicit-coordinate mode). On chromium the four coordinates are a real left-button drag (sliders, resizable panes, pointer-event drag libraries); native HTML5 drag and drop (draggable=true) is not started by synthesized mouse events.",
          "type": "number"
        },
        "screenshot": {
--- pinned/tap
+++ observed/tap
  {
-   "description": "Tap a UI element. Automatically tries multiple strategies: fiber tree (React), accessibility tree (native), and OCR (visual).\n[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]\nPURPOSE: Single unified tap entry point — resolves text/testID/component/coordinates into a real touch event on the correct device.\nWHEN 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.\nWORKFLOW: 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.\nLIMITATIONS: 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.\nGOOD: tap({ testID: \"login-btn\" }); tap({ text: \"Submit\" }); tap({ x: 300, y: 600 }); tap({ x: 300, y: 600, native: true, device: \"emulator-5554\" })\nLONG PRESS: tap({ testID: \"row-3\", duration: 800 }) holds the touch.\nBAD: tap({ text: \"\" }) or tap({ x: 0, y: 0 }) — missing a target. tap({ text: \"Submit\" }) without first screenshotting an ambiguous screen.\nSOURCE: need the file:line that renders an element? inspect_at_point(x, y).\n",
+   "description": "Tap a UI element. Automatically tries multiple strategies: fiber tree (React), accessibility tree (native), and OCR (visual).\n[PRIMARY INTERACTION TOOL — works on iOS and Android; prefer over ios_*/android_* siblings]\nPURPOSE: Single unified tap entry point — resolves text/testID/component/coordinates into a real touch event on the correct device.\nWHEN 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.\nWORKFLOW: screenshot -> tap(testID=\"...\") | tap(text=\"...\") | tap(x, y) -> screenshot again to verify. Use burst=true when meaningful=false but visual feedback looks transient.\nLIMITATIONS: 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.\nGOOD: tap({ testID: \"login-btn\" }); tap({ text: \"Submit\" }); tap({ x: 300, y: 600 }); tap({ x: 300, y: 600, native: true, device: \"emulator-5554\" })\nLONG PRESS: tap({ testID: \"row-3\", duration: 800 }) holds the touch.\nCHROMIUM: testID matches data-testid/id; clicks via CDP.\nBAD: tap({ text: \"\" }) or tap({ x: 0, y: 0 }) — missing a target. tap({ text: \"Submit\" }) without first screenshotting an ambiguous screen.\nSOURCE: need the file:line that renders an element? inspect_at_point(x, y).\n",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "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.",
+         "description": "Exact match on the element's testID prop (on chromium: data-testid, data-test-id or id; text matches visible text, component a React component name, and the target is scrolled into view). 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.",
          "type": "string"
        },
        "text": {

+ tool added: vault_capture

tool removed: ocr_screenshot

Current tools

Show all 67 tool fingerprints
activate_license
dc306922c40c22e9
android_key_event
f754e1427512437c
android_launch_app
325fef82973bd7fd
android_list_packages
5dbbbc29751f2b03
android_long_press
d6094b777f70f171
android_screenshot
41a3cbbc428aec05
app_request
7c420b443c549d31
clear_logs
ea817b02b11a24e8
clear_network
63f775126cc858c2
connect_metro
385f10e91f5742e5
delete_account
1efbd76f5b970641
disconnect_metro
e9c5620fabef333c
dismiss_keyboard
f8d8bb872bb9c3e8
electron_launch_app
8169b6cd13cbfcff
ensure_connection
795d2f0e5a68c118
execute_in_app
5e3566f6a007a86d
find_components
bb94c49a5ade7270
get_apps
4fc6af709a37bb28
get_bundle_errors
88d0f3d7233256e0
get_bundle_status
8c04cdabdb06aa66
get_component_tree
a09a5a6587f445c6
get_connection_status
ef88ae553d142b6e
get_images
b3e06eef36f6cf1d
get_license_status
a7dc1fa830709001
get_log_details
fcd3811c489b55a2
get_logs
66c65eab651587de
get_network_requests
2cb6a98c6cf7efad
get_refresh_status
2c07c5f0ccc0a52d
get_request_details
5a783900d85fb252
get_screen_layout
4083e38bf0f53977
get_screen_state
571a543b8775facb
get_usage_guide
f5fa076b79246af3
handle_dialog
c856b99ef9db7392
http_request
2cf9b392d1735c30
input_text
514af6985ee914c5
inspect_at_point
e5b921ad0ef45d27
inspect_component
fb10fe8944958161
inspect_global
6078ea04ea395d64
ios_boot_simulator
294ce2e500477be0
ios_button
fbd7377d6f9d9e4f
ios_launch_app
4cbe2064799b959f
ios_open_url
9cc38c8d6b3d6494
ios_screenshot
d04e4da98b608e5e
ios_terminate_app
48f4156e9dd187e7
list_debug_globals
6ad9718385ada702
list_devices
c1f72afc508cfdb2
list_secrets
1fd39ebda8092208
logbox
d8c11cb2bcb577ac
measure
1f2279b79270277e
navigate
cb2428d84df1770e
network_condition
0f675c6f9f3e2899
network_mock
791cf3ad7cc3b4b0
network_replay
5b0593ca030ca4f3
pinch
92c733abcee3c522
press_key
034c460682437591
redux_dispatch
73a4b5110aca6f9a
redux_get_state
bb67bc456c8af84e
reload_app
0abf8494af498628
run_app_command
21e60ee7eedf0a04
scan_metro
dbac849dfe3ec635
screenshot
3cda7e6e7f74e365
search_logs
810eeb49431724d7
search_network
04296cf1a8b03d0b
send_feedback
a42b1df57a7e36d2
swipe
49f00e192cab8980
tap
f0f116c568205d70
vault_capture
e4dee5a93dfcae6e

Watch this server yourself

If you run this server, put the proxy in front of it. It pins these exact fingerprints on first connect and stops the session if they move.

npx -y mcp-pin@0.2.3 -- <your execbro command>

Or subscribe to this page's RSS feed to be told when it changes.

Badge

mcp-pin status badge for execbro

The badge states one fact about time and nothing else. It never claims a server is safe.

[![mcp-pin](https://mcp-pin.gautamkhosla.com/badge/21c38d28f2080f06.svg)](https://mcp-pin.gautamkhosla.com/servers/21c38d28f2080f06.html)