11#!/usr/bin/env node
22import pkg from '../package.json' with { type : 'json' } ;
3+ import http from 'node:http' ;
34import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js' ;
45import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' ;
56import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' ;
7+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' ;
8+ import { parseArgs } from './utils/parse-args' ;
9+ import { extractHost , sendJsonRpcError } from './utils/http-helpers' ;
610import type { ToolDefinition } from './types/tool' ;
711import type { ResourceDefinition } from './types/resource' ;
812import { navigateTool , navigateToolDefinition } from './tools/navigate.tool' ;
@@ -62,12 +66,7 @@ import {
6266 startSessionToolDefinition
6367} from './tools/session.tool' ;
6468import { switchTabTool , switchTabToolDefinition } from './tools/tabs.tool' ;
65- import {
66- listAppsTool ,
67- listAppsToolDefinition ,
68- uploadAppTool ,
69- uploadAppToolDefinition ,
70- } from './tools/browserstack.tool' ;
69+ import { listAppsTool , listAppsToolDefinition , uploadAppTool , uploadAppToolDefinition , } from './tools/browserstack.tool' ;
7170import { screenshotTool , screenshotToolDefinition } from './tools/screenshot.tool' ;
7271import { accessibilityTool , accessibilityToolDefinition } from './tools/accessibility.tool' ;
7372import { getTabsTool , getTabsToolDefinition } from './tools/get-tabs.tool' ;
@@ -80,105 +79,190 @@ console.info = (...args) => console.error('[INFO]', ...args);
8079console . warn = ( ...args ) => console . error ( '[WARN]' , ...args ) ;
8180console . debug = ( ...args ) => console . error ( '[DEBUG]' , ...args ) ;
8281
83- const server = new McpServer ( {
84- title : 'WebdriverIO MCP Server' ,
85- name : pkg . name ,
86- version : pkg . version ,
87- description : pkg . description ,
88- websiteUrl : 'https://github.com/webdriverio/mcp' ,
89- } , {
90- 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.' ,
91- capabilities : {
92- tools : { } ,
93- resources : { } ,
94- } ,
95- } ) ;
96-
97- const registerTool = ( definition : ToolDefinition , callback : ToolCallback ) =>
98- server . registerTool ( definition . name , {
99- description : definition . description ,
100- inputSchema : definition . inputSchema ,
101- } , callback ) ;
102-
103- const registerResource = ( definition : ResourceDefinition ) => {
104- if ( 'uri' in definition ) {
105- server . registerResource (
106- definition . name ,
107- definition . uri ,
108- { description : definition . description } ,
109- definition . handler ,
110- ) ;
111- } else {
112- server . registerResource (
113- definition . name ,
114- definition . template ,
115- { description : definition . description } ,
116- definition . handler ,
117- ) ;
118- }
119- } ;
82+ function createServer ( ) : McpServer {
83+ const server = new McpServer ( {
84+ title : 'WebdriverIO MCP Server' ,
85+ name : pkg . name ,
86+ version : pkg . version ,
87+ description : pkg . description ,
88+ websiteUrl : 'https://github.com/webdriverio/mcp' ,
89+ } , {
90+ 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.' ,
91+ capabilities : {
92+ tools : { } ,
93+ resources : { } ,
94+ } ,
95+ } ) ;
96+
97+ const registerTool = ( definition : ToolDefinition , callback : ToolCallback ) =>
98+ server . registerTool ( definition . name , {
99+ description : definition . description ,
100+ inputSchema : definition . inputSchema ,
101+ } , callback ) ;
102+
103+ const registerResource = ( definition : ResourceDefinition ) => {
104+ if ( 'uri' in definition ) {
105+ server . registerResource (
106+ definition . name ,
107+ definition . uri ,
108+ { description : definition . description } ,
109+ definition . handler ,
110+ ) ;
111+ } else {
112+ server . registerResource (
113+ definition . name ,
114+ definition . template ,
115+ { description : definition . description } ,
116+ definition . handler ,
117+ ) ;
118+ }
119+ } ;
120120
121- registerTool ( startSessionToolDefinition , withRecording ( 'start_session' , startSessionTool ) ) ;
122- registerTool ( closeSessionToolDefinition , closeSessionTool ) ;
123- registerTool ( launchChromeToolDefinition , withRecording ( 'launch_chrome' , launchChromeTool ) ) ;
124- registerTool ( emulateDeviceToolDefinition , emulateDeviceTool ) ;
125- registerTool ( navigateToolDefinition , withRecording ( 'navigate' , navigateTool ) ) ;
121+ registerTool ( startSessionToolDefinition , withRecording ( 'start_session' , startSessionTool ) ) ;
122+ registerTool ( closeSessionToolDefinition , closeSessionTool ) ;
123+ registerTool ( launchChromeToolDefinition , withRecording ( 'launch_chrome' , launchChromeTool ) ) ;
124+ registerTool ( emulateDeviceToolDefinition , emulateDeviceTool ) ;
125+ registerTool ( navigateToolDefinition , withRecording ( 'navigate' , navigateTool ) ) ;
126126
127- registerTool ( switchTabToolDefinition , switchTabTool ) ;
127+ registerTool ( switchTabToolDefinition , switchTabTool ) ;
128128
129- registerTool ( scrollToolDefinition , withRecording ( 'scroll' , scrollTool ) ) ;
129+ registerTool ( scrollToolDefinition , withRecording ( 'scroll' , scrollTool ) ) ;
130130
131- registerTool ( clickToolDefinition , withRecording ( 'click_element' , clickTool ) ) ;
132- registerTool ( setValueToolDefinition , withRecording ( 'set_value' , setValueTool ) ) ;
131+ registerTool ( clickToolDefinition , withRecording ( 'click_element' , clickTool ) ) ;
132+ registerTool ( setValueToolDefinition , withRecording ( 'set_value' , setValueTool ) ) ;
133133
134- registerTool ( setCookieToolDefinition , setCookieTool ) ;
135- registerTool ( deleteCookiesToolDefinition , deleteCookiesTool ) ;
134+ registerTool ( setCookieToolDefinition , setCookieTool ) ;
135+ registerTool ( deleteCookiesToolDefinition , deleteCookiesTool ) ;
136136
137- registerTool ( tapElementToolDefinition , withRecording ( 'tap_element' , tapElementTool ) ) ;
138- registerTool ( swipeToolDefinition , withRecording ( 'swipe' , swipeTool ) ) ;
139- registerTool ( dragAndDropToolDefinition , withRecording ( 'drag_and_drop' , dragAndDropTool ) ) ;
137+ registerTool ( tapElementToolDefinition , withRecording ( 'tap_element' , tapElementTool ) ) ;
138+ registerTool ( swipeToolDefinition , withRecording ( 'swipe' , swipeTool ) ) ;
139+ registerTool ( dragAndDropToolDefinition , withRecording ( 'drag_and_drop' , dragAndDropTool ) ) ;
140140
141- registerTool ( switchContextToolDefinition , switchContextTool ) ;
141+ registerTool ( switchContextToolDefinition , switchContextTool ) ;
142142
143- registerTool ( rotateDeviceToolDefinition , rotateDeviceTool ) ;
144- registerTool ( hideKeyboardToolDefinition , hideKeyboardTool ) ;
145- registerTool ( setGeolocationToolDefinition , setGeolocationTool ) ;
143+ registerTool ( rotateDeviceToolDefinition , rotateDeviceTool ) ;
144+ registerTool ( hideKeyboardToolDefinition , hideKeyboardTool ) ;
145+ registerTool ( setGeolocationToolDefinition , setGeolocationTool ) ;
146146
147- registerTool ( executeScriptToolDefinition , withRecording ( 'execute_script' , executeScriptTool ) ) ;
148- registerTool ( getElementsToolDefinition , getElementsTool ) ;
147+ registerTool ( executeScriptToolDefinition , withRecording ( 'execute_script' , executeScriptTool ) ) ;
148+ registerTool ( getElementsToolDefinition , getElementsTool ) ;
149149
150- registerTool ( listAppsToolDefinition , listAppsTool ) ;
151- registerTool ( uploadAppToolDefinition , uploadAppTool ) ;
150+ registerTool ( listAppsToolDefinition , listAppsTool ) ;
151+ registerTool ( uploadAppToolDefinition , uploadAppTool ) ;
152152
153- registerTool ( screenshotToolDefinition , screenshotTool ) ;
154- registerTool ( accessibilityToolDefinition , accessibilityTool ) ;
155- registerTool ( getTabsToolDefinition , getTabsTool ) ;
156- registerTool ( getContextsToolDefinition , getContextsTool ) ;
157- registerTool ( appStateToolDefinition , appStateTool ) ;
158- registerTool ( getCookiesToolDefinition , getCookiesTool ) ;
153+ registerTool ( screenshotToolDefinition , screenshotTool ) ;
154+ registerTool ( accessibilityToolDefinition , accessibilityTool ) ;
155+ registerTool ( getTabsToolDefinition , getTabsTool ) ;
156+ registerTool ( getContextsToolDefinition , getContextsTool ) ;
157+ registerTool ( appStateToolDefinition , appStateTool ) ;
158+ registerTool ( getCookiesToolDefinition , getCookiesTool ) ;
159159
160- registerResource ( sessionsIndexResource ) ;
161- registerResource ( sessionCurrentStepsResource ) ;
162- registerResource ( sessionCurrentCodeResource ) ;
163- registerResource ( sessionStepsResource ) ;
164- registerResource ( sessionCodeResource ) ;
160+ registerResource ( sessionsIndexResource ) ;
161+ registerResource ( sessionCurrentStepsResource ) ;
162+ registerResource ( sessionCurrentCodeResource ) ;
163+ registerResource ( sessionStepsResource ) ;
164+ registerResource ( sessionCodeResource ) ;
165165
166- registerResource ( browserstackLocalBinaryResource ) ;
167- registerResource ( capabilitiesResource ) ;
168- registerResource ( elementsResource ) ;
169- registerResource ( accessibilityResource ) ;
170- registerResource ( screenshotResource ) ;
171- registerResource ( cookiesResource ) ;
172- registerResource ( appStateResource ) ;
173- registerResource ( contextsResource ) ;
174- registerResource ( contextResource ) ;
175- registerResource ( geolocationResource ) ;
176- registerResource ( tabsResource ) ;
166+ registerResource ( browserstackLocalBinaryResource ) ;
167+ registerResource ( capabilitiesResource ) ;
168+ registerResource ( elementsResource ) ;
169+ registerResource ( accessibilityResource ) ;
170+ registerResource ( screenshotResource ) ;
171+ registerResource ( cookiesResource ) ;
172+ registerResource ( appStateResource ) ;
173+ registerResource ( contextsResource ) ;
174+ registerResource ( contextResource ) ;
175+ registerResource ( geolocationResource ) ;
176+ registerResource ( tabsResource ) ;
177+
178+ return server ;
179+ }
177180
178181async function main ( ) {
179- const transport = new StdioServerTransport ( ) ;
180- await server . connect ( transport ) ;
181- console . error ( 'WebdriverIO MCP Server running on stdio' ) ;
182+ let args : { http: boolean ; port: number ; allowedHosts: string [ ] ; allowedOrigins: string [ ] } ;
183+ try {
184+ args = parseArgs ( process . argv . slice ( 2 ) ) ;
185+ } catch ( e ) {
186+ console . error ( `Error: ${ e instanceof Error ? e . message : String ( e ) } ` ) ;
187+ process . exit ( 1 ) ;
188+ }
189+
190+ if ( args . http ) {
191+ http . createServer ( ( req , res ) => {
192+
193+ const host = extractHost ( req . headers . host ?? '' ) ;
194+ if ( ! args . allowedHosts . includes ( host ) ) {
195+ sendJsonRpcError ( res , 403 , - 32000 , 'Host not allowed' ) ;
196+ return ;
197+ }
198+
199+ const origin = req . headers . origin ;
200+ if ( origin ) {
201+ const wildcard = args . allowedOrigins . includes ( '*' ) ;
202+ const allowed = wildcard || args . allowedOrigins . includes ( origin ) ;
203+ if ( ! allowed ) {
204+ console . error ( `[WARN] Blocked origin: ${ origin } . Add --allowedOrigins ${ origin } (or '*' for all) to allow it.` ) ;
205+ sendJsonRpcError ( res , 403 , - 32000 , 'Origin not allowed' ) ;
206+ return ;
207+ }
208+ res . setHeader ( 'Access-Control-Allow-Origin' , wildcard ? '*' : origin ) ;
209+ if ( ! wildcard ) res . setHeader ( 'Vary' , 'Origin' ) ;
210+ res . setHeader ( 'Access-Control-Allow-Methods' , 'GET, POST, OPTIONS' ) ;
211+ res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, Accept, mcp-session-id, mcp-protocol-version' ) ;
212+ }
213+
214+ if ( req . method === 'OPTIONS' ) {
215+ res . writeHead ( 204 ) . end ( ) ;
216+ return ;
217+ }
218+
219+ if ( ! req . url ?. startsWith ( '/mcp' ) ) {
220+ sendJsonRpcError ( res , 404 , - 32601 , 'Not found' ) ;
221+ return ;
222+ }
223+
224+ void ( async ( ) => {
225+ try {
226+ const chunks : Buffer [ ] = [ ] ;
227+ let totalSize = 0 ;
228+ for await ( const chunk of req ) {
229+ totalSize += ( chunk as Buffer ) . length ;
230+ if ( totalSize > 1024 * 1024 ) {
231+ sendJsonRpcError ( res , 413 , - 32600 , 'Payload too large' ) ;
232+ return ;
233+ }
234+ chunks . push ( chunk as Buffer ) ;
235+ }
236+ const raw = Buffer . concat ( chunks ) . toString ( ) ;
237+ let body : unknown ;
238+ try {
239+ body = raw ? JSON . parse ( raw ) : undefined ;
240+ } catch {
241+ sendJsonRpcError ( res , 400 , - 32700 , 'Parse error' ) ;
242+ return ;
243+ }
244+ const transport = new StreamableHTTPServerTransport ( { sessionIdGenerator : undefined } ) ;
245+ await createServer ( ) . connect ( transport ) ;
246+ await transport . handleRequest ( req , res , body ) ;
247+ } catch ( e ) {
248+ const code = ( e as NodeJS . ErrnoException ) . code ;
249+ if ( code === 'ECONNRESET' || code === 'ECONNABORTED' || ( e as Error ) . message === 'aborted' ) return ;
250+ console . error ( '[WARN] Request failed:' , e ) ;
251+ if ( ! res . headersSent ) sendJsonRpcError ( res , 500 , - 32603 , 'Internal error' ) ;
252+ }
253+ } ) ( ) ;
254+ } ) . listen ( args . port , ( ) => {
255+ const originsMsg = args . allowedOrigins . length ? args . allowedOrigins . join ( ', ' ) : '(none — browsers blocked)' ;
256+ console . error ( `WebdriverIO MCP Server running on Streamable HTTP at http://localhost:${ args . port } /mcp` ) ;
257+ console . error ( ` allowed hosts: ${ args . allowedHosts . join ( ', ' ) } ` ) ;
258+ console . error ( ` allowed origins: ${ originsMsg } ` ) ;
259+ } ) ;
260+
261+ } else {
262+ const transport = new StdioServerTransport ( ) ;
263+ await createServer ( ) . connect ( transport ) ;
264+ console . error ( 'WebdriverIO MCP Server running on stdio' ) ;
265+ }
182266}
183267
184268main ( ) . catch ( ( error ) => {
0 commit comments