Skip to main content
Glama

@avasapp/agent-bridge

Let coding agents drive any running Expo or React Native app directly. Seed data, flip flags, fake realtime messages, navigate and check the screen in milliseconds, without tapping through the app. It works alongside device tools such as agent-device and Argent: they open the app and handle native UI, and agent-bridge handles everything inside the app. Use it from a shell, from flows or as an MCP server.

The same checks with agent-device alone (22.4 s) and with agent-bridge (1.4 s): hide a tab, turn on dark mode, fill in a form, seed data, fake a live message and undo it all

The same 19 checks on the example app, at real speed: 22.4 s with agent-device alone, 1.4 s with agent-bridge. With Argent instead of agent-device it is 17.1 s against 1.4 s while screen recording (Argent alone takes 11.6 s without the recorder). Most calls take a few milliseconds (benchmarks).

Works on Expo (dev-tools socket) and on bare React Native (CDP over Metro). Nothing ships to users: every entry point compiles to an empty stub in release builds, and agent-bridge assert-absent checks a bundle in CI (security).

Install

npx @avasapp/agent-bridge init

Run it in the app. It installs the package, writes a dev-only src/dev/agent-bridge.tsx wired for the libraries it finds, adds a smoke flow and the agent skill, and prints the two lines to paste into your root component. Agents run it as init --json; see the CLI.

Manual setup

bun add -d @avasapp/agent-bridge   # or npm / yarn / pnpm

The package ships an agent skill that tells your agent when and how to drive the app. Add it to Claude Code, Cursor, Codex and other agents:

npx skills add avas-app/agent-bridge

In Claude Code you can install it as a plugin instead:

/plugin marketplace add avas-app/agent-bridge
/plugin install agent-bridge@agent-bridge

Related MCP server: React Native Debug MCP

In the app

Mount the hook in a file that only runs in development. You choose every tool; adapters for common libraries are one import away.

import { cdpTransport, useAgentBridge } from '@avasapp/agent-bridge'
import { expoTransport } from '@avasapp/agent-bridge/expo'
import { queryTools } from '@avasapp/agent-bridge/tanstack-query'
import { storeTools } from '@avasapp/agent-bridge/zustand'
import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'
// import { asyncStorageTools } from '@avasapp/agent-bridge/async-storage'
// import { secureStoreTools } from '@avasapp/agent-bridge/secure-store'
// import { reduxTools } from '@avasapp/agent-bridge/redux'
// import { jotaiTools } from '@avasapp/agent-bridge/jotai'
import { routerTools } from '@avasapp/agent-bridge/expo-router'
import { networkTools } from '@avasapp/agent-bridge/network'
import { router, useNavigationContainerRef } from 'expo-router'

export function AgentBridge() {
  const queryClient = useQueryClient()
  useAgentBridge({
    name: 'my-app',
    transports: [expoTransport(), cdpTransport()],
    tools: {
      ...queryTools(queryClient),
      ...storeTools(
        { settings: useSettingsStore, auth: useAuthStore },
        // Masks these paths in every store.* output; `store.set` and
        // `store.call` never echo the whole store. An action's return value
        // is matched by store path only if it is a piece of the state itself,
        // not a copy; use a hook function for those.
        { redact: { auth: ['accessToken', 'refreshToken', 'user.phone'] } },
      ),
      ...mmkvTools({ storage }),
      // Redux and Jotai state often holds tokens: add `redact` before wiring them.
      // ...reduxTools(store, { redact: { auth: ['token'] } }),
      // ...jotaiTools(getDefaultStore(), { user: userAtom }),
      // AsyncStorage and SecureStore often hold tokens: wire them only if you accept that.
      // ...asyncStorageTools(AsyncStorage),
      // ...secureStoreTools(SecureStore, ['authToken'], { keychainService: 'auth' }),
      ...routerTools(router, { navigation: useNavigationContainerRef() }),
      ...networkTools(),
      // Your own tools: any function, JSON in and out.
      'auth.signIn': (session) => signInWith(session),
    },
  })
  return null
}

Every app also gets screen.snapshot (one short line per element), screen.press (taps, long presses, Switch toggles), screen.fill, screen.setValue (Switch, Slider, Picker, SegmentedControl), screen.scroll, screen.waitFor, screen.findText, app.reload, and bridge.restore to undo everything the agent changed, even across a reload in a session. See built-in tools and restore, including how to give agents your own hooks and register their undo.

Most flows need the app in a known state first, such as a user signed in locally with every request mocked. Define that once as a scenario.

From the agent

npx @avasapp/agent-bridge session start            # hold one connection; call/tools/run reuse it
npx @avasapp/agent-bridge tools
npx @avasapp/agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
npx @avasapp/agent-bridge call query.pin '[["features"], {"beta": false}]'
npx @avasapp/agent-bridge call screen.press '"add-plant"'
npx @avasapp/agent-bridge call screen.waitFor '"Name is required"'
npx @avasapp/agent-bridge run flows/add-plant.flow.mjs --strict   # fail if the app logged an error
npx @avasapp/agent-bridge run flows/ --bail                        # every *.flow.mjs in order, restore after each
npx @avasapp/agent-bridge session stop             # runs bridge.restore, then disconnects

Agents should run ./node_modules/.bin/agent-bridge rather than npx, which is slower and can print npm warnings into their output. See the CLI for flows, batch mode, the REPL, sessions and the connect() client. Agents that prefer MCP tools to a shell can run agent-bridge mcp; see the MCP server.

No device tool is needed. Pair one (such as agent-device or Argent) with the bridge for what it can't reach: system alerts, permission prompts, the keyboard, screenshots, and one real tap per flow. Argent's tap, swipe, screenshot and UI tree tools work next to the bridge as they are. Its debugger tools do too on Expo apps (the bridge uses the dev-tools socket) and on React Native 0.85 and later, which let several debuggers attach. Before 0.85, Metro's debugger socket takes one client at a time, so a bare React Native app on --transport cdp and a tool that holds the debugger open (Argent's debugger tools, React Native DevTools) can take it from each other.

The screen tools measure with Fabric, so they need React Native's New Architecture; on the old one the tools that need positions fail with an error that says so (snapshot {all: true} and findText still return text, without rects).

Docs

Contributing

See CONTRIBUTING.md.

License

MIT © Avas Enterprises

Available Tools

19 tools
agent_bridge_statusagent-bridge statusA

Whether this server is connected to the app, which app and session, how many tools it has, and why the last connect failed. Works with no app connected.

Inputs: no named parameters.

app_reloadapp.reloadA

Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.

Inputs: args, full.

app_restoreapp.restoreA

Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.

Inputs: args, full.

bridge_logsbridge.logsB

Errors and warnings the app logged, newest last. Options: {level: "error"|"warn"|"all", limit, clear}.

Inputs: args, full.

bridge_pendingbridge.pendingA

What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.

Inputs: args, full.

bridge_restorebridge.restoreA

Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.

Inputs: args, full.

bridge_toolsbridge.toolsC

Every tool this app exposes.

Inputs: args, full.

scenario_applyscenario.applyB

Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.

Inputs: args, full.

scenario_listscenario.listC

Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.

Inputs: args, full.

scenario_restorescenario.restoreA

Undo every active scenario, newest first. bridge.restore runs this after every other restorer.

Inputs: args, full.

screen_capturescreen.captureA

Screenshot the app as a PNG. Returns {path} of a file on the device. {base64:true} returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.

Inputs: args, full.

screen_fillscreen.fillA

Type into an input, then wait for the render. target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument. {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.

Inputs: args, full.

screen_findTextscreen.findTextA

Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); hidden counts matches there.

Inputs: args, full.

screen_pressscreen.pressA

Press a button, then wait for the render. target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument. Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot actions) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.

Inputs: args, full.

screen_refreshscreen.refreshA

Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.

Inputs: args, full.

screen_scrollscreen.scrollA

Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.

Inputs: args, full.

screen_setValuescreen.setValueA

Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has value as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.

Inputs: args, full.

screen_snapshotscreen.snapshotA

Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). actions lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and hidden counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one webview element with its name: use webview.* inside it. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then readonly, actions=…, offscreen, hidden. Lines that repeat get index=N (the {index} of the target that names them); a line with no testID, text, label or placeholder gets @x,y, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).

Inputs: args, full.

screen_waitForscreen.waitForA

Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.

Inputs: args, full.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.3/5.0

    Scored across 19 tools

    Disambiguation4/5

    The screen_* tools are clearly separated by action (press, fill, setValue, scroll, refresh, findText, snapshot, waitFor, capture), and the scenario_* and bridge_* groups mostly target distinct operations. The main ambiguity is the restore trio — app_restore, bridge_restore, and scenario_restore — plus bridge_pending, which require careful reading to choose correctly.

    Naming Consistency3/5

    There are clear domain prefixes (screen_, bridge_, scenario_, app_), which makes the set broadly readable. However, suffixes mix snake_case and camelCase (screen_findText, screen_setValue, screen_waitFor vs. screen_press, screen_snapshot), and agent_bridge_status breaks the bridge_ prefix pattern.

    Tool Count4/5

    At 19 tools, the set is slightly heavy but reasonable for a full app bridge covering diagnostics, scenarios, restoration, and screen automation. Each tool has a recognizable role, though the three restore variants add some surface area that could be consolidated.

    Completeness4/5

    The surface covers screen inspection, interaction, waiting, scrolling, capture, scenario lifecycle, bridge diagnostics, logs, reload, and restoration. Minor gaps remain: no gesture/swipe tool for gesture-handler cases, no direct network/mock inspection despite mentions of net and mocks, and no app launch/terminate operation.

    Maintenance

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      An MCP server that gives AI agents hands-on control of a live React Native app, enabling UI taps, screenshots, UI/component trees, native logs, and JS/React profiling to debug, verify fixes, and diagnose performance issues.
      16
      10 npm
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Connects AI agents to a running React Native app so they can see the screen, tap and type, and inspect logs, network traffic, and state without any SDK or code changes. Composite tools let an agent run a whole interaction flow in a single call instead of one round trip per tap.
      35
      6 npm
      2
      MIT