@wdio/mcp 3.3.0 → 3.4.1

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.
package/README.md CHANGED
@@ -426,6 +426,7 @@ All session types support `reporting` labels that appear in the BrowserStack Aut
426
426
  | `scroll` | Scroll in a direction (up/down) by specified pixels. Browser-only. |
427
427
  | `execute_script` | Execute arbitrary JavaScript in the browser, or Appium mobile commands on devices |
428
428
  | `switch_tab` | Switch to a different browser tab by handle or 0-based index. Browser-only. |
429
+ | `switch_frame` | Switch into an iframe by CSS/XPath selector, or back to the top-level frame if no selector is given. Browser-only. |
429
430
 
430
431
  ### Element Interaction (Web & Mobile)
431
432
 
package/lib/server.js CHANGED
@@ -46,7 +46,7 @@ var package_default = {
46
46
  type: "git",
47
47
  url: "git://github.com/webdriverio/mcp.git"
48
48
  },
49
- version: "3.2.5",
49
+ version: "3.4.0",
50
50
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
51
51
  main: "./lib/server.js",
52
52
  module: "./lib/server.js",
@@ -173,7 +173,8 @@ init_state();
173
173
  import { z } from "zod";
174
174
  var navigateToolDefinition = {
175
175
  name: "navigate",
176
- description: "Loads a URL in the current tab and waits for the page load event. Resets page state (DOM, JS runtime). Use instead of clicking links to go directly to a known URL.",
176
+ description: "Loads a URL in the current tab and waits for the page load event. Resets page state \u2014 DOM, JS runtime, timers, and frame context are destroyed. Use instead of clicking links when the target URL is known.",
177
+ annotations: { title: "Navigate to URL", destructiveHint: false, idempotentHint: true },
177
178
  inputSchema: {
178
179
  url: z.string().min(1).describe("The URL to navigate to")
179
180
  }
@@ -214,7 +215,8 @@ var coerceBoolean = z2.preprocess((val) => {
214
215
  var defaultTimeout = 3e3;
215
216
  var clickToolDefinition = {
216
217
  name: "click_element",
217
- description: "Waits for an element to exist, scrolls it into view, and calls element.click(). For browser sessions. On iOS, element.click() is sometimes ignored \u2014 use tap_element (which calls element.tap()) instead.",
218
+ description: "Waits for an element, scrolls it into view, and fires element.click(). May trigger navigation, form submission, or modals. Browser sessions only \u2014 on iOS element.click() is silently ignored; use tap_element instead. Default timeout: 3000ms.",
219
+ annotations: { title: "Click Element", destructiveHint: false },
218
220
  inputSchema: {
219
221
  selector: z3.string().describe(`Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class='my-class']" or "button=Exact text with spaces" or "a*=Link containing text")`),
220
222
  scrollToView: coerceBoolean.optional().describe("Whether to scroll the element into view before clicking").default(true),
@@ -247,7 +249,8 @@ import { z as z4 } from "zod";
247
249
  var defaultTimeout2 = 3e3;
248
250
  var setValueToolDefinition = {
249
251
  name: "set_value",
250
- description: "Clears an input or textarea and types the given text. Always replaces existing content. Fails if the element is not found or not interactable within the timeout.",
252
+ description: "Clears an input or textarea then types the given text character by character. Always replaces existing content \u2014 clearValue() runs first. Triggers input, change, and key events which may fire validation or autocomplete. Scrolls into view by default.",
253
+ annotations: { title: "Set Input Value", destructiveHint: false, idempotentHint: true },
251
254
  inputSchema: {
252
255
  selector: z4.string().describe(`Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class='my-class']")`),
253
256
  value: z4.string().describe("Text to enter into the element"),
@@ -281,7 +284,8 @@ init_state();
281
284
  import { z as z5 } from "zod";
282
285
  var scrollToolDefinition = {
283
286
  name: "scroll",
284
- description: "scrolls the page by specified pixels (browser only). For mobile, use the swipe tool.",
287
+ description: "Scrolls the page vertically by a pixel amount. Browser-only \u2014 for mobile scrolling use swipe. Only supports up/down; no horizontal scrolling.",
288
+ annotations: { title: "Scroll Page", destructiveHint: false },
285
289
  inputSchema: {
286
290
  direction: z5.enum(["up", "down"]).describe("Scroll direction"),
287
291
  pixels: z5.number().optional().default(500).describe("Number of pixels to scroll")
@@ -316,7 +320,8 @@ var scrollTool = async ({ direction, pixels = 500 }) => scrollAction(direction,
316
320
  import { z as z6 } from "zod";
317
321
  var setCookieToolDefinition = {
318
322
  name: "set_cookie",
319
- description: "Sets a browser cookie for the active session. The browser must already be on the target domain \u2014 cookies cannot be set cross-domain. Use to inject session tokens or feature flags without going through login flows.",
323
+ description: "Sets a browser cookie on the active session. The browser must already be on the target domain \u2014 cookies cannot be set cross-domain. Use to inject session tokens or feature flags without login flows.",
324
+ annotations: { title: "Set Cookie", destructiveHint: false, idempotentHint: true },
320
325
  inputSchema: {
321
326
  name: z6.string().describe("Cookie name"),
322
327
  value: z6.string().describe("Cookie value"),
@@ -355,7 +360,8 @@ var setCookieTool = async ({
355
360
  };
356
361
  var deleteCookiesToolDefinition = {
357
362
  name: "delete_cookies",
358
- description: "deletes all cookies or a specific cookie by name",
363
+ description: "Deletes all cookies or a single cookie by name from the current browser session. Irreversible \u2014 deleted cookies cannot be recovered.",
364
+ annotations: { title: "Delete Cookies", destructiveHint: true, idempotentHint: true },
359
365
  inputSchema: {
360
366
  name: z6.string().optional().describe("Optional cookie name to delete a specific cookie. If not provided, deletes all cookies")
361
367
  }
@@ -387,7 +393,8 @@ init_state();
387
393
  import { z as z7 } from "zod";
388
394
  var tapElementToolDefinition = {
389
395
  name: "tap_element",
390
- description: "Calls element.tap() on a matched element or taps at absolute screen coordinates. Use on iOS when element.click() (click_element) is ignored \u2014 tap is the native gesture iOS responds to. Mobile-only.",
396
+ description: "Taps a matched element via element.tap() or at absolute screen coordinates (x, y). No scroll-into-view or wait \u2014 element must already be visible on screen. Use instead of click_element on iOS where element.click() is ignored. Provide selector OR both x and y. Mobile-only.",
397
+ annotations: { title: "Tap Element", destructiveHint: false },
391
398
  inputSchema: {
392
399
  selector: z7.string().optional().describe("Element selector (CSS, XPath, accessibility ID, or UiAutomator)"),
393
400
  x: z7.number().optional().describe("X coordinate for screen tap (if no selector provided)"),
@@ -424,7 +431,8 @@ var tapAction = async (args) => {
424
431
  var tapElementTool = async (args) => tapAction(args);
425
432
  var swipeToolDefinition = {
426
433
  name: "swipe",
427
- description: 'Performs a full-screen swipe gesture on mobile. Direction is content movement (e.g. "up" scrolls a list upward, not the finger direction). Use to scroll past visible bounds; for moving a specific element use drag_and_drop. Mobile-only \u2014 use scroll for browsers.',
434
+ description: 'Performs a full-screen swipe gesture. Direction is content movement \u2014 "up" scrolls content upward (finger moves down). For browser scrolling use scroll; for dragging a specific element use drag_and_drop. No error if content cannot scroll further. Mobile-only.',
435
+ annotations: { title: "Swipe Screen", destructiveHint: false },
428
436
  inputSchema: {
429
437
  direction: z7.enum(["up", "down", "left", "right"]).describe("Swipe direction"),
430
438
  duration: z7.number().min(100).max(5e3).optional().describe("Swipe duration in milliseconds (default: 500)"),
@@ -460,7 +468,8 @@ var swipeAction = async (args) => {
460
468
  var swipeTool = async (args) => swipeAction(args);
461
469
  var dragAndDropToolDefinition = {
462
470
  name: "drag_and_drop",
463
- description: "drags an element to another element or coordinates (mobile)",
471
+ description: "Drags an element to another element or to relative x/y offsets. x and y are offsets from the source element, not absolute screen coordinates (unlike tap_element). Provide targetSelector OR both x and y. Mobile-only.",
472
+ annotations: { title: "Drag and Drop", destructiveHint: false },
464
473
  inputSchema: {
465
474
  sourceSelector: z7.string().describe("Source element selector to drag"),
466
475
  targetSelector: z7.string().optional().describe("Target element selector to drop onto"),
@@ -504,7 +513,8 @@ init_state();
504
513
  import { z as z8 } from "zod";
505
514
  var switchContextToolDefinition = {
506
515
  name: "switch_context",
507
- description: "Switches between native and webview automation contexts in a hybrid mobile app. Required before using CSS/XPath selectors inside an embedded webview \u2014 switch to WEBVIEW_* first, then switch back to NATIVE_APP for native elements. List available contexts using get_contexts tool or wdio://session/current/contexts resource.",
516
+ description: "Switches between native and webview automation contexts in a hybrid mobile app. In NATIVE_APP context, use accessibility IDs; in WEBVIEW_* context, use CSS/XPath. Changes persist for all subsequent commands. Accepts context name or 1-based index. Use get_contexts to discover available targets. Mobile-only.",
517
+ annotations: { title: "Switch Context", destructiveHint: false, idempotentHint: true },
508
518
  inputSchema: {
509
519
  context: z8.string().describe(
510
520
  'Context name to switch to (e.g., "NATIVE_APP", "WEBVIEW_com.example.app", or use index from wdio://session/current/contexts resource)'
@@ -542,19 +552,22 @@ init_state();
542
552
  import { z as z9 } from "zod";
543
553
  var hideKeyboardToolDefinition = {
544
554
  name: "hide_keyboard",
545
- description: "Dismisses the software keyboard on mobile. Call after text entry when the keyboard obscures elements you need to interact with next. No-op if already hidden. Mobile-only.",
555
+ description: "Dismisses the on-screen keyboard on mobile. Call after text entry when the keyboard obscures elements. No-op if already hidden. Mobile-only.",
556
+ annotations: { title: "Hide Keyboard", destructiveHint: false, idempotentHint: true },
546
557
  inputSchema: {}
547
558
  };
548
559
  var rotateDeviceToolDefinition = {
549
560
  name: "rotate_device",
550
- description: "Rotates a mobile device to portrait or landscape and waits for the OS rotation to complete. Use to test orientation-dependent layouts. Mobile-only; no effect in browser sessions.",
561
+ description: "Rotates a mobile device to portrait or landscape orientation. Waits for the OS rotation animation to complete. Use to test orientation-dependent layouts. Mobile-only; no effect in browser sessions.",
562
+ annotations: { title: "Rotate Device", destructiveHint: false, idempotentHint: true },
551
563
  inputSchema: {
552
564
  orientation: z9.enum(["PORTRAIT", "LANDSCAPE"]).describe("Device orientation")
553
565
  }
554
566
  };
555
567
  var setGeolocationToolDefinition = {
556
568
  name: "set_geolocation",
557
- description: "Overrides the device GPS coordinates for the session. Affects navigator.geolocation on web and location services on mobile. Location permissions must be granted to the app before calling this.",
569
+ description: "Overrides GPS coordinates for the session. Affects navigator.geolocation in browsers and location services on mobile. Location permissions must already be granted to the app.",
570
+ annotations: { title: "Set Geolocation", destructiveHint: false, idempotentHint: true },
558
571
  inputSchema: {
559
572
  latitude: z9.number().min(-90).max(90).describe("Latitude coordinate"),
560
573
  longitude: z9.number().min(-180).max(180).describe("Longitude coordinate"),
@@ -619,21 +632,8 @@ init_state();
619
632
  import { z as z10 } from "zod";
620
633
  var executeScriptToolDefinition = {
621
634
  name: "execute_script",
622
- description: `Executes JavaScript in browser or mobile commands via Appium.
623
-
624
- **Option B for browser interaction** \u2014 prefer get_visible_elements or click_element/set_value with a selector instead. Use execute_script only when no dedicated tool covers the action (e.g. reading computed values, triggering custom events, scrolling to a position).
625
-
626
- **Browser:** Runs JavaScript in page context. Use 'return' to get values back.
627
- - Example: execute_script({ script: "return document.title" })
628
- - Example: execute_script({ script: "return window.scrollY" })
629
- - Example: execute_script({ script: "arguments[0].click()", args: ["#myButton"] })
630
-
631
- **Mobile (Appium):** Executes mobile-specific commands using 'mobile: <command>' syntax.
632
- - Press key (Android): execute_script({ script: "mobile: pressKey", args: [{ keycode: 4 }] }) // BACK=4, HOME=3
633
- - Activate app: execute_script({ script: "mobile: activateApp", args: [{ appId: "com.example" }] })
634
- - Terminate app: execute_script({ script: "mobile: terminateApp", args: [{ appId: "com.example" }] })
635
- - Deep link: execute_script({ script: "mobile: deepLink", args: [{ url: "myapp://screen", package: "com.example" }] })
636
- - Shell command (Android): execute_script({ script: "mobile: shell", args: [{ command: "dumpsys", args: ["battery"] }] })`,
635
+ description: `Executes arbitrary JavaScript in browser page context or Appium mobile: commands. Can read/modify DOM, trigger events, terminate apps, or run Android shell commands \u2014 use only when no dedicated tool covers the action. Browser: pass JS in script, use 'return' for values, string args matching selectors auto-resolve to elements. Mobile: use 'mobile: <command>' syntax in script with args array (e.g. "mobile: pressKey", "mobile: activateApp"). Prefer click_element/set_value/get_elements for standard interactions.`,
636
+ annotations: { title: "Execute Script", destructiveHint: false },
637
637
  inputSchema: {
638
638
  script: z10.string().describe('JavaScript code (browser) or mobile command string like "mobile: pressKey" (Appium)'),
639
639
  args: z10.array(z10.any()).optional().describe("Arguments to pass to the script. For browser: element selectors or values. For mobile commands: command-specific parameters as objects.")
@@ -1839,7 +1839,8 @@ async function getElements(browser, params) {
1839
1839
  import { encode } from "@toon-format/toon";
1840
1840
  var getElementsToolDefinition = {
1841
1841
  name: "get_elements",
1842
- description: "Get interactable elements on the current page. Use when wdio://session/current/elements does not return the desired elements.",
1842
+ description: "Returns interactable elements on the current page with selectors, text, and bounding boxes. Supports filtering by element type, viewport visibility, and pagination. Use when the wdio://session/current/elements resource does not return desired elements.",
1843
+ annotations: { title: "Get Visible Elements", readOnlyHint: true, idempotentHint: true },
1843
1844
  inputSchema: {
1844
1845
  inViewportOnly: coerceBoolean.optional().default(false).describe("Only return elements visible in the current viewport (default: false)."),
1845
1846
  includeContainers: coerceBoolean.optional().default(false).describe("Include container elements like divs and sections (default: false)"),
@@ -1874,19 +1875,8 @@ import { z as z12 } from "zod";
1874
1875
  var USER_DATA_DIR = join(tmpdir(), "chrome-debug");
1875
1876
  var launchChromeToolDefinition = {
1876
1877
  name: "launch_chrome",
1877
- description: `Prepares and launches Chrome with remote debugging enabled so attach_browser() can connect.
1878
-
1879
- Two modes:
1880
-
1881
- newInstance (default): Opens a Chrome window alongside your existing one using a separate
1882
- profile dir. Your current Chrome session is untouched.
1883
-
1884
- freshSession: Launches Chrome with an empty profile (no cookies, no logins).
1885
-
1886
- Use copyProfileFiles: true to carry over your cookies and logins into the debug session.
1887
- Note: changes made during the session won't sync back to your main profile.
1888
-
1889
- After this tool succeeds, call attach_browser() to connect.`,
1878
+ description: 'Launches Chrome with remote debugging enabled. Wipes and recreates a temporary profile directory on each call. Mode "newInstance" (default) runs alongside existing Chrome; "freshSession" starts with an empty profile. Set copyProfileFiles to copy cookies/logins from your Default profile \u2014 changes do not sync back. After launch, call start_session with attach: true to connect. Spawns a detached Chrome process that persists if the server exits.',
1879
+ annotations: { title: "Launch Chrome", destructiveHint: false },
1890
1880
  inputSchema: {
1891
1881
  port: z12.number().default(9222).describe("Remote debugging port (default: 9222)"),
1892
1882
  mode: z12.enum(["newInstance", "freshSession"]).default("newInstance").describe(
@@ -1990,14 +1980,8 @@ import { z as z13 } from "zod";
1990
1980
  var restoreFunctions = /* @__PURE__ */ new Map();
1991
1981
  var emulateDeviceToolDefinition = {
1992
1982
  name: "emulate_device",
1993
- description: `Emulate a mobile or tablet device in the current browser session (sets viewport, DPR, user-agent, touch events).
1994
-
1995
- Requires a BiDi-enabled session: start_browser({ capabilities: { webSocketUrl: true } })
1996
-
1997
- Usage:
1998
- emulate_device() \u2014 list available device presets
1999
- emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
2000
- emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
1983
+ description: 'Emulates a mobile or tablet device in the current browser session by setting viewport, DPR, user-agent, and touch events. Requires a BiDi-enabled session (start_session with capabilities: { webSocketUrl: true }). Omit device to list available presets. Pass "reset" to restore desktop defaults. Changes persist for all subsequent tool calls until reset or session close. Browser-only.',
1984
+ annotations: { title: "Emulate Device", destructiveHint: false, idempotentHint: true },
2001
1985
  inputSchema: {
2002
1986
  device: z13.string().optional().describe(
2003
1987
  'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
@@ -3152,7 +3136,8 @@ function getAppiumServerConfig(overrides) {
3152
3136
  return {
3153
3137
  hostname: overrides?.hostname || process.env.APPIUM_URL || "127.0.0.1",
3154
3138
  port: overrides?.port || Number(process.env.APPIUM_URL_PORT) || 4723,
3155
- path: overrides?.path || process.env.APPIUM_PATH || "/"
3139
+ path: overrides?.path || process.env.APPIUM_PATH || "/",
3140
+ protocol: overrides?.protocol || process.env.APPIUM_PROTOCOL || "http"
3156
3141
  };
3157
3142
  }
3158
3143
  function buildIOSCapabilities(appPath, options) {
@@ -3235,12 +3220,12 @@ var LocalAppiumProvider = class {
3235
3220
  name = "local-appium";
3236
3221
  getConnectionConfig(options) {
3237
3222
  const appiumConfig = options.appiumConfig;
3238
- const config = getAppiumServerConfig({
3223
+ return getAppiumServerConfig({
3239
3224
  hostname: appiumConfig?.host,
3240
3225
  port: appiumConfig?.port,
3241
- path: appiumConfig?.path
3226
+ path: appiumConfig?.path,
3227
+ protocol: appiumConfig?.protocol
3242
3228
  });
3243
- return { protocol: "http", ...config };
3244
3229
  }
3245
3230
  buildCapabilities(options) {
3246
3231
  const platform2 = options.platform;
@@ -3501,7 +3486,8 @@ var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
3501
3486
  var automationEnum = z14.enum(["XCUITest", "UiAutomator2"]);
3502
3487
  var startSessionToolDefinition = {
3503
3488
  name: "start_session",
3504
- description: 'Starts a new browser or mobile automation session. Only one active session at a time \u2014 starting a new one closes the existing one. Use platform "browser" with a browser name, or "ios"/"android" with a deviceName. Use attach mode to connect to an already-running Chrome instance via CDP.',
3489
+ description: 'Starts a browser or mobile automation session. Only one active session at a time \u2014 starting a new one closes the existing session first. Use platform "browser" with a browser name, or "ios"/"android" with deviceName. Set attach: true to connect to a running Chrome via CDP instead of launching a new browser.',
3490
+ annotations: { title: "Start Session", destructiveHint: false },
3505
3491
  inputSchema: {
3506
3492
  provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
3507
3493
  platform: platformEnum.describe("Session platform type"),
@@ -3538,7 +3524,8 @@ var startSessionToolDefinition = {
3538
3524
  appiumConfig: z14.object({
3539
3525
  host: z14.string().optional(),
3540
3526
  port: z14.number().optional(),
3541
- path: z14.string().optional()
3527
+ path: z14.string().optional(),
3528
+ protocol: z14.string().optional()
3542
3529
  }).optional().describe("Appium server connection (local provider only)"),
3543
3530
  browserstackLocal: z14.union([coerceBoolean, z14.literal("external")]).optional().default(false).describe('Enable BrowserStack Local tunnel routing (BrowserStack only, default: false). true = auto-start tunnel before session and stop on close. "external" = tunnel already running externally, set local: true in capabilities only.'),
3544
3531
  navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
@@ -3547,7 +3534,8 @@ var startSessionToolDefinition = {
3547
3534
  };
3548
3535
  var closeSessionToolDefinition = {
3549
3536
  name: "close_session",
3550
- description: "Closes or detaches from the current session. Detach disconnects without terminating the process, preserving app state on the Appium server. Sessions started with noReset: true auto-detach by default.",
3537
+ description: "Closes the current session or detaches without terminating. Detach preserves app state on the Appium server \u2014 sessions with noReset: true auto-detach by default. Closing a browser attach session terminates chromedriver but the Chrome process spawned by launch_chrome remains running.",
3538
+ annotations: { title: "Close Session", destructiveHint: true },
3551
3539
  inputSchema: {
3552
3540
  detach: coerceBoolean.optional().describe("If true, disconnect without terminating (preserves app state). Default: false")
3553
3541
  }
@@ -3797,7 +3785,8 @@ init_state();
3797
3785
  import { z as z15 } from "zod";
3798
3786
  var switchTabToolDefinition = {
3799
3787
  name: "switch_tab",
3800
- description: "Focuses a browser tab by window handle or 0-based index. All subsequent tool calls operate on the newly active tab. Get handles from get_tabs tool or wdio://session/current/tabs resource. Browser-only \u2014 use switch_context for mobile webviews.",
3788
+ description: "Focuses a browser tab by window handle or 0-based index. All subsequent tool calls operate on the active tab. Provide handle OR index \u2014 use get_tabs to find them. Browser-only; use switch_context for mobile webviews.",
3789
+ annotations: { title: "Switch Browser Tab", destructiveHint: false, idempotentHint: true },
3801
3790
  inputSchema: {
3802
3791
  handle: z15.string().optional().describe("Window handle to switch to"),
3803
3792
  index: z15.number().int().min(0).optional().describe("0-based tab index to switch to")
@@ -3823,9 +3812,40 @@ var switchTabTool = async ({ handle, index }) => {
3823
3812
  }
3824
3813
  };
3825
3814
 
3815
+ // src/tools/switch-frame.tool.ts
3816
+ init_state();
3817
+ import { z as z16 } from "zod";
3818
+ var switchFrameToolDefinition = {
3819
+ name: "switch_frame",
3820
+ description: "Switches WebDriver frame context into an iframe by CSS/XPath selector, or back to top-level if selector is omitted. Changes persist \u2014 all subsequent click_element, set_value, get_elements calls operate within the switched frame until you switch back. Waits up to 5s for the iframe. Browser-only.",
3821
+ annotations: { title: "Switch Frame", destructiveHint: false, idempotentHint: true },
3822
+ inputSchema: {
3823
+ selector: z16.string().optional().describe(
3824
+ "CSS/XPath selector for the iframe element. Omit to switch back to the top-level frame."
3825
+ )
3826
+ }
3827
+ };
3828
+ var switchFrameTool = async ({
3829
+ selector
3830
+ }) => {
3831
+ try {
3832
+ const browser = getBrowser();
3833
+ if (!selector) {
3834
+ await browser.switchFrame(null);
3835
+ return { content: [{ type: "text", text: "Switched back to top-level frame" }] };
3836
+ }
3837
+ const iframe = await browser.$(selector);
3838
+ await iframe.waitForExist({ timeout: 5e3 });
3839
+ await browser.switchFrame(iframe);
3840
+ return { content: [{ type: "text", text: `Switched to iframe: ${selector}` }] };
3841
+ } catch (e) {
3842
+ return { isError: true, content: [{ type: "text", text: `Error switching frame: ${e}` }] };
3843
+ }
3844
+ };
3845
+
3826
3846
  // src/tools/browserstack.tool.ts
3827
3847
  import { existsSync as existsSync2, createReadStream } from "fs";
3828
- import { z as z16 } from "zod";
3848
+ import { z as z17 } from "zod";
3829
3849
  var BS_API = "https://api-cloud.browserstack.com";
3830
3850
  function getAuth() {
3831
3851
  const user = process.env.BROWSERSTACK_USERNAME;
@@ -3843,10 +3863,11 @@ function formatAppList(apps) {
3843
3863
  var listAppsToolDefinition = {
3844
3864
  name: "list_apps",
3845
3865
  description: "List apps uploaded to BrowserStack App Automate. Reads BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY from environment.",
3866
+ annotations: { title: "List BrowserStack Apps", readOnlyHint: true, idempotentHint: true },
3846
3867
  inputSchema: {
3847
- sortBy: z16.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
3868
+ sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
3848
3869
  organizationWide: coerceBoolean.optional().default(false).describe("List apps uploaded by all users in the organization (uses recent_group_apps endpoint). Defaults to false (own uploads only)."),
3849
- limit: z16.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
3870
+ limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
3850
3871
  }
3851
3872
  };
3852
3873
  var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, limit = 20 }) => {
@@ -3875,9 +3896,10 @@ var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, li
3875
3896
  var uploadAppToolDefinition = {
3876
3897
  name: "upload_app",
3877
3898
  description: "Upload a local .apk or .ipa to BrowserStack App Automate. Returns a bs:// URL for use in start_session.",
3899
+ annotations: { title: "Upload App to BrowserStack", destructiveHint: false },
3878
3900
  inputSchema: {
3879
- path: z16.string().describe("Absolute path to the .apk or .ipa file"),
3880
- customId: z16.string().optional().describe("Optional custom ID for the app (used to reference it later)")
3901
+ path: z17.string().describe("Absolute path to the .apk or .ipa file"),
3902
+ customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)")
3881
3903
  }
3882
3904
  };
3883
3905
  var uploadAppTool = async ({ path, customId }) => {
@@ -3919,6 +3941,7 @@ Use this URL as the "app" parameter in start_session with provider: "browserstac
3919
3941
  var screenshotToolDefinition = {
3920
3942
  name: "get_screenshot",
3921
3943
  description: "Takes a screenshot of the current page or screen and returns a base64-encoded image, resized and compressed for model context limits.",
3944
+ annotations: { title: "Get Screenshot", readOnlyHint: true, idempotentHint: true },
3922
3945
  inputSchema: {}
3923
3946
  };
3924
3947
  var screenshotTool = async () => {
@@ -3930,14 +3953,15 @@ var screenshotTool = async () => {
3930
3953
  };
3931
3954
 
3932
3955
  // src/tools/accessibility.tool.ts
3933
- import { z as z17 } from "zod";
3956
+ import { z as z18 } from "zod";
3934
3957
  var accessibilityToolDefinition = {
3935
3958
  name: "get_accessibility_tree",
3936
3959
  description: "Returns the page accessibility tree with roles, names, and selectors. Browser-only. Supports filtering by ARIA roles and pagination via limit/offset.",
3960
+ annotations: { title: "Get Accessibility Tree", readOnlyHint: true, idempotentHint: true },
3937
3961
  inputSchema: {
3938
- limit: z17.number().optional().default(0).describe("Maximum number of nodes to return (0 = no limit)"),
3939
- offset: z17.number().optional().default(0).describe("Number of nodes to skip for pagination"),
3940
- roles: z17.array(z17.string()).optional().describe('Filter by ARIA roles, e.g. ["button", "link", "heading"]')
3962
+ limit: z18.number().optional().default(0).describe("Maximum number of nodes to return (0 = no limit)"),
3963
+ offset: z18.number().optional().default(0).describe("Number of nodes to skip for pagination"),
3964
+ roles: z18.array(z18.string()).optional().describe('Filter by ARIA roles, e.g. ["button", "link", "heading"]')
3941
3965
  }
3942
3966
  };
3943
3967
  var accessibilityTool = async ({ limit = 0, offset = 0, roles }) => {
@@ -3952,6 +3976,7 @@ var accessibilityTool = async ({ limit = 0, offset = 0, roles }) => {
3952
3976
  var getTabsToolDefinition = {
3953
3977
  name: "get_tabs",
3954
3978
  description: "Lists all browser tabs with handle, title, URL, and which is active. Use before switch_tab to find the target handle or index. Browser-only.",
3979
+ annotations: { title: "Get Browser Tabs", readOnlyHint: true, idempotentHint: true },
3955
3980
  inputSchema: {}
3956
3981
  };
3957
3982
  var getTabsTool = async () => {
@@ -3966,6 +3991,7 @@ var getTabsTool = async () => {
3966
3991
  var getContextsToolDefinition = {
3967
3992
  name: "get_contexts",
3968
3993
  description: "Returns available automation contexts and the currently active one. Use before switch_context to discover NATIVE_APP and WEBVIEW_* targets. Mobile-only.",
3994
+ annotations: { title: "Get Automation Contexts", readOnlyHint: true, idempotentHint: true },
3969
3995
  inputSchema: {}
3970
3996
  };
3971
3997
  var getContextsTool = async () => {
@@ -3984,12 +4010,13 @@ var getContextsTool = async () => {
3984
4010
  };
3985
4011
 
3986
4012
  // src/tools/app-state.tool.ts
3987
- import { z as z18 } from "zod";
4013
+ import { z as z19 } from "zod";
3988
4014
  var appStateToolDefinition = {
3989
4015
  name: "get_app_state",
3990
4016
  description: "Returns the current state of a mobile app: not installed, not running, background, or foreground. Mobile-only.",
4017
+ annotations: { title: "Get App State", readOnlyHint: true, idempotentHint: true },
3991
4018
  inputSchema: {
3992
- bundleId: z18.string().describe('App bundle ID (iOS) or package name (Android), e.g. "com.example.app"')
4019
+ bundleId: z19.string().describe('App bundle ID (iOS) or package name (Android), e.g. "com.example.app"')
3993
4020
  }
3994
4021
  };
3995
4022
  var appStateTool = async ({ bundleId }) => {
@@ -4001,12 +4028,13 @@ var appStateTool = async ({ bundleId }) => {
4001
4028
  };
4002
4029
 
4003
4030
  // src/tools/get-cookies.tool.ts
4004
- import { z as z19 } from "zod";
4031
+ import { z as z20 } from "zod";
4005
4032
  var getCookiesToolDefinition = {
4006
4033
  name: "get_cookies",
4007
4034
  description: "Returns all cookies for the current session, or a single cookie by name. Use to verify auth state, session tokens, or feature flags after login flows.",
4035
+ annotations: { title: "Get Cookies", readOnlyHint: true, idempotentHint: true },
4008
4036
  inputSchema: {
4009
- name: z19.string().optional().describe("Cookie name to retrieve a specific cookie. If omitted, returns all cookies.")
4037
+ name: z20.string().optional().describe("Cookie name to retrieve a specific cookie. If omitted, returns all cookies.")
4010
4038
  }
4011
4039
  };
4012
4040
  var getCookiesTool = async ({ name }) => {
@@ -4038,7 +4066,8 @@ function createServer() {
4038
4066
  });
4039
4067
  const registerTool = (definition, callback) => server.registerTool(definition.name, {
4040
4068
  description: definition.description,
4041
- inputSchema: definition.inputSchema
4069
+ inputSchema: definition.inputSchema,
4070
+ ...definition.annotations && { annotations: definition.annotations }
4042
4071
  }, callback);
4043
4072
  const registerResource = (definition) => {
4044
4073
  if ("uri" in definition) {
@@ -4063,6 +4092,7 @@ function createServer() {
4063
4092
  registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
4064
4093
  registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
4065
4094
  registerTool(switchTabToolDefinition, switchTabTool);
4095
+ registerTool(switchFrameToolDefinition, switchFrameTool);
4066
4096
  registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
4067
4097
  registerTool(clickToolDefinition, withRecording("click_element", clickTool));
4068
4098
  registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));