Skip to content

feat: agent-driven real devices and browsers - #485

Draft
Vishalan wants to merge 6 commits into
mainfrom
feat/agent-device-and-browser-sessions
Draft

Vishalan wants to merge 6 commits into
mainfrom
feat/agent-device-and-browser-sessions

Conversation

@Vishalan

@Vishalan Vishalan commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

What this adds

An MCP client can now drive a real Android or iOS device or a real desktop or mobile browser on BrowserStack step by step. It starts a session, reads the screen, taps, types, swipes and checks the result, then ends the session with a passed/failed status. The server only exposes capabilities; deciding what to do stays with the agent.

📄 Start here: docs/AGENT-DEVICE-AND-BROWSER-SESSIONS.md covers what it does, how it works, setup, prompts to try, settings, safety and limits. It's written for product and engineering readers.

Group Tools Runs on
Device catalog listMobileDevices, listDedicatedDevices, getTeamDefaultDevices, uploadMobileApp, listUploadedApps, uploadMediaFile, getAppAutomatePlan App Automate REST
Device sessions startDeviceSession, getDeviceSession, endDeviceSession, takeDeviceScreenshot, tapOnDevice, typeOnDevice, swipeOnDevice, pressDeviceKey, manageDeviceApp, uninstallDeviceApp, setDeviceState, simulateBiometric, injectCameraImage, setDeviceLocation, setNetworkConditions, pushFileToDevice, pullFileFromDevice, runAdbShellCommand App Automate (Appium hub)
Browser sessions listBrowsers, getAutomatePlan, startBrowserSession, getBrowserSession, endBrowserSession, navigateBrowser, clickInBrowser, typeInBrowser, scrollBrowser, switchBrowserContext, takeBrowserScreenshot Automate (WebDriver hub)
Shared server npm run start:http (Streamable HTTP, per-user login) src/http-server.ts

Design notes

  • App Automate, not App Live, for phones. App Live has no API an agent can drive, so agent sessions run on App Automate with the user's own credentials. Device lists and uploads use App Automate too, so names match what startDeviceSession accepts. runAppLiveSession is unchanged.
  • No guessing. Device, OS and browser names must match exactly. A near miss returns the valid options instead of a substitute.
  • Credentials: they reach the hub as a Basic auth header, never in capabilities. Every result and error is scrubbed of the username and key, and webdriverio runs silent because stdout is the MCP channel.
  • Shared HTTP server:
    • The login is checked against BrowserStack and fails closed: only a 2xx response logs in.
    • Each session gets its own server instance.
    • Device and browser sessions are owned by a hash of username + key.
    • Local-machine features (BrowserStack Local, uploading files from the server's disk) are refused in this mode.
  • Guard rails:
    • a per-user session-start limit (BROWSERSTACK_SESSION_STARTS_PER_HOUR)
    • an open-session cap (BROWSERSTACK_DEVICE_SESSION_MAX)
    • waits around actions
    • typed values never echoed and password fields masked
    • adb limited to getprop, so nothing can list apps other customers left on shared phones
    • uninstallDeviceApp marked destructive
  • Isolated from existing tools. Existing tools are untouched. The new groups register beside them and can be switched off together with BROWSERSTACK_DEVICE_SESSION_TOOLS=false.

Pre-release feedback addressed (commits 2e9f00e, 151ca21, a431dd1 and the keyboard fix after them)

From internal testing on real devices. Re-checked with BrowserStack's own sample apps (BStackSampleApp, WikipediaSample) and bstackdemo.com:

Feedback Fix Verified on real devices
The iOS back swipe does not work pressDeviceKey back starts the edge swipe exactly at x = 0 and moves in steps (a start at x = 2 or one long move is ignored by iOS). swipeOnDevice on iOS also moves in steps, so a manual edge swipe goes back too. When a screen ignores the swipe (apps that draw their own header instead of an iOS navigation bar), the tool says the screen did not change and points to the app's own back control iPhone 15, iOS 17 and 27: back returned to the previous screen in BStackSampleApp and Settings, through both pressDeviceKey and swipeOnDevice from x = 0
Read-screen tools expose locators readDeviceScreen and readBrowserPage removed; agents use screenshots Tool list no longer has them; a server test guards it
Reading a web page's HTML Removed with readBrowserPage As above
Android phones show other customers' app names runAdbShellCommand accepts getprop <property> only. BrowserStack itself accepts only some properties; its refusal now comes back as a clear message Pixel 8: pm list packages, dumpsys and a bare getprop refused; getprop ro.build.version.release and ro.product.manufacturer work
The keyboard covers fields and buttons and the agent cannot close it Taps and typing close the keyboard first when it covers the target, then find the element again. typeOnDevice gains hideKeyboard (Done, a drag above the keyboard, then Return last, as setDeviceState hideKeyboard does). When nothing closes it, the tool says so instead of reporting a tap that hit the keyboard iPhone 15, iOS 17 (BStackSampleApp): an agent typed into the text field, closed the keyboard and saw the text on screen. Android: every typed field closed the keyboard. An app whose keyboard ignores every method is reported as an app issue
The live view does not open when a session starts The live view opens in the local browser on start (https BrowserStack links only, no shell, local server only, BROWSERSTACK_OPEN_LIVE_VIEW=false to turn off) Opened automatically when a Pixel 8 session started
Closing the keyboard typed "TV" into the field The swipe-down step started on the keys, which iOS reads as swipe-typing. It now drags above the keyboard and never touches a key; a test guards it iPhone 15, iOS 17 and 27: typed text stays exactly as typed after setDeviceState hideKeyboard (live repro of the old step typed "to")
Found while re-testing manageDeviceApp launch/restart fails if the app never reached the front instead of claiming "Launched" iPhone 15: launching Settings where it is restricted now reports the home screen is still showing

End-to-end agent runs (headless Claude Code, only this server loaded): BStackSampleApp on iPhone 15 (type, close keyboard, back, alert) passed; bstackdemo.com in Safari on iPhone 15 (sign in, two items, shipping form, stop before Submit) passed; WikipediaSample on Pixel 8 searched fine (its old build can no longer load articles, an app limitation).

Testing

  • tsc and eslint are clean. The new unit tests cover keyboard handling, opening the live view, the iOS back swipe, credential scrubbing, strict resolution, session ownership, the rate limit, HTTP login (including fail-closed), the upload guards, every action against a fake driver, and registration.
  • The full suite passes except 4 timing-based tests in buildFailureThemes / tfaRcaCollaboration that already fail on main, so CI will show them.
  • Live against BrowserStack:
    • Android (Pixel 8, Android 14) and iOS (iPhone 15) sessions end to end.
    • Device extras: biometric, camera injection, media preload, network profiles, adb getprop, file push/pull, GPS.
    • Chrome on Windows 11: navigation, typing, <select>, tabs, iframes, scrolling.
    • A localhost page reached through BrowserStack Local.
    • The HTTP server's login and isolation checks.
  • MCP clients: Claude Code (stdio and HTTP) and Claude Desktop, where a chat called the tools and answered with live plan data.
  • A security review of the new code found two issues, both fixed with regression tests:
    • The HTTP login failed open on non-401/403 responses.
    • The file-push extension allow-list was derived from the input path.

Before merging

  • Security sign-off for agent access to real devices
  • Decide whether the shared HTTP mode should ship now or wait for OAuth sign-in
  • Confirm App Automate minutes and parallels are the intended billing surface for agent sessions

🤖 Generated with Claude Code

Lets an MCP client drive a real Android/iOS device (App Automate, Appium)
or a desktop/mobile browser (Automate, WebDriver) step by step: start a
session, read the screen, tap, type, swipe, press keys, manage the app,
change device state, and end it with a passed/failed status.

- Device catalog: listMobileDevices (App Automate list, exact names),
  listDedicatedDevices, getTeamDefaultDevices, uploadMobileApp,
  listUploadedApps, uploadMediaFile, getAppAutomatePlan.
- Device sessions: 19 tools, including biometric, camera image injection,
  GPS, network profiles, file push/pull and read-only adb.
- Browser sessions: 12 tools (navigate, click, type/select, scroll, tabs
  and frames, screenshot, page reading); BrowserStack Local via local: true.
- src/http-server.ts: Streamable HTTP entry point with per-user Basic login
  verified against BrowserStack (fails closed); one server instance per
  session; sessions owned by a hash of username + key.
- Guard rails: exact device/OS/browser names only, per-user session-start
  limit, open-session cap, waits around actions, credential scrubbing on
  every result and error, typed values never echoed, password fields masked.
- Existing tools are unchanged; the new groups register beside them and can
  be switched off with BROWSERSTACK_DEVICE_SESSION_TOOLS=false.
- docs/AGENT-DEVICE-AND-BROWSER-SESSIONS.md explains the feature for
  product and engineering readers; README and TOOLS.md link to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Central YAML (base), Organization UI (inherited), Workspace UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Enterprise
  • Run ID: 4a9e8f8b-9ad2-49f1-8ce7-c52bbf63e82b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Vishalan and others added 5 commits October 15, 2026 11:08
- iOS back: the edge-pan gesture now starts exactly at x = 0 and moves in
  steps. A start at x = 2 or one long move is ignored by iOS; verified on
  iPhone 15 with iOS 17 and iOS 27.
- Remove readDeviceScreen and readBrowserPage (including the page-HTML
  mode). Agents see the screen through screenshots; the server no longer
  hands out element locators or page HTML.
- runAdbShellCommand accepts getprop only. pm list / dumpsys could show
  apps other customers left on shared public phones.
- On-screen keyboard: tapOnDevice and typeOnDevice close the keyboard first
  when it covers the target element (iOS: by the keyboard's bounds; Android:
  before tapping a non-text element), then look the element up again.
  typeOnDevice gains hideKeyboard and says when the keyboard is left open.
  When an app offers no way to close the keyboard, the tools say so instead
  of reporting a tap that landed on the keyboard; a coordinate tap on the
  keyboard is noted in the result. setDeviceState hideKeyboard tries Done,
  a swipe down, then Return, and fails clearly if all three are ignored.
- Live view: starting a device or browser session opens its live view in
  the local browser (https browserstack.com links only, no shell; local
  server only; BROWSERSTACK_OPEN_LIVE_VIEW=false turns it off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while re-running the failing cases end to end with an agent:

- pressDeviceKey back on iOS compares the screen before and after the edge
  swipe. Screens an app draws with its own header instead of an iOS
  navigation bar ignore the system swipe; the tool now says so and points
  the agent at the app's own back control instead of reporting success.
  The swipe itself is unchanged (verified again in Settings).
- manageDeviceApp launch/restart checks which app is in front afterwards.
  activateApp can return while the device stays on the home screen (an app
  restricted on a shared device); that is now an error, not "Launched".
- The keyboard-blocked message suggests a route that needs no typing, or
  leaving the screen with the app's own back control, before reporting an
  app issue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… close via typeOnDevice

From a second round of internal re-testing:

- runAdbShellCommand needs a property ("getprop <property>"). A bare
  getprop is refused by BrowserStack, which also accepts only some
  properties on shared phones (verified live: ro.build.version.release and
  ro.product.manufacturer pass; the model and SDK level are refused). Its
  refusal now comes back as a clear message pointing at getDeviceSession
  for the device name and OS version.
- swipeOnDevice on iOS moves the finger in five short steps. iOS ignores an
  edge swipe made of one long move, so a manual swipe from x = 0 did not go
  back; it now does (verified on iPhone 15, iOS 17).
- typeOnDevice hideKeyboard tries Return last, as setDeviceState does, and
  says how the keyboard was closed. Without it, apps whose keyboard closes
  only on Return were wrongly reported as having no way to close it. A tap
  blocked by the keyboard now says Return was not pressed and points at
  setDeviceState hideKeyboard instead of calling it an app issue outright.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the keyboard tried a downward swipe that started on the keys. iOS
reads a drag that starts on the keyboard as swipe-typing, so a word was
typed into the focused field ("TV" in internal testing; "to" in a live
repro on iPhone 15, iOS 27). That step now drags down above the keyboard
and stops short of it, so no key is ever touched. A test checks that no
pointer move lands on the keyboard while closing it.

Verified on iPhone 15, iOS 17 and 27: typed text stays exactly as typed
after setDeviceState hideKeyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant