Remote debugging for React Native: let your AI coding tool see the screen, read the logs and drive real input on a real device — from a TypeScript SDK with native bridges that autolink themselves.
OmniDebugLink connects your running React Native app to your AI tool over MCP. The tool calls a task on the device and waits for the result, so “find the button, tap it, screenshot, read the errors” is one conversation turn instead of a copy-paste loop.
Built on the WebSocket React Native already ships — no third-party JS dependencies. Handshake, heartbeat, backoff reconnect and console capture are handled for you.
One native module per platform covers what JS cannot reach: full-window screenshots, the native view hierarchy, touch injection and the native preference store. No manual MainApplication.kt or AppDelegate edits.
find_objects returns coordinates plus an atomic-action hint; ui_click locates and clicks a node in a single call.
setActionsEnabled(false) turns every write task into a no-op that returns ACTION_DISABLED — let an agent look around without touching anything.
npx expo prebuild, then as usual); managed + expo-dev-client works too (rebuild the dev client). Expo Go does not — no third-party native modules.The package is installed as a GitHub git dependency (it is not published to npm):
npm install omnidebuglink/omnidebuglink_react_native#v0.1.5 cd ios && pod install # iOS autolinking (RN 0.60+); Android is automatic
Or pin it in package.json:
"dependencies": {
"@omnidebuglink/react-native": "omnidebuglink/omnidebuglink_react_native#v0.1.5"
}
Sign up, create a token pair, and copy the client token. One token pair per device: if the token is replaced, the old connection receives close code 4000 and stops permanently instead of reconnecting.
import { OmniDebugLink } from '@omnidebuglink/react-native';
const client = new OmniDebugLink({
onLog: (msg) => console.log('[ODL]', msg),
onStateChange: (connected) => console.log('[ODL] connected:', connected),
});
client.start('your-device-token'); // from the console
Reconnects use exponential backoff (1s up to 30s). By default console.log/warn/error and global JS errors land in the log buffer, so read_logs sees your app’s output with no extra wiring.
// Report the react-navigation route stack in get_state
OmniDebugLink.setNavigator(navigationRef.current);
// Register custom tasks (registry changes auto-resend hello)
client.registry.register(
'my_task',
async (payload) => ({ status: 'done' }),
'Does something meaningful, returns status.',
{ type: 'object', properties: { value: { type: 'string' } } },
);
registry.register(type, handler, description?, schema?, { write }) — the description and schema are what your AI tool sees when it lists tasks, and write: true marks the task as a write operation gated by actionsEnabled. Call client.stop() to disconnect.
Sign in to the same account in your MCP client, pick the device, and start calling tasks — see how synchronous MCP calls work.
18 built-in tasks plus one registered in dev builds only. Pure-JS tasks work even without the native module; native-backed tasks return TASK_FAILED with install guidance when it is not linked.
| Task | Write | What it does |
|---|---|---|
echo / ping / get_stats | no | Connectivity basics. |
read_logs | no | 500-line ring buffer: console output, global errors and SDK events, with level / contains / sinceMs filters. |
find_objects | no | Find nodes by text, type or id substring; returns center px, normalized coordinates and an atomic-action hint. |
wait_for | no | Polls every 200ms until a node appears; a timeout returns found:false without raising an error. |
reload | yes | Reloads the JS bundle like the RN dev menu. Registered in dev builds only; pairs with Metro for an AI edit → reload → verify loop. |
| Task | Write | What it does |
|---|---|---|
screenshot | no | JPEG capture of all windows, so RN <Modal> shows up; quality and size are reduced until the capture fits the message budget. |
ui_traverse | no | View hierarchy dump, flat by default to save tokens (flat:false for nested), 3000-node cap, absolute screen px from the top-left origin, overlay windows included. |
tap_screen | yes | Tap at normalized [0,1] coordinates, top-left origin, routed to the topmost window covering the point. |
long_press | yes | Long press at normalized coordinates (default 800ms). |
swipe | yes | Swipe gesture with a controlled durationMs. |
ui_click | yes | Click a node located by text / type / index / fieldId — locating and clicking happen atomically in one call, since React tags go stale after re-renders. |
input_text | yes | Write text into a field located by fieldId or type + index; text is the value to enter. |
send_key | yes | Android: back / home / recents (back dismisses dialogs via overlay-window dispatch). iOS: enter / escape / backspace / tab / space. |
get_state | no | Screen, network and native activity / view-controller stack, plus react-navigation routes (requires setNavigator). |
get_perf | no | ~1s fps sample (p50/p95/p99 frame times) plus process memory. |
view_component | no | Single-node detail: layout plus native view state. |
prefs | no/yes | Read and write the native preference store (SharedPreferences / NSUserDefaults): get / set / delete / list with valueType coercion. |
ui_traverse reports absolute screen px (Android px / iOS pt).input_text by fieldId, view_component, ui_click by fieldId — are unavailable. Disable newArchEnabled or wait for Fabric support. Text/type locating, screenshot, ui_traverse, tap, swipe, get_state and get_perf work on both architectures.input_text on fully controlled inputs can be overwritten by the next re-render; verify the result with ui_traverse.UITouch internals, as every in-process injection technique does; a major iOS release may require adaptation.prefs only covers the native preference store. AsyncStorage (SQLite) and MMKV data is not visible — wrap your own storage behind a custom task if you need it.reload and re-establishes via auto-reconnect; the task returns before the drop, so the call does not hang.