@wdio/mcp 3.2.5 → 3.4.0

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/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.4",
49
+ version: "3.3.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",
@@ -79,7 +79,9 @@ var package_default = {
79
79
  postbundle: "npm pack",
80
80
  lint: "eslint src/ tests/ --fix && tsc --noEmit",
81
81
  start: "node lib/server.js",
82
+ "start:http": "node lib/server.js --http --allowedOrigins http://localhost:8080",
82
83
  dev: "tsx --watch src/server.ts",
84
+ "dev:http": "tsx --watch src/server.ts --http --allowedOrigins http://localhost:8080",
83
85
  prepare: "husky",
84
86
  test: "vitest run"
85
87
  },
@@ -120,15 +122,58 @@ var package_default = {
120
122
  };
121
123
 
122
124
  // src/server.ts
125
+ import http from "http";
123
126
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
124
127
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
128
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
129
+
130
+ // src/utils/parse-args.ts
131
+ function parseList(argv, flag) {
132
+ const idx = argv.indexOf(flag);
133
+ if (idx === -1) return null;
134
+ const raw = argv[idx + 1];
135
+ if (!raw || raw.startsWith("--")) {
136
+ throw new Error(`${flag} requires a comma-separated list`);
137
+ }
138
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
139
+ }
140
+ function parseArgs(argv) {
141
+ const http2 = argv.includes("--http");
142
+ const portIdx = argv.indexOf("--port");
143
+ let port = 3e3;
144
+ if (portIdx !== -1) {
145
+ const raw = argv[portIdx + 1];
146
+ const parsed = Number(raw);
147
+ if (!raw || !Number.isInteger(parsed) || parsed <= 0) {
148
+ throw new Error("--port must be a valid number");
149
+ }
150
+ port = parsed;
151
+ }
152
+ const allowedHosts = parseList(argv, "--allowedHosts") ?? ["localhost", "127.0.0.1", "::1"];
153
+ const allowedOrigins = parseList(argv, "--allowedOrigins") ?? [];
154
+ return { http: http2, port, allowedHosts, allowedOrigins };
155
+ }
156
+
157
+ // src/utils/http-helpers.ts
158
+ function extractHost(header) {
159
+ if (header.startsWith("[")) {
160
+ const end = header.indexOf("]");
161
+ return end === -1 ? header : header.slice(1, end);
162
+ }
163
+ return header.split(":")[0];
164
+ }
165
+ function sendJsonRpcError(res, httpStatus, code, message) {
166
+ const body = JSON.stringify({ jsonrpc: "2.0", id: null, error: { code, message } });
167
+ res.writeHead(httpStatus, { "Content-Type": "application/json" });
168
+ res.end(body);
169
+ }
125
170
 
126
171
  // src/tools/navigate.tool.ts
127
172
  init_state();
128
173
  import { z } from "zod";
129
174
  var navigateToolDefinition = {
130
175
  name: "navigate",
131
- description: "navigates to a URL",
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.",
132
177
  inputSchema: {
133
178
  url: z.string().min(1).describe("The URL to navigate to")
134
179
  }
@@ -169,7 +214,7 @@ var coerceBoolean = z2.preprocess((val) => {
169
214
  var defaultTimeout = 3e3;
170
215
  var clickToolDefinition = {
171
216
  name: "click_element",
172
- description: "clicks an 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.",
173
218
  inputSchema: {
174
219
  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")`),
175
220
  scrollToView: coerceBoolean.optional().describe("Whether to scroll the element into view before clicking").default(true),
@@ -202,7 +247,7 @@ import { z as z4 } from "zod";
202
247
  var defaultTimeout2 = 3e3;
203
248
  var setValueToolDefinition = {
204
249
  name: "set_value",
205
- description: "set value to an element, aka typing",
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.",
206
251
  inputSchema: {
207
252
  selector: z4.string().describe(`Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class='my-class']")`),
208
253
  value: z4.string().describe("Text to enter into the element"),
@@ -271,7 +316,7 @@ var scrollTool = async ({ direction, pixels = 500 }) => scrollAction(direction,
271
316
  import { z as z6 } from "zod";
272
317
  var setCookieToolDefinition = {
273
318
  name: "set_cookie",
274
- description: "sets a cookie with specified name, value, and optional attributes",
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.",
275
320
  inputSchema: {
276
321
  name: z6.string().describe("Cookie name"),
277
322
  value: z6.string().describe("Cookie value"),
@@ -342,7 +387,7 @@ init_state();
342
387
  import { z as z7 } from "zod";
343
388
  var tapElementToolDefinition = {
344
389
  name: "tap_element",
345
- description: "taps an element by selector or screen coordinates (mobile)",
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.",
346
391
  inputSchema: {
347
392
  selector: z7.string().optional().describe("Element selector (CSS, XPath, accessibility ID, or UiAutomator)"),
348
393
  x: z7.number().optional().describe("X coordinate for screen tap (if no selector provided)"),
@@ -379,7 +424,7 @@ var tapAction = async (args) => {
379
424
  var tapElementTool = async (args) => tapAction(args);
380
425
  var swipeToolDefinition = {
381
426
  name: "swipe",
382
- description: "performs a swipe gesture in specified direction (mobile)",
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.',
383
428
  inputSchema: {
384
429
  direction: z7.enum(["up", "down", "left", "right"]).describe("Swipe direction"),
385
430
  duration: z7.number().min(100).max(5e3).optional().describe("Swipe duration in milliseconds (default: 500)"),
@@ -459,7 +504,7 @@ init_state();
459
504
  import { z as z8 } from "zod";
460
505
  var switchContextToolDefinition = {
461
506
  name: "switch_context",
462
- description: "switches between native and webview contexts",
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.",
463
508
  inputSchema: {
464
509
  context: z8.string().describe(
465
510
  'Context name to switch to (e.g., "NATIVE_APP", "WEBVIEW_com.example.app", or use index from wdio://session/current/contexts resource)'
@@ -497,19 +542,19 @@ init_state();
497
542
  import { z as z9 } from "zod";
498
543
  var hideKeyboardToolDefinition = {
499
544
  name: "hide_keyboard",
500
- description: "hides the on-screen 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.",
501
546
  inputSchema: {}
502
547
  };
503
548
  var rotateDeviceToolDefinition = {
504
549
  name: "rotate_device",
505
- description: "rotates device to portrait or landscape orientation",
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.",
506
551
  inputSchema: {
507
552
  orientation: z9.enum(["PORTRAIT", "LANDSCAPE"]).describe("Device orientation")
508
553
  }
509
554
  };
510
555
  var setGeolocationToolDefinition = {
511
556
  name: "set_geolocation",
512
- description: "sets device geolocation (latitude, longitude, altitude)",
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.",
513
558
  inputSchema: {
514
559
  latitude: z9.number().min(-90).max(90).describe("Latitude coordinate"),
515
560
  longitude: z9.number().min(-180).max(180).describe("Longitude coordinate"),
@@ -2855,7 +2900,7 @@ async function readCookies(name) {
2855
2900
  const cookies = await browser.getCookies();
2856
2901
  return { mimeType: "application/json", text: JSON.stringify(cookies) };
2857
2902
  } catch (e) {
2858
- return { mimeType: "application/json", text: JSON.stringify({ error: String(e) }) };
2903
+ return { mimeType: "text/plain", text: `Error: ${e}` };
2859
2904
  }
2860
2905
  }
2861
2906
  var cookiesResource = {
@@ -2874,6 +2919,12 @@ import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk
2874
2919
  async function readAppState(bundleId) {
2875
2920
  try {
2876
2921
  const browser = getBrowser();
2922
+ if (!browser.isMobile) {
2923
+ return {
2924
+ mimeType: "text/plain",
2925
+ text: "Error: get_app_state is mobile-only. Use it on an iOS or Android session."
2926
+ };
2927
+ }
2877
2928
  const appIdentifier = browser.isAndroid ? { appId: bundleId } : { bundleId };
2878
2929
  const state2 = await browser.execute("mobile: queryAppState", appIdentifier);
2879
2930
  const stateMap = {
@@ -3101,7 +3152,8 @@ function getAppiumServerConfig(overrides) {
3101
3152
  return {
3102
3153
  hostname: overrides?.hostname || process.env.APPIUM_URL || "127.0.0.1",
3103
3154
  port: overrides?.port || Number(process.env.APPIUM_URL_PORT) || 4723,
3104
- path: overrides?.path || process.env.APPIUM_PATH || "/"
3155
+ path: overrides?.path || process.env.APPIUM_PATH || "/",
3156
+ protocol: overrides?.protocol || process.env.APPIUM_PROTOCOL || "http"
3105
3157
  };
3106
3158
  }
3107
3159
  function buildIOSCapabilities(appPath, options) {
@@ -3184,12 +3236,12 @@ var LocalAppiumProvider = class {
3184
3236
  name = "local-appium";
3185
3237
  getConnectionConfig(options) {
3186
3238
  const appiumConfig = options.appiumConfig;
3187
- const config = getAppiumServerConfig({
3239
+ return getAppiumServerConfig({
3188
3240
  hostname: appiumConfig?.host,
3189
3241
  port: appiumConfig?.port,
3190
- path: appiumConfig?.path
3242
+ path: appiumConfig?.path,
3243
+ protocol: appiumConfig?.protocol
3191
3244
  });
3192
- return { protocol: "http", ...config };
3193
3245
  }
3194
3246
  buildCapabilities(options) {
3195
3247
  const platform2 = options.platform;
@@ -3450,7 +3502,7 @@ var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
3450
3502
  var automationEnum = z14.enum(["XCUITest", "UiAutomator2"]);
3451
3503
  var startSessionToolDefinition = {
3452
3504
  name: "start_session",
3453
- description: "Starts a browser or mobile app session. For local browser, use browser platform. For mobile apps, use ios or android platform. Use attach mode to connect to an existing Chrome instance.",
3505
+ 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.',
3454
3506
  inputSchema: {
3455
3507
  provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
3456
3508
  platform: platformEnum.describe("Session platform type"),
@@ -3487,7 +3539,8 @@ var startSessionToolDefinition = {
3487
3539
  appiumConfig: z14.object({
3488
3540
  host: z14.string().optional(),
3489
3541
  port: z14.number().optional(),
3490
- path: z14.string().optional()
3542
+ path: z14.string().optional(),
3543
+ protocol: z14.string().optional()
3491
3544
  }).optional().describe("Appium server connection (local provider only)"),
3492
3545
  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.'),
3493
3546
  navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
@@ -3496,7 +3549,7 @@ var startSessionToolDefinition = {
3496
3549
  };
3497
3550
  var closeSessionToolDefinition = {
3498
3551
  name: "close_session",
3499
- description: "Closes or detaches from the current browser or app session",
3552
+ 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.",
3500
3553
  inputSchema: {
3501
3554
  detach: coerceBoolean.optional().describe("If true, disconnect without terminating (preserves app state). Default: false")
3502
3555
  }
@@ -3746,7 +3799,7 @@ init_state();
3746
3799
  import { z as z15 } from "zod";
3747
3800
  var switchTabToolDefinition = {
3748
3801
  name: "switch_tab",
3749
- description: "switches to a browser tab by handle or index",
3802
+ 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.",
3750
3803
  inputSchema: {
3751
3804
  handle: z15.string().optional().describe("Window handle to switch to"),
3752
3805
  index: z15.number().int().min(0).optional().describe("0-based tab index to switch to")
@@ -3772,9 +3825,39 @@ var switchTabTool = async ({ handle, index }) => {
3772
3825
  }
3773
3826
  };
3774
3827
 
3828
+ // src/tools/switch-frame.tool.ts
3829
+ init_state();
3830
+ import { z as z16 } from "zod";
3831
+ var switchFrameToolDefinition = {
3832
+ name: "switch_frame",
3833
+ description: "Switches into an iframe by CSS or XPath selector, or back to the top-level frame if no selector is given. Required before interacting with elements inside iframes \u2014 click, set_value, and get_elements only see the current frame context. Browser-only.",
3834
+ inputSchema: {
3835
+ selector: z16.string().optional().describe(
3836
+ "CSS/XPath selector for the iframe element. Omit to switch back to the top-level frame."
3837
+ )
3838
+ }
3839
+ };
3840
+ var switchFrameTool = async ({
3841
+ selector
3842
+ }) => {
3843
+ try {
3844
+ const browser = getBrowser();
3845
+ if (!selector) {
3846
+ await browser.switchFrame(null);
3847
+ return { content: [{ type: "text", text: "Switched back to top-level frame" }] };
3848
+ }
3849
+ const iframe = await browser.$(selector);
3850
+ await iframe.waitForExist({ timeout: 5e3 });
3851
+ await browser.switchFrame(iframe);
3852
+ return { content: [{ type: "text", text: `Switched to iframe: ${selector}` }] };
3853
+ } catch (e) {
3854
+ return { isError: true, content: [{ type: "text", text: `Error switching frame: ${e}` }] };
3855
+ }
3856
+ };
3857
+
3775
3858
  // src/tools/browserstack.tool.ts
3776
3859
  import { existsSync as existsSync2, createReadStream } from "fs";
3777
- import { z as z16 } from "zod";
3860
+ import { z as z17 } from "zod";
3778
3861
  var BS_API = "https://api-cloud.browserstack.com";
3779
3862
  function getAuth() {
3780
3863
  const user = process.env.BROWSERSTACK_USERNAME;
@@ -3793,9 +3876,9 @@ var listAppsToolDefinition = {
3793
3876
  name: "list_apps",
3794
3877
  description: "List apps uploaded to BrowserStack App Automate. Reads BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY from environment.",
3795
3878
  inputSchema: {
3796
- sortBy: z16.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
3879
+ sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
3797
3880
  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)."),
3798
- limit: z16.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
3881
+ limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
3799
3882
  }
3800
3883
  };
3801
3884
  var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, limit = 20 }) => {
@@ -3825,8 +3908,8 @@ var uploadAppToolDefinition = {
3825
3908
  name: "upload_app",
3826
3909
  description: "Upload a local .apk or .ipa to BrowserStack App Automate. Returns a bs:// URL for use in start_session.",
3827
3910
  inputSchema: {
3828
- path: z16.string().describe("Absolute path to the .apk or .ipa file"),
3829
- customId: z16.string().optional().describe("Optional custom ID for the app (used to reference it later)")
3911
+ path: z17.string().describe("Absolute path to the .apk or .ipa file"),
3912
+ customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)")
3830
3913
  }
3831
3914
  };
3832
3915
  var uploadAppTool = async ({ path, customId }) => {
@@ -3864,87 +3947,273 @@ Use this URL as the "app" parameter in start_session with provider: "browserstac
3864
3947
  }
3865
3948
  };
3866
3949
 
3950
+ // src/tools/screenshot.tool.ts
3951
+ var screenshotToolDefinition = {
3952
+ name: "get_screenshot",
3953
+ description: "Takes a screenshot of the current page or screen and returns a base64-encoded image, resized and compressed for model context limits.",
3954
+ inputSchema: {}
3955
+ };
3956
+ var screenshotTool = async () => {
3957
+ const result = await readScreenshot();
3958
+ if (result.mimeType === "text/plain") {
3959
+ return { isError: true, content: [{ type: "text", text: Buffer.from(result.blob, "base64").toString("utf-8") }] };
3960
+ }
3961
+ return { content: [{ type: "image", data: result.blob, mimeType: result.mimeType }] };
3962
+ };
3963
+
3964
+ // src/tools/accessibility.tool.ts
3965
+ import { z as z18 } from "zod";
3966
+ var accessibilityToolDefinition = {
3967
+ name: "get_accessibility_tree",
3968
+ description: "Returns the page accessibility tree with roles, names, and selectors. Browser-only. Supports filtering by ARIA roles and pagination via limit/offset.",
3969
+ inputSchema: {
3970
+ limit: z18.number().optional().default(0).describe("Maximum number of nodes to return (0 = no limit)"),
3971
+ offset: z18.number().optional().default(0).describe("Number of nodes to skip for pagination"),
3972
+ roles: z18.array(z18.string()).optional().describe('Filter by ARIA roles, e.g. ["button", "link", "heading"]')
3973
+ }
3974
+ };
3975
+ var accessibilityTool = async ({ limit = 0, offset = 0, roles }) => {
3976
+ const result = await readAccessibilityTree({ limit, offset, roles });
3977
+ if (result.text.startsWith("Error")) {
3978
+ return { isError: true, content: [{ type: "text", text: result.text }] };
3979
+ }
3980
+ return { content: [{ type: "text", text: result.text }] };
3981
+ };
3982
+
3983
+ // src/tools/get-tabs.tool.ts
3984
+ var getTabsToolDefinition = {
3985
+ name: "get_tabs",
3986
+ 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.",
3987
+ inputSchema: {}
3988
+ };
3989
+ var getTabsTool = async () => {
3990
+ const result = await readTabs();
3991
+ if (result.text.startsWith("Error")) {
3992
+ return { isError: true, content: [{ type: "text", text: result.text }] };
3993
+ }
3994
+ return { content: [{ type: "text", text: result.text }] };
3995
+ };
3996
+
3997
+ // src/tools/get-contexts.tool.ts
3998
+ var getContextsToolDefinition = {
3999
+ name: "get_contexts",
4000
+ description: "Returns available automation contexts and the currently active one. Use before switch_context to discover NATIVE_APP and WEBVIEW_* targets. Mobile-only.",
4001
+ inputSchema: {}
4002
+ };
4003
+ var getContextsTool = async () => {
4004
+ const [contexts, current] = await Promise.all([readContexts(), readCurrentContext()]);
4005
+ if (contexts.mimeType === "text/plain" && contexts.text.startsWith("Error")) {
4006
+ return { isError: true, content: [{ type: "text", text: contexts.text }] };
4007
+ }
4008
+ if (current.mimeType === "text/plain" && current.text.startsWith("Error")) {
4009
+ return { isError: true, content: [{ type: "text", text: current.text }] };
4010
+ }
4011
+ const combined = {
4012
+ contexts: JSON.parse(contexts.text),
4013
+ currentContext: JSON.parse(current.text)
4014
+ };
4015
+ return { content: [{ type: "text", text: JSON.stringify(combined) }] };
4016
+ };
4017
+
4018
+ // src/tools/app-state.tool.ts
4019
+ import { z as z19 } from "zod";
4020
+ var appStateToolDefinition = {
4021
+ name: "get_app_state",
4022
+ description: "Returns the current state of a mobile app: not installed, not running, background, or foreground. Mobile-only.",
4023
+ inputSchema: {
4024
+ bundleId: z19.string().describe('App bundle ID (iOS) or package name (Android), e.g. "com.example.app"')
4025
+ }
4026
+ };
4027
+ var appStateTool = async ({ bundleId }) => {
4028
+ const result = await readAppState(bundleId);
4029
+ if (result.text.startsWith("Error")) {
4030
+ return { isError: true, content: [{ type: "text", text: result.text }] };
4031
+ }
4032
+ return { content: [{ type: "text", text: result.text }] };
4033
+ };
4034
+
4035
+ // src/tools/get-cookies.tool.ts
4036
+ import { z as z20 } from "zod";
4037
+ var getCookiesToolDefinition = {
4038
+ name: "get_cookies",
4039
+ 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.",
4040
+ inputSchema: {
4041
+ name: z20.string().optional().describe("Cookie name to retrieve a specific cookie. If omitted, returns all cookies.")
4042
+ }
4043
+ };
4044
+ var getCookiesTool = async ({ name }) => {
4045
+ const result = await readCookies(name);
4046
+ if (result.mimeType === "text/plain" && result.text.startsWith("Error")) {
4047
+ return { isError: true, content: [{ type: "text", text: result.text }] };
4048
+ }
4049
+ return { content: [{ type: "text", text: result.text }] };
4050
+ };
4051
+
3867
4052
  // src/server.ts
3868
4053
  console.log = (...args) => console.error("[LOG]", ...args);
3869
4054
  console.info = (...args) => console.error("[INFO]", ...args);
3870
4055
  console.warn = (...args) => console.error("[WARN]", ...args);
3871
4056
  console.debug = (...args) => console.error("[DEBUG]", ...args);
3872
- var server = new McpServer({
3873
- title: "WebdriverIO MCP Server",
3874
- name: package_default.name,
3875
- version: package_default.version,
3876
- description: package_default.description,
3877
- websiteUrl: "https://github.com/webdriverio/mcp"
3878
- }, {
3879
- instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome, Firefox, Edge, and Safari browser control plus iOS/Android native app testing via Appium.",
3880
- capabilities: {
3881
- tools: {},
3882
- resources: {}
3883
- }
3884
- });
3885
- var registerTool = (definition, callback) => server.registerTool(definition.name, {
3886
- description: definition.description,
3887
- inputSchema: definition.inputSchema
3888
- }, callback);
3889
- var registerResource = (definition) => {
3890
- if ("uri" in definition) {
3891
- server.registerResource(
3892
- definition.name,
3893
- definition.uri,
3894
- { description: definition.description },
3895
- definition.handler
3896
- );
4057
+ function createServer() {
4058
+ const server = new McpServer({
4059
+ title: "WebdriverIO MCP Server",
4060
+ name: package_default.name,
4061
+ version: package_default.version,
4062
+ description: package_default.description,
4063
+ websiteUrl: "https://github.com/webdriverio/mcp"
4064
+ }, {
4065
+ instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome, Firefox, Edge, and Safari browser control plus iOS/Android native app testing via Appium.",
4066
+ capabilities: {
4067
+ tools: {},
4068
+ resources: {}
4069
+ }
4070
+ });
4071
+ const registerTool = (definition, callback) => server.registerTool(definition.name, {
4072
+ description: definition.description,
4073
+ inputSchema: definition.inputSchema
4074
+ }, callback);
4075
+ const registerResource = (definition) => {
4076
+ if ("uri" in definition) {
4077
+ server.registerResource(
4078
+ definition.name,
4079
+ definition.uri,
4080
+ { description: definition.description },
4081
+ definition.handler
4082
+ );
4083
+ } else {
4084
+ server.registerResource(
4085
+ definition.name,
4086
+ definition.template,
4087
+ { description: definition.description },
4088
+ definition.handler
4089
+ );
4090
+ }
4091
+ };
4092
+ registerTool(startSessionToolDefinition, withRecording("start_session", startSessionTool));
4093
+ registerTool(closeSessionToolDefinition, closeSessionTool);
4094
+ registerTool(launchChromeToolDefinition, withRecording("launch_chrome", launchChromeTool));
4095
+ registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
4096
+ registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
4097
+ registerTool(switchTabToolDefinition, switchTabTool);
4098
+ registerTool(switchFrameToolDefinition, switchFrameTool);
4099
+ registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
4100
+ registerTool(clickToolDefinition, withRecording("click_element", clickTool));
4101
+ registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
4102
+ registerTool(setCookieToolDefinition, setCookieTool);
4103
+ registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
4104
+ registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
4105
+ registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
4106
+ registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
4107
+ registerTool(switchContextToolDefinition, switchContextTool);
4108
+ registerTool(rotateDeviceToolDefinition, rotateDeviceTool);
4109
+ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
4110
+ registerTool(setGeolocationToolDefinition, setGeolocationTool);
4111
+ registerTool(executeScriptToolDefinition, withRecording("execute_script", executeScriptTool));
4112
+ registerTool(getElementsToolDefinition, getElementsTool);
4113
+ registerTool(listAppsToolDefinition, listAppsTool);
4114
+ registerTool(uploadAppToolDefinition, uploadAppTool);
4115
+ registerTool(screenshotToolDefinition, screenshotTool);
4116
+ registerTool(accessibilityToolDefinition, accessibilityTool);
4117
+ registerTool(getTabsToolDefinition, getTabsTool);
4118
+ registerTool(getContextsToolDefinition, getContextsTool);
4119
+ registerTool(appStateToolDefinition, appStateTool);
4120
+ registerTool(getCookiesToolDefinition, getCookiesTool);
4121
+ registerResource(sessionsIndexResource);
4122
+ registerResource(sessionCurrentStepsResource);
4123
+ registerResource(sessionCurrentCodeResource);
4124
+ registerResource(sessionStepsResource);
4125
+ registerResource(sessionCodeResource);
4126
+ registerResource(browserstackLocalBinaryResource);
4127
+ registerResource(capabilitiesResource);
4128
+ registerResource(elementsResource);
4129
+ registerResource(accessibilityResource);
4130
+ registerResource(screenshotResource);
4131
+ registerResource(cookiesResource);
4132
+ registerResource(appStateResource);
4133
+ registerResource(contextsResource);
4134
+ registerResource(contextResource);
4135
+ registerResource(geolocationResource);
4136
+ registerResource(tabsResource);
4137
+ return server;
4138
+ }
4139
+ async function main() {
4140
+ let args;
4141
+ try {
4142
+ args = parseArgs(process.argv.slice(2));
4143
+ } catch (e) {
4144
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
4145
+ process.exit(1);
4146
+ }
4147
+ if (args.http) {
4148
+ http.createServer((req, res) => {
4149
+ const host = extractHost(req.headers.host ?? "");
4150
+ if (!args.allowedHosts.includes(host)) {
4151
+ sendJsonRpcError(res, 403, -32e3, "Host not allowed");
4152
+ return;
4153
+ }
4154
+ const origin = req.headers.origin;
4155
+ if (origin) {
4156
+ const wildcard = args.allowedOrigins.includes("*");
4157
+ const allowed = wildcard || args.allowedOrigins.includes(origin);
4158
+ if (!allowed) {
4159
+ console.error(`[WARN] Blocked origin: ${origin}. Add --allowedOrigins ${origin} (or '*' for all) to allow it.`);
4160
+ sendJsonRpcError(res, 403, -32e3, "Origin not allowed");
4161
+ return;
4162
+ }
4163
+ res.setHeader("Access-Control-Allow-Origin", wildcard ? "*" : origin);
4164
+ if (!wildcard) res.setHeader("Vary", "Origin");
4165
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4166
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, mcp-session-id, mcp-protocol-version");
4167
+ }
4168
+ if (req.method === "OPTIONS") {
4169
+ res.writeHead(204).end();
4170
+ return;
4171
+ }
4172
+ if (!req.url?.startsWith("/mcp")) {
4173
+ sendJsonRpcError(res, 404, -32601, "Not found");
4174
+ return;
4175
+ }
4176
+ void (async () => {
4177
+ try {
4178
+ const chunks = [];
4179
+ let totalSize = 0;
4180
+ for await (const chunk of req) {
4181
+ totalSize += chunk.length;
4182
+ if (totalSize > 1024 * 1024) {
4183
+ sendJsonRpcError(res, 413, -32600, "Payload too large");
4184
+ return;
4185
+ }
4186
+ chunks.push(chunk);
4187
+ }
4188
+ const raw = Buffer.concat(chunks).toString();
4189
+ let body;
4190
+ try {
4191
+ body = raw ? JSON.parse(raw) : void 0;
4192
+ } catch {
4193
+ sendJsonRpcError(res, 400, -32700, "Parse error");
4194
+ return;
4195
+ }
4196
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
4197
+ await createServer().connect(transport);
4198
+ await transport.handleRequest(req, res, body);
4199
+ } catch (e) {
4200
+ const code = e.code;
4201
+ if (code === "ECONNRESET" || code === "ECONNABORTED" || e.message === "aborted") return;
4202
+ console.error("[WARN] Request failed:", e);
4203
+ if (!res.headersSent) sendJsonRpcError(res, 500, -32603, "Internal error");
4204
+ }
4205
+ })();
4206
+ }).listen(args.port, () => {
4207
+ const originsMsg = args.allowedOrigins.length ? args.allowedOrigins.join(", ") : "(none \u2014 browsers blocked)";
4208
+ console.error(`WebdriverIO MCP Server running on Streamable HTTP at http://localhost:${args.port}/mcp`);
4209
+ console.error(` allowed hosts: ${args.allowedHosts.join(", ")}`);
4210
+ console.error(` allowed origins: ${originsMsg}`);
4211
+ });
3897
4212
  } else {
3898
- server.registerResource(
3899
- definition.name,
3900
- definition.template,
3901
- { description: definition.description },
3902
- definition.handler
3903
- );
4213
+ const transport = new StdioServerTransport();
4214
+ await createServer().connect(transport);
4215
+ console.error("WebdriverIO MCP Server running on stdio");
3904
4216
  }
3905
- };
3906
- registerTool(startSessionToolDefinition, withRecording("start_session", startSessionTool));
3907
- registerTool(closeSessionToolDefinition, closeSessionTool);
3908
- registerTool(launchChromeToolDefinition, withRecording("launch_chrome", launchChromeTool));
3909
- registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
3910
- registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
3911
- registerTool(switchTabToolDefinition, switchTabTool);
3912
- registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
3913
- registerTool(clickToolDefinition, withRecording("click_element", clickTool));
3914
- registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
3915
- registerTool(setCookieToolDefinition, setCookieTool);
3916
- registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
3917
- registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
3918
- registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
3919
- registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
3920
- registerTool(switchContextToolDefinition, switchContextTool);
3921
- registerTool(rotateDeviceToolDefinition, rotateDeviceTool);
3922
- registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
3923
- registerTool(setGeolocationToolDefinition, setGeolocationTool);
3924
- registerTool(executeScriptToolDefinition, withRecording("execute_script", executeScriptTool));
3925
- registerTool(getElementsToolDefinition, getElementsTool);
3926
- registerTool(listAppsToolDefinition, listAppsTool);
3927
- registerTool(uploadAppToolDefinition, uploadAppTool);
3928
- registerResource(sessionsIndexResource);
3929
- registerResource(sessionCurrentStepsResource);
3930
- registerResource(sessionCurrentCodeResource);
3931
- registerResource(sessionStepsResource);
3932
- registerResource(sessionCodeResource);
3933
- registerResource(browserstackLocalBinaryResource);
3934
- registerResource(capabilitiesResource);
3935
- registerResource(elementsResource);
3936
- registerResource(accessibilityResource);
3937
- registerResource(screenshotResource);
3938
- registerResource(cookiesResource);
3939
- registerResource(appStateResource);
3940
- registerResource(contextsResource);
3941
- registerResource(contextResource);
3942
- registerResource(geolocationResource);
3943
- registerResource(tabsResource);
3944
- async function main() {
3945
- const transport = new StdioServerTransport();
3946
- await server.connect(transport);
3947
- console.error("WebdriverIO MCP Server running on stdio");
3948
4217
  }
3949
4218
  main().catch((error) => {
3950
4219
  console.error("Fatal error in main():", error);