@wdio/mcp 3.2.4 → 3.3.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.3",
49
+ version: "3.2.5",
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 = {
@@ -3450,7 +3501,7 @@ var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
3450
3501
  var automationEnum = z14.enum(["XCUITest", "UiAutomator2"]);
3451
3502
  var startSessionToolDefinition = {
3452
3503
  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.",
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.',
3454
3505
  inputSchema: {
3455
3506
  provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
3456
3507
  platform: platformEnum.describe("Session platform type"),
@@ -3496,7 +3547,7 @@ var startSessionToolDefinition = {
3496
3547
  };
3497
3548
  var closeSessionToolDefinition = {
3498
3549
  name: "close_session",
3499
- description: "Closes or detaches from the current browser or app 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.",
3500
3551
  inputSchema: {
3501
3552
  detach: coerceBoolean.optional().describe("If true, disconnect without terminating (preserves app state). Default: false")
3502
3553
  }
@@ -3746,7 +3797,7 @@ init_state();
3746
3797
  import { z as z15 } from "zod";
3747
3798
  var switchTabToolDefinition = {
3748
3799
  name: "switch_tab",
3749
- description: "switches to a browser tab by handle or index",
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.",
3750
3801
  inputSchema: {
3751
3802
  handle: z15.string().optional().describe("Window handle to switch to"),
3752
3803
  index: z15.number().int().min(0).optional().describe("0-based tab index to switch to")
@@ -3864,87 +3915,272 @@ Use this URL as the "app" parameter in start_session with provider: "browserstac
3864
3915
  }
3865
3916
  };
3866
3917
 
3918
+ // src/tools/screenshot.tool.ts
3919
+ var screenshotToolDefinition = {
3920
+ name: "get_screenshot",
3921
+ description: "Takes a screenshot of the current page or screen and returns a base64-encoded image, resized and compressed for model context limits.",
3922
+ inputSchema: {}
3923
+ };
3924
+ var screenshotTool = async () => {
3925
+ const result = await readScreenshot();
3926
+ if (result.mimeType === "text/plain") {
3927
+ return { isError: true, content: [{ type: "text", text: Buffer.from(result.blob, "base64").toString("utf-8") }] };
3928
+ }
3929
+ return { content: [{ type: "image", data: result.blob, mimeType: result.mimeType }] };
3930
+ };
3931
+
3932
+ // src/tools/accessibility.tool.ts
3933
+ import { z as z17 } from "zod";
3934
+ var accessibilityToolDefinition = {
3935
+ name: "get_accessibility_tree",
3936
+ description: "Returns the page accessibility tree with roles, names, and selectors. Browser-only. Supports filtering by ARIA roles and pagination via limit/offset.",
3937
+ 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"]')
3941
+ }
3942
+ };
3943
+ var accessibilityTool = async ({ limit = 0, offset = 0, roles }) => {
3944
+ const result = await readAccessibilityTree({ limit, offset, roles });
3945
+ if (result.text.startsWith("Error")) {
3946
+ return { isError: true, content: [{ type: "text", text: result.text }] };
3947
+ }
3948
+ return { content: [{ type: "text", text: result.text }] };
3949
+ };
3950
+
3951
+ // src/tools/get-tabs.tool.ts
3952
+ var getTabsToolDefinition = {
3953
+ name: "get_tabs",
3954
+ 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.",
3955
+ inputSchema: {}
3956
+ };
3957
+ var getTabsTool = async () => {
3958
+ const result = await readTabs();
3959
+ if (result.text.startsWith("Error")) {
3960
+ return { isError: true, content: [{ type: "text", text: result.text }] };
3961
+ }
3962
+ return { content: [{ type: "text", text: result.text }] };
3963
+ };
3964
+
3965
+ // src/tools/get-contexts.tool.ts
3966
+ var getContextsToolDefinition = {
3967
+ name: "get_contexts",
3968
+ description: "Returns available automation contexts and the currently active one. Use before switch_context to discover NATIVE_APP and WEBVIEW_* targets. Mobile-only.",
3969
+ inputSchema: {}
3970
+ };
3971
+ var getContextsTool = async () => {
3972
+ const [contexts, current] = await Promise.all([readContexts(), readCurrentContext()]);
3973
+ if (contexts.mimeType === "text/plain" && contexts.text.startsWith("Error")) {
3974
+ return { isError: true, content: [{ type: "text", text: contexts.text }] };
3975
+ }
3976
+ if (current.mimeType === "text/plain" && current.text.startsWith("Error")) {
3977
+ return { isError: true, content: [{ type: "text", text: current.text }] };
3978
+ }
3979
+ const combined = {
3980
+ contexts: JSON.parse(contexts.text),
3981
+ currentContext: JSON.parse(current.text)
3982
+ };
3983
+ return { content: [{ type: "text", text: JSON.stringify(combined) }] };
3984
+ };
3985
+
3986
+ // src/tools/app-state.tool.ts
3987
+ import { z as z18 } from "zod";
3988
+ var appStateToolDefinition = {
3989
+ name: "get_app_state",
3990
+ description: "Returns the current state of a mobile app: not installed, not running, background, or foreground. Mobile-only.",
3991
+ inputSchema: {
3992
+ bundleId: z18.string().describe('App bundle ID (iOS) or package name (Android), e.g. "com.example.app"')
3993
+ }
3994
+ };
3995
+ var appStateTool = async ({ bundleId }) => {
3996
+ const result = await readAppState(bundleId);
3997
+ if (result.text.startsWith("Error")) {
3998
+ return { isError: true, content: [{ type: "text", text: result.text }] };
3999
+ }
4000
+ return { content: [{ type: "text", text: result.text }] };
4001
+ };
4002
+
4003
+ // src/tools/get-cookies.tool.ts
4004
+ import { z as z19 } from "zod";
4005
+ var getCookiesToolDefinition = {
4006
+ name: "get_cookies",
4007
+ 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.",
4008
+ inputSchema: {
4009
+ name: z19.string().optional().describe("Cookie name to retrieve a specific cookie. If omitted, returns all cookies.")
4010
+ }
4011
+ };
4012
+ var getCookiesTool = async ({ name }) => {
4013
+ const result = await readCookies(name);
4014
+ if (result.mimeType === "text/plain" && result.text.startsWith("Error")) {
4015
+ return { isError: true, content: [{ type: "text", text: result.text }] };
4016
+ }
4017
+ return { content: [{ type: "text", text: result.text }] };
4018
+ };
4019
+
3867
4020
  // src/server.ts
3868
4021
  console.log = (...args) => console.error("[LOG]", ...args);
3869
4022
  console.info = (...args) => console.error("[INFO]", ...args);
3870
4023
  console.warn = (...args) => console.error("[WARN]", ...args);
3871
4024
  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
- );
4025
+ function createServer() {
4026
+ const server = new McpServer({
4027
+ title: "WebdriverIO MCP Server",
4028
+ name: package_default.name,
4029
+ version: package_default.version,
4030
+ description: package_default.description,
4031
+ websiteUrl: "https://github.com/webdriverio/mcp"
4032
+ }, {
4033
+ 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.",
4034
+ capabilities: {
4035
+ tools: {},
4036
+ resources: {}
4037
+ }
4038
+ });
4039
+ const registerTool = (definition, callback) => server.registerTool(definition.name, {
4040
+ description: definition.description,
4041
+ inputSchema: definition.inputSchema
4042
+ }, callback);
4043
+ const registerResource = (definition) => {
4044
+ if ("uri" in definition) {
4045
+ server.registerResource(
4046
+ definition.name,
4047
+ definition.uri,
4048
+ { description: definition.description },
4049
+ definition.handler
4050
+ );
4051
+ } else {
4052
+ server.registerResource(
4053
+ definition.name,
4054
+ definition.template,
4055
+ { description: definition.description },
4056
+ definition.handler
4057
+ );
4058
+ }
4059
+ };
4060
+ registerTool(startSessionToolDefinition, withRecording("start_session", startSessionTool));
4061
+ registerTool(closeSessionToolDefinition, closeSessionTool);
4062
+ registerTool(launchChromeToolDefinition, withRecording("launch_chrome", launchChromeTool));
4063
+ registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
4064
+ registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
4065
+ registerTool(switchTabToolDefinition, switchTabTool);
4066
+ registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
4067
+ registerTool(clickToolDefinition, withRecording("click_element", clickTool));
4068
+ registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
4069
+ registerTool(setCookieToolDefinition, setCookieTool);
4070
+ registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
4071
+ registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
4072
+ registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
4073
+ registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
4074
+ registerTool(switchContextToolDefinition, switchContextTool);
4075
+ registerTool(rotateDeviceToolDefinition, rotateDeviceTool);
4076
+ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
4077
+ registerTool(setGeolocationToolDefinition, setGeolocationTool);
4078
+ registerTool(executeScriptToolDefinition, withRecording("execute_script", executeScriptTool));
4079
+ registerTool(getElementsToolDefinition, getElementsTool);
4080
+ registerTool(listAppsToolDefinition, listAppsTool);
4081
+ registerTool(uploadAppToolDefinition, uploadAppTool);
4082
+ registerTool(screenshotToolDefinition, screenshotTool);
4083
+ registerTool(accessibilityToolDefinition, accessibilityTool);
4084
+ registerTool(getTabsToolDefinition, getTabsTool);
4085
+ registerTool(getContextsToolDefinition, getContextsTool);
4086
+ registerTool(appStateToolDefinition, appStateTool);
4087
+ registerTool(getCookiesToolDefinition, getCookiesTool);
4088
+ registerResource(sessionsIndexResource);
4089
+ registerResource(sessionCurrentStepsResource);
4090
+ registerResource(sessionCurrentCodeResource);
4091
+ registerResource(sessionStepsResource);
4092
+ registerResource(sessionCodeResource);
4093
+ registerResource(browserstackLocalBinaryResource);
4094
+ registerResource(capabilitiesResource);
4095
+ registerResource(elementsResource);
4096
+ registerResource(accessibilityResource);
4097
+ registerResource(screenshotResource);
4098
+ registerResource(cookiesResource);
4099
+ registerResource(appStateResource);
4100
+ registerResource(contextsResource);
4101
+ registerResource(contextResource);
4102
+ registerResource(geolocationResource);
4103
+ registerResource(tabsResource);
4104
+ return server;
4105
+ }
4106
+ async function main() {
4107
+ let args;
4108
+ try {
4109
+ args = parseArgs(process.argv.slice(2));
4110
+ } catch (e) {
4111
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
4112
+ process.exit(1);
4113
+ }
4114
+ if (args.http) {
4115
+ http.createServer((req, res) => {
4116
+ const host = extractHost(req.headers.host ?? "");
4117
+ if (!args.allowedHosts.includes(host)) {
4118
+ sendJsonRpcError(res, 403, -32e3, "Host not allowed");
4119
+ return;
4120
+ }
4121
+ const origin = req.headers.origin;
4122
+ if (origin) {
4123
+ const wildcard = args.allowedOrigins.includes("*");
4124
+ const allowed = wildcard || args.allowedOrigins.includes(origin);
4125
+ if (!allowed) {
4126
+ console.error(`[WARN] Blocked origin: ${origin}. Add --allowedOrigins ${origin} (or '*' for all) to allow it.`);
4127
+ sendJsonRpcError(res, 403, -32e3, "Origin not allowed");
4128
+ return;
4129
+ }
4130
+ res.setHeader("Access-Control-Allow-Origin", wildcard ? "*" : origin);
4131
+ if (!wildcard) res.setHeader("Vary", "Origin");
4132
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4133
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, mcp-session-id, mcp-protocol-version");
4134
+ }
4135
+ if (req.method === "OPTIONS") {
4136
+ res.writeHead(204).end();
4137
+ return;
4138
+ }
4139
+ if (!req.url?.startsWith("/mcp")) {
4140
+ sendJsonRpcError(res, 404, -32601, "Not found");
4141
+ return;
4142
+ }
4143
+ void (async () => {
4144
+ try {
4145
+ const chunks = [];
4146
+ let totalSize = 0;
4147
+ for await (const chunk of req) {
4148
+ totalSize += chunk.length;
4149
+ if (totalSize > 1024 * 1024) {
4150
+ sendJsonRpcError(res, 413, -32600, "Payload too large");
4151
+ return;
4152
+ }
4153
+ chunks.push(chunk);
4154
+ }
4155
+ const raw = Buffer.concat(chunks).toString();
4156
+ let body;
4157
+ try {
4158
+ body = raw ? JSON.parse(raw) : void 0;
4159
+ } catch {
4160
+ sendJsonRpcError(res, 400, -32700, "Parse error");
4161
+ return;
4162
+ }
4163
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
4164
+ await createServer().connect(transport);
4165
+ await transport.handleRequest(req, res, body);
4166
+ } catch (e) {
4167
+ const code = e.code;
4168
+ if (code === "ECONNRESET" || code === "ECONNABORTED" || e.message === "aborted") return;
4169
+ console.error("[WARN] Request failed:", e);
4170
+ if (!res.headersSent) sendJsonRpcError(res, 500, -32603, "Internal error");
4171
+ }
4172
+ })();
4173
+ }).listen(args.port, () => {
4174
+ const originsMsg = args.allowedOrigins.length ? args.allowedOrigins.join(", ") : "(none \u2014 browsers blocked)";
4175
+ console.error(`WebdriverIO MCP Server running on Streamable HTTP at http://localhost:${args.port}/mcp`);
4176
+ console.error(` allowed hosts: ${args.allowedHosts.join(", ")}`);
4177
+ console.error(` allowed origins: ${originsMsg}`);
4178
+ });
3897
4179
  } else {
3898
- server.registerResource(
3899
- definition.name,
3900
- definition.template,
3901
- { description: definition.description },
3902
- definition.handler
3903
- );
4180
+ const transport = new StdioServerTransport();
4181
+ await createServer().connect(transport);
4182
+ console.error("WebdriverIO MCP Server running on stdio");
3904
4183
  }
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
4184
  }
3949
4185
  main().catch((error) => {
3950
4186
  console.error("Fatal error in main():", error);