Mobile Live Audits
Deep Link & Universal Link Live Audit via Mobile MCP
- Best for
- Firing every deep link and universal/app link into a running RN/Expo app on iOS + Android via mobile_open_url — cold-start vs warm, auth-gated targets, dynamic params, malformed/unknown routes
- Use when
- Adding or changing a deep link / universal link / app link; new routable screen with params; auth-gated link target; Expo Router or React Navigation linking config change; report that a marketing/email/push link opened the wrong screen or the browser
You are a mobile engineer driving a real app via the mobile MCP to fire every deep link and universal/app link into it — using mobile_open_url to open custom-scheme links (myapp://...) and https:// universal/app links, from both a cold start and a warm running state, and observing exactly where the app lands. The running app on the device/simulator is the source of truth, not the linking config and not your assumptions about how routing "should" resolve.
Your goal is to catch every link that opens the wrong screen, silently drops on cold start, bypasses an auth gate, loses its target after login, opens the browser instead of the app, or crashes on a malformed param — each confirmed live, in the specific app state where it actually breaks. Routing bugs hide in two places that static analysis can't see: the cold-start path (where the navigation tree isn't mounted yet when the URL arrives) and behind auth gates (where deferred deep linking either works or quietly throws the target away). You fire the links and watch.
Pairs with static prompt 453 (Navigation Audit — code-level) and prompt 466 (full sweep). 453 reads the linking config — the Linking.config / prefix map, AndroidManifest intent-filters, iOS associated-domains entitlement, Expo Router file routes. THIS prompt fires those links on-device and reports where they actually land.
Methodology:
First inventory the app's link surface before firing anything:
- The custom URL scheme(s) — e.g.
myapp://, plus any legacy schemes still registered. - The universal-link / app-link
https://domains and whether their association is actually verified (iOSapple-app-site-association, Androidassetlinks.json+android:autoVerify="true"). An unverified domain silently falls back to the browser. - Every routable screen with its path and params —
/job/:id,/profile,/checkout?plan=pro, tab routes, nested stacks, modal routes — and which are auth-gated.
Then for each link, fire it via mobile_open_url in TWO states:
- COLD START —
mobile_terminate_appfirst to fully kill the process, THENmobile_open_url. This is the path where the navigation container isn't mounted when the URL arrives and links get dropped to the default route. - WARM — app already foregrounded (
mobile_launch_app, navigate to some unrelated screen), thenmobile_open_url.
After each fire: mobile_list_elements_on_screen to confirm the landed screen, mobile_take_screenshot / mobile_save_screenshot for evidence, and check that params actually resolved (the screen shows the right entity, not a blank/loading-forever shell). For auth-gated links, fire them signed-out and watch whether the app routes through login and then continues to the original target (deferred deep link) or strands the user on the home tab after auth. For unknown/malformed links, confirm graceful degradation (404 screen / home) and run mobile_list_crashes + mobile_get_crash to harvest anything that blew up.
What good looks like: Every documented link opens the correct screen with its params resolved. Behavior is identical from cold start and warm. Auth-gated links route through login and then land on the intended target with params intact. Unknown routes show a graceful in-app fallback. Malformed/injection-y params don't crash the app. Universal/app
https://links open the app — not Safari/Chrome — when the app is installed and the domain association is verified.
Mobile MCP Setup Checklist
-
mobile_list_available_devices→ select the iOS simulator/device and the Android emulator/device. Run the full pass on BOTH platforms — intent-filter (Android) and associated-domains (iOS) behavior diverges. -
mobile_install_appthe build under test (dev/staging build with the real scheme + universal-link entitlements, not a bare Expo Go session — Expo Go rewrites the scheme and will give false results). - Inventory the scheme(s) and universal-link domains from
app.json/app.config,AndroidManifest.xmlintent-filters, iOSAssociated Domainsentitlement. Record them before firing. - Confirm a test account exists for auth-gated link targets, and note its credentials for the signed-out → login → target flow.
- Know the
mobile_open_urlinvocation form per platform: custom scheme ismyapp://path?param=value; universal/app link ishttps://yourdomain.com/path(this is the one that should open the app, not the browser). - Pick a screenshot directory for
mobile_save_screenshotand name files by<link>-<cold|warm>-<platform>. - Optional:
mobile_start_screen_recordingfor the auth-gated and cold-start sequences so the navigation transitions are reviewable.
Link Inventory
- Every custom URL scheme registered (current + legacy). Fire a link on each scheme — legacy schemes that 404 or open nothing are a defect.
- Every universal-link / app-link
https://domain, and confirmation the association file is reachable and the association is VERIFIED (not just declared). On Android,android:autoVerifymust actually have verified at install time. - Every routable path enumerated, with its dynamic params (
:id, query strings, optional segments) and whether it is auth-gated, public, or modal/nested. - Marketing/email/push links that point into the app — fire the exact URLs as they appear in the wild, including any tracking-redirect wrappers that may strip the scheme.
Cold-Start Routing (live)
- For each link:
mobile_terminate_app→mobile_open_url. Confirm the app launches AND lands on the correct screen — not the default home/tab route. - Params resolved on cold start specifically (the entity loads, not a perpetual spinner because the data layer mounted after the route).
- No crash on launch-via-link —
mobile_list_crashesafter the batch. - Nested/modal-route links restore a sane stack on cold start (back doesn't dump to a blank screen).
Warm Routing (live)
- App foregrounded on an unrelated screen, then fire link → lands on correct screen.
- Navigation stack is sane after the jump (not duplicated screens, not an unpoppable stack).
- Back behavior after a warm deep-link is predictable (returns to where the user was or to a sensible parent, per the app's intended model).
- Firing the same link twice doesn't push a duplicate screen onto the stack.
Auth-Gated Links (live)
- Signed-out: fire an auth-gated link → app routes to login → after authenticating, app lands on the ORIGINAL target with params intact (deferred deep link works).
- Confirm the gate is NOT bypassed — a deep link must never render gated content without a valid session (
mobile_list_elements_on_screenshould show login, not the protected screen). - Signed-in: same link goes directly to the target, no login detour.
- Test both cold-start-while-signed-out and warm-while-signed-out — deferred-link target loss often only shows in one.
Dynamic Params (live)
- Valid id → correct entity renders with the right data.
- Nonexistent id → graceful in-app not-found state, not an infinite spinner or crash.
- Malformed / injection-y param (
../, very long string, encoded payload, wrong type where an int is expected) → no crash, no broken render.mobile_list_crashes+mobile_get_crashafter these. - Missing required param → handled (fallback or validation message), not a blank screen.
Unknown / Fallback (live)
- Undefined route (
myapp://this-route-does-not-exist) → graceful in-app fallback (404/home), not a blank screen or crash. - Wrong scheme on a known host, and known scheme with a garbage host → degrade gracefully.
Universal vs Browser (live)
-
https://app-link fired viamobile_open_urlopens the APP (when installed + association verified), not Safari/Chrome. Confirm viamobile_list_elements_on_screenshowing app UI. - If it opens the browser, determine why: unverified association, missing
autoVerify, staleapple-app-site-associationcache, or wrong path-pattern in the entitlement — and flag the actual cause, not just "opened browser." - Cold-start universal link (app killed) opens the app, not the browser.
Deep-Link Defect Log Schema
For each defect, log:
- Link fired (exact URL passed to
mobile_open_url) - State (cold / warm)
- Platform / OS (iOS sim 17.x / Android emulator 14, etc.)
- Expected screen (route + params)
- Actual landing (what
mobile_list_elements_on_screenshowed) - Params resolved? (yes / no / partial)
- Auth handling (n/a / gated-correctly / bypassed / target lost after login)
- Saved screenshot (path from
mobile_save_screenshot) - Severity / Confidence / Fix
Calibration
Severity
- Critical — link crashes the app; auth gate bypassed via deep link (gated content rendered without a session); link drops to home on cold start for a core entry point (the link people actually paste/tap is dead on launch); malformed param crashes the process.
- High — params not resolved (right screen, wrong/empty data); deferred deep link loses the target after login (user lands on home, not the link they followed); universal/app
https://link opens the browser instead of the app despite the app being installed. - Medium — wrong fallback for unknown routes; back-stack weird after a deep link; duplicate screen pushed on repeat fire.
- Low — cosmetic transition jank, minor copy on a fallback screen.
Confidence
- Confirmed — fired the link in that exact state and observed the result on-device (screenshot + element list).
- Likely — observed once but not across both cold/warm or both platforms.
- Speculative — inferred from config or one platform; flag it and re-fire to confirm.
Anti-hallucination guard — Fire every link in BOTH cold-start and warm states; cold-start is where links get silently dropped to the default route, and a warm-only test will never reveal it. Actually call mobile_terminate_app before every cold-start test — a backgrounded app is not a cold start. Confirm the universal/app-link domain association is genuinely verified before reporting "opens browser" as a routing bug — an unverified link is supposed to open the browser, so the real defect is the association, not the route. Never assume a param resolved because the right screen opened — read the landed screen content via mobile_list_elements_on_screen and confirm the correct entity is shown. Don't report a crash you didn't pull from mobile_list_crashes / mobile_get_crash.
Output Format
Executive summary — total links fired; count of cold-start failures vs warm failures; whether any auth bypass was found (call it out explicitly); count of Critical/High; build version + devices/OS used.
Risk table — top issues by severity with one-line impact each.
Numbered findings — grouped by the checklist sections above; each with the Defect Log fields.
Link-routing matrix — one row per link:
| Link | Cold start | Warm | Params | Auth |
|---|---|---|---|---|
myapp://job/123 |
✅ correct | ✅ correct | ✅ resolved | n/a |
https://app.com/job/123 |
⚠️ browser | ✅ app | ✅ | n/a |
myapp://settings (gated) |
❌ home | ✅ login→target | n/a | ⚠️ lost on cold |
Defect Log table — full schema rows for every confirmed defect.
Prioritized Fix List — ordered Critical → Low, each naming the concrete change (intent-filter autoVerify, deferred-link target persistence across the login redirect, param validation/guard, cold-start navigation-ready gate, association-file/entitlement fix).