Through the JS runtime
≈ 1 ms per press · ≈ 80 ms per full-screen layout
The server walks React's fiber tree over the Chrome DevTools Protocol. It measures elements and calls press and text handlers directly, with no touch involved.
React Native MCP server · iOS and Android
react-native-eye is a React Native MCP server. It connects Claude Code, Cursor or any MCP client to a running React Native app. The agent reads the screen, taps and types, and checks logs, network and state. It works through Metro, so the app needs no SDK and no code changes.
Add it to Claude Code
claude mcp add react-native-eye -- npx -y react-native-eye@latest
The screen
What the agent reads
Example output for a login screen. Point at a line to find the element.
Most device tools cost the agent one model round trip per tap. Here the agent sends the steps together and the server runs them back to back. A redbox or an uncaught exception stops the flow at once, and the agent gets the result of every step.
observeScreenshot, interactive elements, recent errors and recent logs.run_flowTaps, typing, waits and assertions, run on the server.check_stateCurrent route, store state and unhandled errors.{ "steps": [
{ "type": { "text": "a@b.c", "into": { "placeholder": "Email" } } },
{ "type": { "text": "hunter2", "into": { "placeholder": "Password" } } },
{ "tap": { "text": "Log in", "via": "os" } },
{ "wait_for": { "text": "Home", "timeout_ms": 3000 } },
{ "expect_not": { "text": "Error" } }
] }
Flow completed.
1. type({"text":"a@b.c","into":{"placeholder":"Email"}}) — ok (input: js)
2. type({"text":"hunter2","into":{"placeholder":"Password"}}) — ok (input: js)
3. tap({"text":"Log in","via":"os"}) — ok (input: os/runner)
4. wait_for({"text":"Home","timeout_ms":3000}) — ok
5. expect_not({"text":"Error"}) — ok
35 tools in five groups. Every response says which path ran, so nothing switches silently.
Element positions come from the app's JS runtime in about 80 ms. OCR reads what the tree cannot: images, WebViews and native dialogs.
observescreenshotget_layoutread_screen_textget_component_treeA JS press calls the element's own onPress in about a millisecond. A real touch goes through the driver on the device, for gestures, system dialogs and native views.
run_flowtaptype_textswipescroll_topress_keynavigateopen_urlLogs, network requests with bodies, store state and JS evaluation. Crashes that LogBox swallows are still reported.
check_stateget_logsget_errorsget_networkget_stateeval_jsget_jobWhich components rendered and why, a Hermes CPU profile, CPU by thread and frame timing. No setup in the app.
profile_rendersprofile_jssample_cpuget_fpsInstall, launch and reload the app, grant permissions and record video.
connectlist_targetslist_deviceslaunch_appreload_appinstall_appopen_simulatorset_permissionrecord_videostop_recordingpingComposite tools: one call instead of many.
The server reaches the app two ways and picks the cheaper one for each call.
≈ 1 ms per press · ≈ 80 ms per full-screen layout
The server walks React's fiber tree over the Chrome DevTools Protocol. It measures elements and calls press and text handlers directly, with no touch involved.
Prebuilt in the npm package · installs itself on first use
An XCUITest runner on iOS and an instrumentation server on Android deliver real touches and type any Unicode text. If the driver cannot start, input falls back to axe or adb, and the response says so.
Measured: the device driver against the tools it replaces
From the project's own field tests against a busy video feed, on an iOS 26 simulator and an Android 16 emulator. Both charts share one scale.
# in your project folder
claude mcp add react-native-eye -- npx -y react-native-eye@latest
// .cursor/mcp.json
{
"mcpServers": {
"react-native-eye": {
"command": "npx",
"args": ["-y", "react-native-eye@latest"]
}
}
}
# ~/.codex/config.toml
[mcp_servers.react-native-eye]
command = "npx"
args = ["-y", "react-native-eye@latest"]
// the standard MCP config, Claude Desktop included
{
"mcpServers": {
"react-native-eye": {
"command": "npx",
"args": ["-y", "react-native-eye@latest"]
}
}
}
Run the app in an iOS simulator or an Android emulator the way you usually do. Metro on ports 8081–8083 and 19000–19002 is found automatically. For any other port, add --port 8090 to the arguments.
Connect to my app and tell me what is on the screen.
The agent calls connect, then observe. The first call on each device installs the driver once.
The only network traffic goes to localhost: Metro and the device drivers. A test fails the build if an external URL appears in the server's source.
Good to know
globalThis.__RNE_STORES__ = …, makes store state easier to read.