@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/README.md +166 -102
- package/lib/server.js +368 -99
- package/lib/server.js.map +1 -1
- package/package.json +3 -1
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.
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
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
|
|
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: "
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
3829
|
-
customId:
|
|
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
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
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
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
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);
|