# Mobile app development with Cursor

URL: https://developertoolkit.ai/en/cursor-ide/build/mobile-dev/
Description: Build React Native screens with Cursor's agent: a scoped mobile rule, simulator MCP servers, and gates plus both-platform screenshots that prove each change.

Mobile app development with Cursor works when the agent can check its own output on a device. A glob-scoped project rule pins React Native versions and the "done" commands. MCP servers let the agent build, launch and screenshot the app on an iOS simulator and an Android emulator. Each change ships with gate results and one screenshot per platform.

This page is for developers and tech leads who build React Native or Expo apps in Cursor. The familiar situation: Agent produced a profile screen that looks right on the iPhone simulator you had open, the pull request says "done", and QA finds the Android keyboard hiding the save button two days later. Nobody read the 600-line diff closely, and nobody should have to. The fix is a loop in which the agent runs the app on both platforms and hands you evidence.

## What a verified mobile loop in Cursor gives you

- A project rule, `.cursor/rules/react-native.mdc`, that stops Agent from writing web code or APIs from an older React Native.
- Two MCP servers in `.cursor/mcp.json` that give Agent a simulator and an emulator to test on.
- Four copy-paste prompts: set up the rule, build a screen from a design, fix a platform-only bug, and triage a failing native build.
- A gate table that says what proves a mobile change and who signs off, so you review screenshots and test output instead of every line.
- Failure modes specific to mobile work in Cursor, each with a recovery step.

Framework recipes (offline-first data, FlashList, biometrics, EAS builds, over-the-air updates) live in the [mobile development cookbook](/en/cookbook/mobile-development/). This page covers the Cursor side: context, the agent loop, and the proof.

## Which versions does this page assume?

Agents mix APIs from several majors because their training data spans them, so the versions belong in the rule, not in your head. The table lists what npm served on 2026-09-26; replace the numbers with the ones in your lockfile.

| Package | Version on npm, 2026-09-26 | Why the agent needs to know |
| --- | --- | --- |
| `react-native` | 0.87.1 (Expo SDK 57 pins 0.86.3) | The New Architecture is the only architecture since 0.82; old bridge-module tutorials no longer apply |
| `expo` | 57.0.26 | Add native packages with `npx expo install`, never by hand-editing versions |
| `@react-navigation/native` | 7.5.0 | Typed route params come from your `RootStackParamList` |
| `nativewind` | 4.2.7 | Styling through `className`, not `StyleSheet.create`, if your project uses it |
| `@tanstack/react-query` | 5.104.0 | Server state and the `networkMode` options for offline behaviour |
| `mobilebuildmcp` | 2.7.1 | iOS builds, simulators and tests for the agent (formerly `xcodebuildmcp`) |
| `@mobilenext/mobile-mcp` | 1.0.6 | Tap, type, screenshot and read logs on iOS and Android devices |

Cursor itself is the 3.x line; Cursor 3.24 was released on 2026-10-09 (Cursor's release notes, checked 2026-10-12).

## Set up Cursor for a React Native repository

Do these four steps once per repository and commit the results, so every teammate's Agent starts from the same context. If you have never written a Cursor rule, read [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/) first.

1. **Write a mobile rule scoped to app code.** Save it as `.cursor/rules/react-native.mdc` (the format the [project rules](/en/cursor-ide/quick-start/project-rules/) page uses). The `globs` line loads it whenever Agent touches app code, and it costs nothing while Agent works on backend files.

   ```markdown
   ---
   description: React Native app conventions and the done criteria for mobile changes
   globs: src/**/*.ts,src/**/*.tsx,app/**/*.tsx
   alwaysApply: false
   ---

   This is React Native 0.86 (New Architecture only) on Expo SDK 57, not React DOM.
   - Never use HTML elements (div, span, img, button), CSS strings or web-only style properties.
   - Add native packages with `npx expo install <pkg>`. Never change a version in package.json by hand.
   - Screens live in src/screens/, one per file. Register each in @src/navigation/RootNavigator.tsx
     and add its params to RootStackParamList in @src/navigation/types.ts.
   - Style with NativeWind className. Shadows: use the boxShadow style prop (RN 0.76+, New Architecture)
     or NativeWind shadow-* classes; never CSS strings or web-only properties.
   - Platform files (.ios.tsx / .android.tsx) take priority over the base file in Metro.
     Before editing a component, check whether a platform variant exists and edit that one too.

   Done means all of these pass, and you paste the output:
   1. npx tsc --noEmit
   2. npx expo lint
   3. npx jest --ci
   4. npx expo install --check   (native packages at the versions the SDK expects)
   5. rg -n "<(div|span|img|button|p)[ >]" src app   (must print nothing; list only folders that exist)
   6. For UI changes: a screenshot of every changed screen on the iOS simulator and the
      Android emulator, saved to evidence/<task>/ios-*.png and evidence/<task>/android-*.png
   ```

   For a bare React Native app without Expo, replace `npx expo install` with your package manager and `npx expo lint` with your ESLint command.

2. **Keep committed noise out of the agent's search.** Cursor's agent code loads `.gitignore` as well as `.cursorignore` (checked in `@cursor/sdk` 1.0.32, 2026-09-26), so gitignored `node_modules/`, `ios/Pods/` and build folders need no second entry. `.cursorignore` is for files that are **committed** but that Agent must not read or imitate. Run `git ls-files ios/Pods android/app/build | head` to see whether your team commits any of them, then list only what shows up:

   ```bash
   # .cursorignore: committed files the agent must not read or imitate
   # (example entries; keep only paths that git ls-files shows in your repo)
   ios/Pods/
   android/app/src/main/assets/index.android.bundle
   **/__snapshots__/
   src/legacy-web/
   ```

   Never ignore `ios/Podfile`, `android/app/build.gradle` or `app.json`: Agent needs them to fix native build errors. [Context management in Cursor](/en/cursor-ide/quick-start/context-management/) covers the syntax and the error Agent reports for a blocked path.

3. **Give Agent a device.** Add both servers to `.cursor/mcp.json` in the repository root. [MobileBuildMCP](/en/ecosystem/mcp/framework-devtools-mcp/) (maintained by Sentry) builds and tests iOS apps and needs macOS 14.5 or later with Xcode 16 or later. mobile-mcp (Mobile Next) drives iOS simulators, Android emulators and real devices through screenshots and accessibility snapshots; Android needs the platform tools (`adb`) on your `PATH`.

   ```json
   {
     "mcpServers": {
       "MobileBuildMCP": {
         "command": "npx",
         "args": ["-y", "mobilebuildmcp@latest", "mcp"]
       },
       "mobile-mcp": {
         "command": "npx",
         "args": ["-y", "@mobilenext/mobile-mcp@latest"]
       }
     }
   }
   ```

   Both entries follow the vendors' own Cursor snippets (MobileBuildMCP's MCP Clients docs page and the mobile-mcp README, checked 2026-09-26). MobileBuildMCP reports its own runtime errors to Sentry; its README links the opt-out. On Linux or Windows, drop MobileBuildMCP and keep mobile-mcp for Android. Each connected server gives Agent more tools to choose from and more descriptions to weigh, so enable them only in the app repository. [Set up MCP servers in Cursor](/en/cursor-ide/quick-start/mcp-setup/) shows how to enable, disable and check servers.

4. **Prove the setup before the first real task.** Start a new Agent chat and ask: "Use mobile-mcp to list the available devices, then use MobileBuildMCP to build the app for the first iPhone simulator. Report the tool calls you made." A correct answer names a real device ID and shows a tool call. An answer without a tool call means the server is not connected, and Agent guessed.

Optional: framework skills add version-specific knowledge that loads only when a task needs it. The React Native set from Callstack (community, not Meta) installs for Cursor with the cross-agent `skills` CLI (1.7.0 on npm, 2026-09-26; see [installing and managing skills](/en/ecosystem/skills/installing-managing/)): `npx skills@latest add callstackincubator/agent-skills --skill react-native-best-practices --agent cursor -y`. The [mobile cookbook](/en/cookbook/mobile-development/) lists the Expo and Flutter sets.

**Copy-paste prompt: draft the mobile rule from the repository**

```text
Read package.json, the lockfile, app.json or app.config.ts, tsconfig.json, the
ESLint config, and the CI workflow files. Draft .cursor/rules/react-native.mdc with
frontmatter: description "React Native app conventions and the done criteria for
mobile changes", globs covering the app source folders you found, alwaysApply false.
The body has three parts, at most 40 lines in total:
1. The exact React Native, Expo, React Navigation and styling versions from the
   lockfile, and "This is React Native, not React DOM".
2. Conventions the repository actually follows (folders, navigation registration,
   styling, platform files), each with one real file path as the example.
3. A "Done means" list with the exact type-check, lint and test commands CI runs
   (copy them, do not invent any), plus a both-platform screenshot requirement.
Show me the file and list any command you could not find. Do not write it yet.
```

## Build a screen from a design: plan, implement, prove

Screen work goes wrong in the plan, not in the typing. Start in Plan Mode, approve a plan that names files and tests, then switch to Agent mode to implement against the gates. [Agent modes in Cursor](/en/cursor-ide/build/agent-modes-deep-dive/) explains when to switch.

Point the prompt at your existing components with `@` so Agent uses their real props instead of inventing new ones. If the design lives in Figma, [building frontend UI from designs](/en/cursor-ide/build/frontend-ui/) shows the Figma MCP side; the prompt below works from a written spec.

**Copy-paste prompt: a profile screen with evidence from both platforms**

```text
@src/navigation/RootNavigator.tsx @src/navigation/types.ts
@src/components/Button.tsx @src/components/TextField.tsx

Build src/screens/ProfileScreen.tsx, a profile editor:
- Avatar (80px circle) with a camera badge that opens the image picker.
- Fields: display name, email (read-only), phone, bio (multiline, max 280 chars).
- Toggles for email, push and SMS notifications.
- "Save changes" uses our Button, disabled until a field changes.
- Loading and error states for GET /api/users/me; save with PATCH /api/users/me.
- The focused field stays visible above the keyboard on both platforms.

Acceptance criteria as tests first: write src/screens/__tests__/ProfileScreen.test.tsx
with React Native Testing Library covering: save disabled until an edit, the bio
limit, and the error state. Show me the failing tests before implementing.

Then implement, register the screen in RootNavigator with typed params, and run
every "Done means" command from the react-native rule. Launch the app on the
iOS simulator and the Android emulator, open the screen, focus the bio field so
the keyboard is up, and save screenshots to evidence/profile-screen/.
Finish with: files changed, command results, screenshot paths, and anything that
looks different between iOS and Android.
```

Review the result in this order, and stop at the first failure:

1. **The file list.** Run `git diff --stat`. A screen task touches the screen, its test, the navigator and the types file. Anything under `ios/` or `android/` means a native change you did not ask for: ask Agent why before going further.
2. **The gate output.** All five commands pass, and the web-element search printed nothing.
3. **The screenshots.** Two images with the keyboard up. The bio field is visible on both, and the save button is reachable.
4. **The tests.** Open the test file only. It is the spec; if it asserts what you asked for, the implementation behind it does not need a line-by-line read.

If a step fails, restore the checkpoint from before the implementation turn rather than stacking fixes on a bad base. See [checkpoints and branching](/en/cursor-ide/build/checkpoints-branching/).

## Handle iOS and Android differences without two codebases

Most platform differences are values, not structures. Pick the smallest tool that expresses the difference, because every platform file is a second place a later fix has to land.

| The difference | Use | Why |
| --- | --- | --- |
| A value: `android_ripple`, status-bar style, padding, font weight, header option | `Platform.select()` inline | One file, and the difference is visible in review |
| A component whose behaviour differs (date picker, haptics, permissions flow) | `Component.ios.tsx` and `Component.android.tsx` beside a shared props type | Metro loads the platform file first; the shared type keeps both variants in step |
| Something React Native has no API for | A native module (Expo Modules API or a Turbo Module) | Needs a rebuild and a reviewer who knows Swift or Kotlin; see [native iOS and Android recipes](/en/cookbook/mobile-development/native-ios-android/) |

**Copy-paste prompt: fix a bug that appears on one platform only**

```text
@src/screens/ChatScreen.tsx

Bug: on Android the message input is hidden behind the keyboard. On iOS the input
is visible but there is a 20px gap under it. The scroll-to-bottom button overlaps
the input on both.

1. Reproduce first: launch the app on the Android emulator and the iOS simulator,
   open ChatScreen, focus the input, and save "before" screenshots to
   evidence/chat-keyboard/.
2. Explain the cause for each platform in two sentences before changing code.
   Check app.json / AndroidManifest.xml for the windowSoftInputMode setting and
   whether a bottom tab bar offset is involved.
3. Fix it with Platform.select values where possible. Create platform files only
   if the component structure must differ, and tell me why.
4. Run every "Done means" command, then save "after" screenshots from both
   platforms. Do not change any file outside src/screens/ and src/components/
   without asking.
```

The "before" screenshots matter as much as the "after" ones. A fix verified only after the change cannot show that Agent reproduced the bug you reported rather than a different one.

## Debug Metro, native build and device-only failures

Mobile failures often sit outside the TypeScript code: in Metro's cache, CocoaPods, Gradle or a missing rebuild. Give Agent the full log and a rule about what it may change, because the fastest way to make a build error disappear is to upgrade or remove a dependency, and that is rarely the fix you want.

The usual first moves, which Agent should try before editing any code:

| Symptom | First command (terminal) |
| --- | --- |
| `Unable to resolve module` for an installed package | `npx expo start --clear` (Expo) or `npx react-native start --reset-cache` (bare) |
| iOS build fails after adding a native package | `npx expo prebuild --clean` (Expo prebuild) or `cd ios && pod install` (bare) |
| Android build fails after adding a native package | `cd android && ./gradlew clean`, then rebuild |
| The JavaScript change works but a native change does not appear | Rebuild the app; a Metro reload does not load native code |

**Copy-paste prompt: triage a failing native build**

```text
The iOS build fails after I added react-native-vision-camera. Build it yourself
with MobileBuildMCP for the first available iPhone simulator and read the full
log, not only the last error.

Rules for this task:
- Do not upgrade, downgrade or remove any package, and do not edit package.json,
  without asking me first.
- Do not edit files under ios/Pods/ or any generated file.
- Try the cache and rebuild steps first (expo prebuild --clean or pod install),
  one at a time, rebuilding after each.

Report: the first real error in the log (not the last), its cause, the smallest
fix, and the build result after the fix. If the fix needs a version change, stop
and show me the two candidate versions from `npm view <pkg> versions` instead.
```

For crashes that happen only on a device or in a release build, the React Native cookbook covers [release-build debugging with React Native DevTools](/en/cookbook/mobile-development/react-native/), and [bug hunting in Cursor](/en/cursor-ide/test/bug-hunting/) covers the general reproduce-first loop.

## How do you verify mobile changes without reading every line?

Make Agent produce evidence, and review the evidence. The rule's "Done means" list already forces most of it; this table says what each layer catches and who owns it.

| Layer | Command or artifact | Catches | Signed off by |
| --- | --- | --- | --- |
| Types and lint | `npx tsc --noEmit`, `npx expo lint` | Wrong props, wrong navigation params, unused code | CI |
| Web-code search | `rg -n "<(div\|span\|img\|button\|p)[ >]" src app` | HTML elements, which `tsc` does not reject in a React Native project | CI |
| Dependency alignment | `npx expo install --check` (Expo) | Native packages at versions the SDK does not expect | CI |
| Unit tests | `npx jest --ci` with React Native Testing Library | Behaviour you wrote as acceptance criteria | Developer who ran the task |
| Device evidence | One screenshot per changed screen per platform in `evidence/<task>/` | Layout, keyboard, safe-area and platform-only bugs | Developer who ran the task |
| End-to-end flows | Maestro flows in `.maestro/` (see [mobile app testing patterns](/en/workflows/test/mobile-testing/)) | Broken navigation, deep links and auth gates | CI, on every pull request |
| Native changes | `git diff --name-only \| grep -E '^(ios\|android)/'` prints something | Unrequested native edits that need a rebuild and a native reviewer | Tech lead or native owner |
| Release | Staged store rollout, and for Expo a fingerprint check before any OTA update | A bad build reaching every user | Tech lead |

In the web-code search, list only the folders your repository has: `rg` exits with code 2 on a missing path such as `app`, and the gate then fails for the wrong reason. On a CI runner without ripgrep, install it or run `grep -rnE` with the same pattern.

Put the gates that do not need a device into CI, so a green pull request already means types, lint, the web-code search and the unit tests passed; [CI/CD pipelines with Cursor](/en/cursor-ide/ship/ci-cd/) shows the workflow. Keep the screenshots in the pull request description, and use the [evidence bundle](/en/workflows/ship/evidence-bundle/) format if your team has adopted it. The reviewer then reads what changed in behaviour, not the diff.

## What breaks when Cursor's agent works on a mobile app?

**Agent writes web code.** Symptom: `<div>`, `onClick`, `className` strings with web-only utilities, or `style={{ cursor: 'pointer' }}` in a screen. Cause: the rule did not load: its `globs` miss the folder Agent edited, or its frontmatter is malformed. Recovery: fix the globs or frontmatter, start a new chat, confirm with the canary check from [project rules](/en/cursor-ide/quick-start/project-rules/), and keep the web-element search in CI so the next slip fails the build.

**The fix lands in the wrong platform file.** Symptom: Agent edited `DatePicker.tsx`, the tests pass, and nothing changes on the device. Cause: `DatePicker.ios.tsx` exists and Metro loads it first. Recovery: ask Agent to list platform variants with `ls src/components/DatePicker*`, apply the fix to each, and keep the platform-file line in the rule.

**A native change without a rebuild.** Symptom: Agent reports the feature works, but the screenshot shows the old behaviour or a red "native module not found" screen. Cause: Metro reloaded JavaScript only. Recovery: rebuild through MobileBuildMCP or `npx expo run:android`, and require a fresh screenshot after the rebuild.

**Agent "fixes" a build by changing versions.** Symptom: the build is green and `package.json` or the lockfile changed. Recovery: restore the checkpoint, rerun the build-triage prompt with its no-version-change rule, and keep the `npx expo install --check` gate in CI so a mismatched native package fails loudly.

**Only one platform was tested.** Symptom: the summary has screenshots from one simulator. Cause: Agent used whatever device was booted. Recovery: reject the result and ask for the missing platform; the "Done means" list names both, so this is a rule violation you can point to.

**The MCP server finds no device.** Symptom: an empty device list or "no booted simulator". Recovery: boot a simulator in Xcode or an emulator in Android Studio, check `adb devices` for Android, and ask Agent to list devices again. On a machine without Xcode, MobileBuildMCP cannot build iOS at all; use mobile-mcp with Android and a teammate's Mac or CI for iOS.

**Navigation types drift.** Symptom: `tsc` errors on `navigation.navigate('Profile', …)` after a new screen. Cause: Agent registered the screen but did not update `RootStackParamList`. Recovery: `@`-mention `src/navigation/types.ts` in the prompt; the rule already names it, so repeat the prompt in a new chat if the rule did not load.

## Where to go next with mobile development in Cursor

- [React Native development patterns](/en/cookbook/mobile-development/react-native/) — Offline-first data, FlashList, biometric gates and store releases, for Cursor, Claude Code and Codex.
- [Expo development patterns](/en/cookbook/mobile-development/expo/) — Expo skills and MCP, Expo Router auth gates, EAS builds and staged OTA updates.
- [Mobile app testing patterns](/en/workflows/test/mobile-testing/) — Maestro flows, device matrices and offline testing for the gates above.
- [Test-driven development in Cursor](/en/cursor-ide/test/testing-tdd/) — Approve failing tests as the spec, then let Agent implement until green.
- [API integrations with Cursor](/en/cursor-ide/build/api-integration/) — The typed API layer your screens call, with retries and tests.
- [Build with Cursor](/en/cursor-ide/build/) — All Cursor build guides, from agent chat to verified change.
