@browserstack/mcp-server 2.3.0-beta.6 → 2.3.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -253,18 +253,25 @@
253
253
  "mode": "write",
254
254
  "entity": "browser_session",
255
255
  "intent": "Start a browser session on a real desktop or mobile browser and get a session id to drive it with \u2014 use this to open a page and interact with it step by step.",
256
- "guidance": [
257
- "A DESKTOP target needs browser_name with os and os_version; a MOBILE BROWSER needs browser_name with device_name and real_mobile instead, and takes no browser_version because the browser ships with the OS. Sending both kinds together is refused.",
258
- "ON MOBILE THE BROWSER IS NAMED AFTER THE PLATFORM, not the product: Chrome on an Android phone is \"android\", Safari on an iPhone is \"iphone\" and on an iPad \"ipad\". Samsung Internet is \"samsung\" and Chromium-based mobile browsers are \"chromium\". Asking for \"chrome\" or \"safari\" on a phone is refused.",
259
- "Take every value from list_browsers exactly as it spells them; an unlisted combination is refused rather than substituted.",
260
- "The session id comes back at value.sessionId \u2014 carry it into every other browser_session capability.",
261
- "A session starts in seconds rather than the tens of seconds a device session takes, but it still holds a browser and consumes Automate minutes until it ends: always finish with set_session_status and then delete_session.",
262
- "The session ends itself after idle_timeout seconds without a command, and after 2 hours in total.",
263
- "console_logs and network_logs must be asked for here; they cannot be turned on once the session is running, and get_session only links to them when they were.",
264
- "Credentials belong in the Authorization header, never inside bstack:options: capabilities are echoed back in session-not-created errors.",
265
- "resolution takes one of a fixed set of values per operating system, not any width and height: an unsupported pair is refused outright rather than rounded to the nearest one. The browser window is well short of the screen it sits on \u2014 1920x1080 gives a viewport around 930x860 \u2014 so measure it rather than assuming the request was honoured.",
266
- "THIS CALL RETURNS NO LINK \u2014 only value.sessionId. Nothing here can open a browser for the user either: this surface makes HTTP requests, and on a remote server there is no browser to open. So never say a live view was opened. If the user may want to watch, call get_session straight after this and give them browser_url in the same reply, which is the session on the dashboard and needs a BrowserStack login. public_url needs no login but carries an auth token, so offer it only when they ask to share outside the team."
267
- ],
256
+ "guidance": {
257
+ "shared": [
258
+ "A DESKTOP target needs browser_name with os and os_version; a MOBILE BROWSER needs browser_name with device_name and real_mobile instead, and takes no browser_version because the browser ships with the OS. Sending both kinds together is refused.",
259
+ "ON MOBILE THE BROWSER IS NAMED AFTER THE PLATFORM, not the product: Chrome on an Android phone is \"android\", Safari on an iPhone is \"iphone\" and on an iPad \"ipad\". Samsung Internet is \"samsung\" and Chromium-based mobile browsers are \"chromium\". Asking for \"chrome\" or \"safari\" on a phone is refused.",
260
+ "Take every value from list_browsers exactly as it spells them; an unlisted combination is refused rather than substituted.",
261
+ "The session id comes back at value.sessionId \u2014 carry it into every other browser_session capability.",
262
+ "A session starts in seconds rather than the tens of seconds a device session takes, but it still holds a browser and consumes Automate minutes until it ends: always finish with set_session_status and then delete_session.",
263
+ "The session ends itself after idle_timeout seconds without a command, and after 2 hours in total.",
264
+ "console_logs and network_logs must be asked for here; they cannot be turned on once the session is running, and get_session only links to them when they were.",
265
+ "Credentials belong in the Authorization header, never inside bstack:options: capabilities are echoed back in session-not-created errors.",
266
+ "resolution takes one of a fixed set of values per operating system, not any width and height: an unsupported pair is refused outright rather than rounded to the nearest one. The browser window is well short of the screen it sits on \u2014 1920x1080 gives a viewport around 930x860 \u2014 so measure it rather than assuming the request was honoured."
267
+ ],
268
+ "local": [
269
+ "ON THE USER'S OWN MACHINE YOU CAN OPEN THE LIVE VIEW FOR THEM. This call returns only value.sessionId; call get_session straight after it, then run the shell command open \"<browser_url>\" on macOS or xdg-open \"<browser_url>\" on Linux, and say you opened the session on the Automate dashboard \u2014 it needs a BrowserStack login in that browser."
270
+ ],
271
+ "remote": [
272
+ "THIS CALL RETURNS NO LINK \u2014 only value.sessionId. Nothing here can open a browser for the user either: this surface makes HTTP requests, and on a remote server there is no browser to open. So never say a live view was opened. If the user may want to watch, call get_session straight after this and give them browser_url in the same reply, which is the session on the dashboard and needs a BrowserStack login. public_url needs no login but carries an auth token, so offer it only when they ask to share outside the team."
273
+ ]
274
+ },
268
275
  "body": [
269
276
  {
270
277
  "name": "browser_name",
@@ -3,7 +3,7 @@
3
3
  "version": "1.0",
4
4
  "build_id": "6782f7e_2026-10-16T13:54:22Z",
5
5
  "mobiledevicecontrol": {
6
- "summary": "Mobile device control: run a real Android or iOS device and drive the app on it step by step. The account side lists the devices this account can use, including private ones, uploads and lists app builds, reports parallel-session capacity, and keeps each session's record \u2014 status, shareable link and video \u2014 after the device is released. The session side starts a device with an installed build and drives it: read the UI tree, find and tap elements, type, swipe, screenshot, press keys, rotate, switch between the native app and a web view, move between the native app and a web view, and launch or close apps. Apps are RUN here \u2014 unlike Test Management, which authors cases, and Test Reporting, which records results pushed in from a runner. Browsers are not here \u2014 those are device control, which drives a page rather than an installed app \u2014 and neither is App Live: it has no API an agent can drive, so agent sessions run on App Automate and consume its minutes. A build must exist before a session starts and can only be uploaded from a public link, never a file on disk \u2014 so media for camera injection must come from elsewhere. The default Appium server is 1.x and rejects every `mobile:` command; ask for 2.19.0.",
6
+ "summary": "Mobile device control: run a real Android or iOS device and drive the app on it step by step. The account side lists the devices this account can use, including private ones, uploads and lists app builds, reports parallel-session capacity, and keeps each session's record \u2014 status, shareable link and video \u2014 after the device is released. The session side starts a device with an installed build and drives it: find elements and read their text, position and attributes, tap them, type, swipe, screenshot, press keys, rotate, switch between the native app and a web view, move between the native app and a web view, and launch or close apps. Apps are RUN here \u2014 unlike Test Management, which authors cases, and Test Reporting, which records results pushed in from a runner. Browsers are not here \u2014 those are device control, which drives a page rather than an installed app \u2014 and neither is App Live: it has no API an agent can drive, so agent sessions run on App Automate and consume its minutes. A build must exist before a session starts and can only be uploaded from a public link, never a file on disk \u2014 so media for camera injection must come from elsewhere. The default Appium server is 1.x and rejects every `mobile:` command; ask for 2.19.0.",
7
7
  "routing_terms": [
8
8
  "app automate",
9
9
  "appium",
@@ -69,25 +69,32 @@
69
69
  "mode": "write",
70
70
  "entity": "app_session",
71
71
  "intent": "Start a live Appium session on a real Android or iOS device with an uploaded app \u2014 use this before reading the screen, tapping, typing or swiping in the app.",
72
- "guidance": [
73
- "The app must already be uploaded: pass the bs:// URL from upload_app_build or list_app_builds, or the custom_id it was uploaded under, which always resolves to the newest build with that id.",
74
- "Allocation and app install usually take 20\u201330 seconds; the response carries the new session id at value.sessionId \u2014 carry it into every other app_session call.",
75
- "The session ends itself after idle_timeout seconds with no command (90 by default, 900 at most) and after 2 hours in total. Keep calling, or call get_window_rect as a cheap keep-alive, while deciding the next step.",
76
- "Every session occupies a device and consumes App Automate minutes until it ends: always finish with set_session_status and then delete_session.",
77
- "To let a person watch the device live while the agent drives it, set interactive_debugging true and leave video on; they then open the running session on the App Automate dashboard and start the interactive session there. That window is two-way: a person's taps and scrolls reach the same device, so ask them to watch only, or re-read the page source before every action.",
78
- "device_name and platform_version must be a combination mobile device control offers; an unknown pair fails with a session-not-created error naming the capability.",
79
- "Set appium_version to 2.19.0 for any session that will scroll with scroll_gesture, dismiss an iOS keyboard with dismiss_keyboard_key, or use any other mobile: capability; the default server is 1.x and answers those with a 404 listing the commands it does know.",
80
- "enable_biometric, enable_camera_image_injection and upload_media only take effect when set here \u2014 none of them can be turned on once the session is running.",
81
- "Credentials belong in the Authorization header, never inside bstack:options: capabilities are echoed back in session-not-created errors.",
82
- "device_name and platform_version come from list_devices; get_plan first if sessions are queueing rather than starting.",
83
- "Treat a response without value.sessionId as a failed start even when the status is 200, and treat a failed call the same way: allocation can outlast the client's own timeout, so BrowserStack may have started a session the answer never named. One is then holding a device and consuming minutes until its idle timeout.",
84
- "After a start that failed or returned no session id, look for a session left running on the account and end it rather than starting another.",
85
- "The response carries no link of any kind \u2014 it is the session id and the negotiated capabilities only. Call get_session for the links: browser_url to watch the run on the dashboard, and public_url to share it with someone who has no BrowserStack account.",
86
- "LANGUAGE AND LOCALE LAND IN DIFFERENT PLACES ON THE TWO PLATFORMS. On Android they change the device, and the app with it \u2014 measured with language de and locale DE: the system's own permission dialog came up in German and the app rendered German throughout. On iOS they leave the device alone \u2014 Settings and General stay in English \u2014 and apply to the app under test at launch. Either way nothing is visible unless the app actually ships that language, so test with an app that does and read the strings back from get_page_source rather than trusting the call's 200.",
87
- "Both are accepted without complaint on both platforms, so a 200 says nothing about whether anything changed. There is also no way to confirm the device locale from inside the session: read_device_property refuses getprop persist.sys.locale and ro.product.locale as unsupported commands. Judge it from what is on screen.",
88
- "THIS CALL RETURNS NO LINK \u2014 only value.sessionId. Nothing here can open a browser for the user either: this surface makes HTTP requests, and on a remote server there is no browser to open. So never say a live view was opened. If the user may want to watch, call get_session straight after this and give them browser_url in the same reply, which is the session on the dashboard and needs a BrowserStack login. public_url needs no login but carries an auth token, so offer it only when they ask to share outside the team.",
89
- "Someone can only WATCH AND INTERACT if interactive_debugging was set true in this call \u2014 it cannot be turned on once the session is running, it is off by default on Android, and it needs video left on. Ask for it up front when a person is going to follow along, because the alternative is restarting the session."
90
- ],
72
+ "guidance": {
73
+ "shared": [
74
+ "The app must already be uploaded: pass the bs:// URL from upload_app_build or list_app_builds, or the custom_id it was uploaded under, which always resolves to the newest build with that id.",
75
+ "Allocation and app install usually take 20\u201330 seconds; the response carries the new session id at value.sessionId \u2014 carry it into every other app_session call.",
76
+ "The session ends itself after idle_timeout seconds with no command (90 by default, 900 at most) and after 2 hours in total. Keep calling, or call get_window_rect as a cheap keep-alive, while deciding the next step.",
77
+ "Every session occupies a device and consumes App Automate minutes until it ends: always finish with set_session_status and then delete_session.",
78
+ "To let a person watch the device live while the agent drives it, set interactive_debugging true and leave video on; they then open the running session on the App Automate dashboard and start the interactive session there. That window is two-way: a person's taps and scrolls reach the same device, so ask them to watch only, or find the element you are about to act on again before every action.",
79
+ "device_name and platform_version must be a combination mobile device control offers; an unknown pair fails with a session-not-created error naming the capability.",
80
+ "Set appium_version to 2.19.0 for any session that will scroll with scroll_gesture, dismiss an iOS keyboard with dismiss_keyboard_key, or use any other mobile: capability; the default server is 1.x and answers those with a 404 listing the commands it does know.",
81
+ "enable_biometric, enable_camera_image_injection and upload_media only take effect when set here \u2014 none of them can be turned on once the session is running.",
82
+ "Credentials belong in the Authorization header, never inside bstack:options: capabilities are echoed back in session-not-created errors.",
83
+ "device_name and platform_version come from list_devices; get_plan first if sessions are queueing rather than starting.",
84
+ "Treat a response without value.sessionId as a failed start even when the status is 200, and treat a failed call the same way: allocation can outlast the client's own timeout, so BrowserStack may have started a session the answer never named. One is then holding a device and consuming minutes until its idle timeout.",
85
+ "After a start that failed or returned no session id, look for a session left running on the account and end it rather than starting another.",
86
+ "The response carries no link of any kind \u2014 it is the session id and the negotiated capabilities only. Call get_session for the links: browser_url to watch the run on the dashboard, and public_url to share it with someone who has no BrowserStack account.",
87
+ "LANGUAGE AND LOCALE LAND IN DIFFERENT PLACES ON THE TWO PLATFORMS. On Android they change the device, and the app with it \u2014 measured with language de and locale DE: the system's own permission dialog came up in German and the app rendered German throughout. On iOS they leave the device alone \u2014 Settings and General stay in English \u2014 and apply to the app under test at launch. Either way nothing is visible unless the app actually ships that language, so test with an app that does and read the strings back with get_element_text rather than trusting the call's 200.",
88
+ "Both are accepted without complaint on both platforms, so a 200 says nothing about whether anything changed. There is also no way to confirm the device locale from inside the session: read_device_property refuses getprop persist.sys.locale and ro.product.locale as unsupported commands. Judge it from what is on screen.",
89
+ "Someone can only WATCH AND INTERACT if interactive_debugging was set true in this call \u2014 it cannot be turned on once the session is running, it is off by default on Android, and it needs video left on. Ask for it up front when a person is going to follow along, because the alternative is restarting the session."
90
+ ],
91
+ "local": [
92
+ "ON THE USER'S OWN MACHINE YOU CAN OPEN THE LIVE VIEW FOR THEM. This call returns only value.sessionId; call get_session straight after it, then run the shell command open \"<browser_url>\" on macOS or xdg-open \"<browser_url>\" on Linux, and say you opened the session on the App Automate dashboard \u2014 it needs a BrowserStack login in that browser. Set interactive_debugging true in this call when they should be able to tap as well as watch; without it the view is watch-only."
93
+ ],
94
+ "remote": [
95
+ "THIS CALL RETURNS NO LINK \u2014 only value.sessionId. Nothing here can open a browser for the user either: this surface makes HTTP requests, and on a remote server there is no browser to open. So never say a live view was opened. If the user may want to watch, call get_session straight after this and give them browser_url in the same reply, which is the session on the dashboard and needs a BrowserStack login. public_url needs no login but carries an auth token, so offer it only when they ask to share outside the team."
96
+ ]
97
+ },
91
98
  "body": [
92
99
  {
93
100
  "name": "platform_name",
@@ -321,11 +328,7 @@
321
328
  "entity": "app_session",
322
329
  "intent": "Read the full UI tree of the screen currently shown in the app \u2014 use this to see what is on screen and choose what to tap, type into or scroll before acting.",
323
330
  "guidance": [
324
- "Returns the whole hierarchy as one XML string in value, unfiltered. On Android each node carries text, content-desc, resource-id, clickable, scrollable and bounds; on iOS name, label, value and x/y/width/height.",
325
- "Read it again after every action: the tree is a snapshot, and element ids from find_element go stale once the screen changes.",
326
- "Android bounds are \"[x1,y1][x2,y2]\" in device pixels \u2014 the centre of that box is the coordinate to use with perform_actions.",
327
- "A product-specific attribute is the best locator: resource-id for find_element using \"id\", content-desc for \"accessibility id\".",
328
- "Inside a web view this returns the page's HTML instead of the native tree, and a real page runs to hundreds of kilobytes where a native screen is a few. Switch back to NATIVE_APP before reading the tree again, or search the HTML for what you need rather than reading all of it."
331
+ "Not published: the whole tree runs to tens of kilobytes, more than a caller can read. Find the elements you need with find_element or find_elements and read them with get_element_text, get_element_rect and get_element_attribute."
329
332
  ],
330
333
  "path_params": [
331
334
  {
@@ -349,7 +352,8 @@
349
352
  "404": {
350
353
  "$response": "WebDriverError"
351
354
  }
352
- }
355
+ },
356
+ "disabled": true
353
357
  },
354
358
  {
355
359
  "name": "find_element",
@@ -360,8 +364,8 @@
360
364
  "intent": "Locate one element on the current screen by id, accessibility id, XPath or UiAutomator selector \u2014 use this to get the element id that click_element and send_keys act on.",
361
365
  "guidance": [
362
366
  "The element id comes back under the W3C key value[\"element-6066-11e4-a52e-4f735466cecf\"]; pass that string as element_id.",
363
- "Take the selector from get_page_source rather than guessing: resource-id \u2192 \"id\", content-desc (Android) or name (iOS) \u2192 \"accessibility id\".",
364
- "No match is a 404 with error \"no such element\" \u2014 re-read the page source; the screen may have changed or the element may be off-screen and need a scroll first.",
367
+ "Locators by platform: resource-id \u2192 \"id\", content-desc (Android) or name (iOS) \u2192 \"accessibility id\", visible text \u2192 xpath such as //*[@text='Submit'] on Android or -ios predicate string label == 'Submit' on iOS. Once you hold an element, get_element_attribute reads its ids and get_element_text what it says.",
368
+ "No match is a 404 with error \"no such element\" \u2014 the screen may have changed, or the element may be off-screen and need a scroll first; find it again after the scroll. \"No such element\" is also the cheapest way to confirm something is NOT on screen.",
365
369
  "\"-android uiautomator\" accepts a UiSelector expression, which can scroll an element into view: new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text(\"Checkout\")).",
366
370
  "find_elements answers the same query with a list and never a 404, which is how to ask whether something is present without treating absence as a failure."
367
371
  ],
@@ -421,8 +425,8 @@
421
425
  "intent": "Tap an element in the app \u2014 use this to press a button, open a menu item, select a list row or focus a field found with find_element.",
422
426
  "guidance": [
423
427
  "element_id must come from find_element on the current screen; after the screen changes it is stale and fails with \"stale element reference\".",
424
- "To tap something that has no usable locator, tap its bounds centre with perform_actions instead.",
425
- "Read get_page_source afterwards to confirm the tap had the intended effect \u2014 a 200 only means the tap was delivered.",
428
+ "To tap something that has no usable locator, take the centre of a nearby element's get_element_rect and tap the coordinates with perform_actions instead.",
429
+ "Confirm the tap had the intended effect \u2014 a 200 only means it was delivered: find an element the new screen should carry, or read a label with get_element_text before and after.",
426
430
  "A keyboard covers the lower part of the screen and a tap aimed underneath it lands on the keyboard while still reporting success. When the target sits low, call is_keyboard_shown and then hide_keyboard before tapping."
427
431
  ],
428
432
  "path_params": [
@@ -463,10 +467,10 @@
463
467
  "entity": "app_session",
464
468
  "intent": "Type text into an input field in the app \u2014 use this to fill a search box, login form or any editable field found with find_element.",
465
469
  "guidance": [
466
- "WHETHER THIS REPLACES OR APPENDS DEPENDS ON THE PLATFORM: on Android it replaces whatever the field already holds, and on iOS it appends to it. Typing twice into one iOS field therefore leaves both values joined. Read the field back from get_page_source whenever the resulting value matters.",
470
+ "WHETHER THIS REPLACES OR APPENDS DEPENDS ON THE PLATFORM: on Android it replaces whatever the field already holds, and on iOS it appends to it. Typing twice into one iOS field therefore leaves both values joined. Read the field back with get_element_text whenever the resulting value matters.",
467
471
  "The element must be an editable field (EditText on Android, TextField on iOS); typing into anything else fails with \"invalid element state\".",
468
- "The on-screen keyboard may stay open afterwards and cover lower elements \u2014 re-read the page source before the next tap.",
469
- "clear_element empties a field, and on iOS \u2014 where this APPENDS \u2014 it is the only way to take back what is already there. It answers 200 whether or not the field changed, so read the field back from get_page_source rather than trusting the status.",
472
+ "The on-screen keyboard may stay open afterwards and cover lower elements \u2014 check is_keyboard_shown, and compare get_element_rect of the next target with the screen's lower part, before the next tap.",
473
+ "clear_element empties a field, and on iOS \u2014 where this APPENDS \u2014 it is the only way to take back what is already there. It answers 200 whether or not the field changed, so read the field back with get_element_text rather than trusting the status.",
470
474
  "The keyboard is usually left open afterwards. On Android and on an iPad, hide_keyboard closes it. On an iPhone it cannot be closed without submitting, so read hide_keyboard's guidance before planning a tap below the field.",
471
475
  "Sending a newline closes the keyboard on iOS by pressing Return, which an app that commits on editing-ended will treat as a submit \u2014 the field resets and the value moves wherever the app sends it. The commit is the app's doing, not the newline's: dismissing the same field any other way can commit it too. See hide_keyboard."
472
476
  ],
@@ -517,7 +521,7 @@
517
521
  "intent": "Empty an editable field in the app \u2014 use this before typing when the field already holds something and you need to replace rather than add to it.",
518
522
  "guidance": [
519
523
  "Takes no parameters of its own.",
520
- "CONFIRM IT ACTUALLY EMPTIED THE FIELD by reading the field back from get_page_source: this endpoint can answer 200 without the field changing, and the status alone does not tell you which happened.",
524
+ "CONFIRM IT ACTUALLY EMPTIED THE FIELD by reading it back with get_element_text: this endpoint can answer 200 without the field changing, and the status alone does not tell you which happened.",
521
525
  "Needed far more on iOS than on Android: send_keys REPLACES the field's contents on Android, so clearing first is usually unnecessary there, while on iOS it APPENDS and this is the only way to take back what is already in the field.",
522
526
  "The element must be an editable field \u2014 EditText on Android, TextField on iOS. Anything else answers \"invalid element state\".",
523
527
  "The element_id must come from find_element on the current screen; after the screen changes it is stale and fails with \"stale element reference\"."
@@ -561,9 +565,9 @@
561
565
  "entity": "app_session",
562
566
  "intent": "Perform touch gestures on the device screen by coordinates \u2014 tap a point, swipe or scroll a list, long-press, or drag \u2014 using W3C pointer actions.",
563
567
  "guidance": [
564
- "Coordinates are device pixels in the same space as the page-source bounds; take them from there rather than from a screenshot, whose height can include the system bars.",
568
+ "Coordinates are device pixels on Android and points on iOS \u2014 the space get_element_rect and get_window_rect report in; take them from there rather than from a screenshot, whose height can include the system bars.",
565
569
  "Tap: one touch pointer with pointerMove(duration 0, x, y), pointerDown(button 0), pause(100), pointerUp(button 0).",
566
- "Swipe up to scroll content down: pointerMove to a low point, pointerDown, pointerMove(duration 600) to a higher point, pointerUp. Keep the start and end inside the scrollable element's bounds.",
570
+ "Swipe up to scroll content down: pointerMove to a low point, pointerDown, pointerMove(duration 600) to a higher point, pointerUp. Keep the start and end inside the scrollable element's get_element_rect. For a plain scroll or swipe, scroll_gesture (Android) and scroll_view (iOS) need no coordinates at all.",
567
571
  "Long-press: replace the pause duration with 1000 or more.",
568
572
  "For a scroll without computing coordinates, scroll_gesture is simpler on Android and reports whether more content remains; on iOS use scroll_view; compute a swipe here only when neither fits.",
569
573
  "To go back on an iOS screen that ignores press_back, swipe in from the left edge: a pointer sequence starting at exactly x=0 and moving right in several short steps. A single long move does not register."
@@ -662,10 +666,11 @@
662
666
  "path": "/wd/hub/session/{session_id}/execute/sync",
663
667
  "mode": "write",
664
668
  "entity": "app_session",
665
- "intent": "Scroll a region of the screen in a direction \u2014 use this to move through a list or page without working out start and end coordinates yourself.",
669
+ "intent": "Swipe or scroll a region of the screen in a direction \u2014 use this to move through a list or page, or to swipe left and right through a carousel of cards, without working out start and end coordinates yourself.",
666
670
  "guidance": [
667
671
  "ANDROID ONLY. On iOS this is a 405 \"unknown method\"; scroll_view is the iOS counterpart.",
668
- "The returned value is true when the view can still scroll further in that direction and false when it cannot \u2014 but the FIRST scroll of a freshly opened list can answer false while the screen moved and more content followed, so treat false as a hint and confirm from the page source before deciding you are at the end.",
672
+ "The returned value is true when the view can still scroll further in that direction and false when it cannot \u2014 but the FIRST scroll of a freshly opened list can answer false while the screen moved and more content followed, so treat false as a hint and confirm by finding the element you expect before deciding you are at the end.",
673
+ "A HORIZONTAL SWIPE IS THE SAME CALL with direction left or right: a rectangle covering the carousel's band of the screen, taken from get_window_rect, is enough \u2014 no element bounds are needed, and perform_actions is the harder way to do the same thing. DIRECTION NAMES WHERE THE CONTENT COMES FROM, as with a list: down brings what is below into view (the finger moves up), so right brings the NEXT card and left the previous one.",
669
674
  "The area is a rectangle in screen pixels \u2014 get_window_rect gives the screen size to base it on \u2014 and percent is how far to scroll within it, 0 to 1.",
670
675
  "Needs Appium 2.x: set appium_version to 2.19.0 at session start, since the default 1.x server answers mobile: commands with a 404 listing what it does know."
671
676
  ],
@@ -691,7 +696,7 @@
691
696
  "name": "args",
692
697
  "type": "array",
693
698
  "required": true,
694
- "description": "Exactly one object: the rectangle to scroll inside, the direction, and how far.",
699
+ "description": "Exactly one object: the rectangle to scroll inside, the direction (up, down, left or right), and how far.",
695
700
  "example": [
696
701
  {
697
702
  "left": 100,
@@ -728,13 +733,14 @@
728
733
  "path": "/wd/hub/session/{session_id}/execute/sync",
729
734
  "mode": "write",
730
735
  "entity": "app_session",
731
- "intent": "Scroll a scrollable view on iOS in a direction, or until a named element is visible \u2014 use this to move through a list or a long screen.",
736
+ "intent": "Swipe or scroll a scrollable view on iOS in a direction, or until a named element is visible \u2014 use this to move through a list or a long screen, or to swipe left and right through a carousel.",
732
737
  "guidance": [
733
738
  "iOS ONLY, and the counterpart of scroll_gesture. On Android the same command exists with a different shape and answers 400 \"invalid argument\" naming strategy and selector as missing; use scroll_gesture there.",
734
739
  "Scroll by direction with {\"direction\":\"down\"}, or stop when something comes into view with {\"predicateString\":\"name == 'Submit'\", \"toVisible\": true} \u2014 the second is the reliable way to reach a known control without guessing how far to go.",
740
+ "A HORIZONTAL SWIPE IS THE SAME CALL with direction left or right, optionally with elementId naming the carousel; no coordinates are needed.",
735
741
  "Pass elementId in args to scroll inside that view rather than the whole screen; without one it scrolls the first scrollable view it finds, which may not be the one you meant.",
736
742
  "Needs Appium 2.x: set appium_version to 2.19.0 at session start, since the default 1.x server answers mobile: commands with a 404 listing what it does know.",
737
- "Confirm from get_page_source that the screen actually moved \u2014 this answers 200 even on a screen with nothing scrollable, so a 200 only says the gesture was delivered."
743
+ "Confirm the screen actually moved \u2014 find the element you were scrolling toward, or compare get_element_rect of one element before and after \u2014 since this answers 200 even on a screen with nothing scrollable: a 200 only says the gesture was delivered."
738
744
  ],
739
745
  "path_params": [
740
746
  {
@@ -793,7 +799,7 @@
793
799
  "intent": "Close the on-screen keyboard by pressing one of its own keys \u2014 use this on an iPhone, where the plain hide_keyboard is refused.",
794
800
  "guidance": [
795
801
  "hide_keyboard already presses these keys on an iPhone when its plain call is refused \u2014 Done-type keys first, then Return \u2014 and reports each attempt. Call this directly to press exactly the key you choose: Return alone, or every key but Return when a submit must not happen.",
796
- "The key must EXIST on that keyboard or this returns the same 400: read the keyboard from get_page_source and pick from the keys that are really there. An iPad carries \"Hide keyboard\"; an iPhone keyboard of this kind usually offers only Return.",
802
+ "The key must EXIST on that keyboard or this returns the same 400: list the keyboard's keys with find_elements using class name XCUIElementTypeKey and read each with get_element_text, then pick from the keys that are really there. An iPad carries \"Hide keyboard\"; an iPhone keyboard of this kind usually offers only Return.",
797
803
  "Several candidates may be listed in one call and it costs nothing, but it still fails when the keyboard carries none of them \u2014 measured with [\"Done\",\"done\",\"Hide keyboard\",\"Dismiss\"].",
798
804
  "PRESSING RETURN SUBMITS. An app that commits when the field stops being edited will commit however the keyboard is closed, so check the field afterwards rather than assuming only the keyboard moved.",
799
805
  "Confirm with is_keyboard_shown; on Android and on an iPad hide_keyboard closes it on its own."
@@ -925,7 +931,7 @@
925
931
  "entity": "app_session",
926
932
  "intent": "Get the device screen size in pixels \u2014 use this to plan gesture coordinates, or as a cheap keep-alive so an idle session is not ended while deciding the next step.",
927
933
  "guidance": [
928
- "On Android the height excludes the system bars, so it can be smaller than a screenshot's height; page-source bounds use this same space."
934
+ "On Android the height is the WHOLE screen including the status bar \u2014 2340 on a Galaxy S24 whose usable area starts 91px down \u2014 so the top strip of that rectangle is not the app's. On iOS it is the app's window in points."
929
935
  ],
930
936
  "path_params": [
931
937
  {
@@ -1453,7 +1459,7 @@
1453
1459
  "entity": "app_session",
1454
1460
  "intent": "Take a screenshot of what is on the device right now, as a base64 PNG \u2014 use this to see the screen as a user would, including anything the UI tree does not describe.",
1455
1461
  "guidance": [
1456
- "The image is returned inline as base64 in value and a phone screen runs to hundreds of kilobytes, so fetch it only when the picture is needed and prefer get_page_source for deciding what to act on.",
1462
+ "The image is returned inline as base64 in value and a phone screen runs to hundreds of kilobytes, so fetch it only when the picture is needed and prefer find_element with get_element_text for deciding what to act on.",
1457
1463
  "Screenshot pixels are the device's full resolution and can be taller than the rectangle get_window_rect reports, because that excludes the system bars; scale before reusing screenshot coordinates for gestures.",
1458
1464
  "A screen the app marks secure comes back black, which is the app's doing and not a failure."
1459
1465
  ],
@@ -1489,8 +1495,8 @@
1489
1495
  "intent": "Go back one screen \u2014 use this to leave a detail page, close a dialog, or undo a navigation step.",
1490
1496
  "guidance": [
1491
1497
  "On Android this is the system back button and is the reliable way back.",
1492
- "On iOS this works on a screen that uses a standard navigation bar, where it acts on the bar's own back control. A screen that draws its own header instead is left where it was, and the call still answers 200 \u2014 so confirm from the page source rather than from the status.",
1493
- "Pressing back on the first screen of an app can send the app to the background and show the home screen; read the page source afterwards to confirm where you are.",
1498
+ "On iOS this works on a screen that uses a standard navigation bar, where it acts on the bar's own back control. A screen that draws its own header instead is left where it was, and the call still answers 200 \u2014 so confirm by finding an element of the screen you expect rather than from the status.",
1499
+ "Pressing back on the first screen of an app can send the app to the background and show the home screen; find one of the app's own elements afterwards to confirm where you are \u2014 \"no such element\" means it went to the background.",
1494
1500
  "The response is null on success and says nothing about whether the screen actually changed.",
1495
1501
  "When an iOS screen ignores it, swipe in from the left edge with perform_actions: start at exactly x=0 and move in several short steps. One long move, or a start at x=2, is ignored."
1496
1502
  ],
@@ -1560,7 +1566,7 @@
1560
1566
  "guidance": [
1561
1567
  "An app that locks its orientation refuses the rotation with a 400 'invalid element state' saying the screen could not be changed; that is the app's choice, not a broken call.",
1562
1568
  "The value must be upper case, PORTRAIT or LANDSCAPE.",
1563
- "Element positions and the window rectangle change after a rotation, so re-read the page source before acting on coordinates.",
1569
+ "Element positions and the window rectangle change after a rotation, so re-read get_element_rect and get_window_rect before acting on coordinates.",
1564
1570
  "The response carries the new orientation on Android and null on iOS, so treat a 200 rather than the value as the sign it worked, and read get_orientation if the value matters."
1565
1571
  ],
1566
1572
  "path_params": [
@@ -1641,7 +1647,7 @@
1641
1647
  "intent": "Switch between the native app and a web view \u2014 use this when the part of the screen you need to drive is web content inside the app.",
1642
1648
  "guidance": [
1643
1649
  "Use a name exactly as list_contexts returned it; an invented WEBVIEW name is rejected.",
1644
- "In a web view, selectors change: find_element then takes css selector and the page source is HTML rather than the native UI tree.",
1650
+ "In a web view, selectors change: find_element then takes css selector and get_element_text reads the page's text rather than a native label.",
1645
1651
  "Switch back to NATIVE_APP before driving native controls again.",
1646
1652
  "A web view's name carries the process id of the app that owns it, so it differs between sessions and after the app restarts. Read it from list_contexts each time rather than reusing one seen earlier."
1647
1653
  ],
@@ -2456,7 +2462,7 @@
2456
2462
  "The session must have been started with enable_biometric true; it cannot be turned on once the session is running.",
2457
2463
  "Call it only while the prompt is actually on screen \u2014 with no prompt showing it fails with BROWSERSTACK_COMMAND_EXECUTION_FAILED.",
2458
2464
  "biometricMatch takes \"pass\" or \"fail\"; \"fail\" is how to exercise the app's rejection path.",
2459
- "Read the page source afterwards to see what the app did with the answer.",
2465
+ "Afterwards, find the element the app shows on success or on failure to see what it did with the answer.",
2460
2466
  "Every failure seen so far reports the same BROWSERSTACK_COMMAND_EXECUTION_FAILED regardless of cause, so it does not distinguish 'no prompt on screen' from 'the session was not started with enable_biometric'. Check both."
2461
2467
  ],
2462
2468
  "path_params": [
@@ -2649,7 +2655,7 @@
2649
2655
  "guidance": [
2650
2656
  "iOS only; press_keycode is the Android equivalent and the two are not interchangeable.",
2651
2657
  "The button names carry no separator: home, volumeup, volumedown.",
2652
- "Pressing home sends the app to the background \u2014 bring it back with launch_app, whose args object takes bundleId rather than appId on iOS, and confirm from the page source that the app is in front again.",
2658
+ "Pressing home sends the app to the background \u2014 bring it back with launch_app, whose args object takes bundleId rather than appId on iOS, and confirm the app is in front again by finding one of its elements.",
2653
2659
  "The response is null on success and says nothing about what the button did."
2654
2660
  ],
2655
2661
  "path_params": [
@@ -2996,6 +3002,144 @@
2996
3002
  "$response": "WebDriverError"
2997
3003
  }
2998
3004
  }
3005
+ },
3006
+ {
3007
+ "name": "get_element_text",
3008
+ "method": "GET",
3009
+ "path": "/wd/hub/session/{session_id}/element/{element_id}/text",
3010
+ "mode": "read",
3011
+ "entity": "app_session",
3012
+ "intent": "Read the visible text of an element on the screen \u2014 use this to check a label, a title, a price, a field's current contents or an error message without reading the whole screen.",
3013
+ "guidance": [
3014
+ "THIS IS HOW TO READ THE SCREEN: find_element gives the id, this gives what it says \u2014 one line per call, nothing to wade through.",
3015
+ "On Android it is the text of the view; a container answers with an empty string, not its children's text, so aim at the TextView itself (class name android.widget.TextView, or the element by its own text).",
3016
+ "On iOS it is the element's label or value \u2014 a text field answers with what it currently holds, or its placeholder when empty.",
3017
+ "To confirm an action did something, read the same element before and after, or find the text you expect with find_element by text and treat \"no such element\" as the answer."
3018
+ ],
3019
+ "path_params": [
3020
+ {
3021
+ "name": "session_id",
3022
+ "type": "string",
3023
+ "required": true,
3024
+ "example": "0123456789abcdef0123456789abcdef01234567"
3025
+ },
3026
+ {
3027
+ "name": "element_id",
3028
+ "type": "string",
3029
+ "required": true,
3030
+ "description": "Element id from find_element or find_elements.",
3031
+ "example": "00000000-0000-0001-ffff-ffff00000012"
3032
+ }
3033
+ ],
3034
+ "returns": [
3035
+ "value"
3036
+ ],
3037
+ "responses": {
3038
+ "200": {
3039
+ "description": "The element's visible text.",
3040
+ "schema": {
3041
+ "$schema": "StringValueResponse"
3042
+ }
3043
+ },
3044
+ "404": {
3045
+ "$response": "WebDriverError"
3046
+ }
3047
+ }
3048
+ },
3049
+ {
3050
+ "name": "get_element_rect",
3051
+ "method": "GET",
3052
+ "path": "/wd/hub/session/{session_id}/element/{element_id}/rect",
3053
+ "mode": "read",
3054
+ "entity": "app_session",
3055
+ "intent": "Get an element's position and size on the screen \u2014 use this to aim a swipe, drag or tap with perform_actions, or to tell whether something sits under the keyboard.",
3056
+ "guidance": [
3057
+ "The coordinates are in the same space perform_actions uses: device pixels on Android, points on iOS \u2014 so x + width / 2 and y + height / 2 is the element's centre, ready to press.",
3058
+ "One call per element: the coordinates to aim at, without reading the whole screen.",
3059
+ "An element that is off screen still has a rect \u2014 one outside get_window_rect's size \u2014 so compare the two before tapping; scroll_gesture or scroll_view brings it on."
3060
+ ],
3061
+ "path_params": [
3062
+ {
3063
+ "name": "session_id",
3064
+ "type": "string",
3065
+ "required": true,
3066
+ "example": "0123456789abcdef0123456789abcdef01234567"
3067
+ },
3068
+ {
3069
+ "name": "element_id",
3070
+ "type": "string",
3071
+ "required": true,
3072
+ "description": "Element id from find_element or find_elements.",
3073
+ "example": "00000000-0000-0001-ffff-ffff00000012"
3074
+ }
3075
+ ],
3076
+ "returns": [
3077
+ "value.x",
3078
+ "value.y",
3079
+ "value.width",
3080
+ "value.height"
3081
+ ],
3082
+ "responses": {
3083
+ "200": {
3084
+ "description": "The element's rectangle on screen.",
3085
+ "schema": {
3086
+ "$schema": "RectResponse"
3087
+ }
3088
+ },
3089
+ "404": {
3090
+ "$response": "WebDriverError"
3091
+ }
3092
+ }
3093
+ },
3094
+ {
3095
+ "name": "get_element_attribute",
3096
+ "method": "GET",
3097
+ "path": "/wd/hub/session/{session_id}/element/{element_id}/attribute/{name}",
3098
+ "mode": "read",
3099
+ "entity": "app_session",
3100
+ "intent": "Read one attribute of an element \u2014 use this for its resource id, content description, enabled, checked, selected or displayed state, or on iOS its name, label, value or visibility.",
3101
+ "guidance": [
3102
+ "The attribute names are the platform's own: on Android text, content-desc, resource-id, class, package, bounds, checked, checkable, clickable, enabled, focused, selected, displayed, scrollable, long-clickable, password; on iOS name, label, value, type, enabled, visible, accessible, focused, index, rect.",
3103
+ "Booleans come back as the strings \"true\" and \"false\", not JSON booleans, so compare as text.",
3104
+ "An attribute the element does not carry answers the STRING \"null\" on Android \u2014 not a JSON null and not an error \u2014 so compare as text; measured on an ImageView's resource-id.",
3105
+ "For the element's text use get_element_text and for its position get_element_rect; this is for everything else."
3106
+ ],
3107
+ "path_params": [
3108
+ {
3109
+ "name": "session_id",
3110
+ "type": "string",
3111
+ "required": true,
3112
+ "example": "0123456789abcdef0123456789abcdef01234567"
3113
+ },
3114
+ {
3115
+ "name": "element_id",
3116
+ "type": "string",
3117
+ "required": true,
3118
+ "description": "Element id from find_element or find_elements.",
3119
+ "example": "00000000-0000-0001-ffff-ffff00000012"
3120
+ },
3121
+ {
3122
+ "name": "name",
3123
+ "type": "string",
3124
+ "required": true,
3125
+ "description": "The attribute to read, as named in the UI tree.",
3126
+ "example": "content-desc"
3127
+ }
3128
+ ],
3129
+ "returns": [
3130
+ "value"
3131
+ ],
3132
+ "responses": {
3133
+ "200": {
3134
+ "description": "The attribute's value as a string; an absent one is the string \"null\" on Android.",
3135
+ "schema": {
3136
+ "$schema": "NullableStringResponse"
3137
+ }
3138
+ },
3139
+ "404": {
3140
+ "$response": "WebDriverError"
3141
+ }
3142
+ }
2999
3143
  }
3000
3144
  ],
3001
3145
  "schemas": {
@@ -3458,6 +3602,15 @@
3458
3602
  "description": "Always empty on success."
3459
3603
  }
3460
3604
  }
3605
+ },
3606
+ "NullableStringResponse": {
3607
+ "type": "object",
3608
+ "properties": {
3609
+ "value": {
3610
+ "type": "string",
3611
+ "description": "Null when the attribute is absent."
3612
+ }
3613
+ }
3461
3614
  }
3462
3615
  },
3463
3616
  "responses": {
@@ -3534,6 +3687,9 @@
3534
3687
  "get_active_app_info",
3535
3688
  "read_device_property",
3536
3689
  "find_elements",
3690
+ "get_element_text",
3691
+ "get_element_rect",
3692
+ "get_element_attribute",
3537
3693
  "clear_element",
3538
3694
  "scroll_gesture",
3539
3695
  "dismiss_keyboard_key",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "2.3.0-beta.6",
3
+ "version": "2.3.0-beta.8",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",