@browserstack/mcp-server 1.2.3 → 1.2.5

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.
Files changed (164) hide show
  1. package/README.md +96 -6
  2. package/dist/lib/apiClient.d.ts +7 -5
  3. package/dist/lib/apiClient.js +76 -15
  4. package/dist/lib/device-cache.d.ts +3 -1
  5. package/dist/lib/device-cache.js +24 -17
  6. package/dist/lib/inmemory-store.d.ts +1 -0
  7. package/dist/lib/inmemory-store.js +1 -0
  8. package/dist/lib/instrumentation.js +6 -3
  9. package/dist/lib/utils.d.ts +78 -0
  10. package/dist/lib/utils.js +47 -0
  11. package/dist/lib/version-resolver.js +26 -14
  12. package/dist/server-factory.js +6 -0
  13. package/dist/tools/add-percy-snapshots.d.ts +5 -0
  14. package/dist/tools/add-percy-snapshots.js +17 -0
  15. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -0
  16. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +70 -0
  17. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +23 -0
  18. package/dist/tools/appautomate-utils/appium-sdk/constants.js +64 -0
  19. package/dist/tools/appautomate-utils/appium-sdk/formatter.d.ts +8 -0
  20. package/dist/tools/appautomate-utils/appium-sdk/formatter.js +59 -0
  21. package/dist/tools/appautomate-utils/appium-sdk/handler.d.ts +3 -0
  22. package/dist/tools/appautomate-utils/appium-sdk/handler.js +66 -0
  23. package/dist/tools/appautomate-utils/appium-sdk/index.d.ts +7 -0
  24. package/dist/tools/appautomate-utils/appium-sdk/index.js +8 -0
  25. package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +3 -0
  26. package/dist/tools/appautomate-utils/appium-sdk/instructions.js +47 -0
  27. package/dist/tools/appautomate-utils/appium-sdk/languages/csharp.d.ts +2 -0
  28. package/dist/tools/appautomate-utils/appium-sdk/languages/csharp.js +78 -0
  29. package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +10 -0
  30. package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +121 -0
  31. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +3 -0
  32. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +194 -0
  33. package/dist/tools/appautomate-utils/appium-sdk/languages/python.d.ts +3 -0
  34. package/dist/tools/appautomate-utils/appium-sdk/languages/python.js +76 -0
  35. package/dist/tools/appautomate-utils/appium-sdk/languages/ruby.d.ts +2 -0
  36. package/dist/tools/appautomate-utils/appium-sdk/languages/ruby.js +85 -0
  37. package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +58 -0
  38. package/dist/tools/appautomate-utils/appium-sdk/types.js +63 -0
  39. package/dist/tools/appautomate-utils/appium-sdk/utils.d.ts +17 -0
  40. package/dist/tools/appautomate-utils/appium-sdk/utils.js +64 -0
  41. package/dist/tools/appautomate-utils/{appautomate.d.ts → native-execution/appautomate.d.ts} +1 -1
  42. package/dist/tools/appautomate-utils/{appautomate.js → native-execution/appautomate.js} +2 -2
  43. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +11 -0
  44. package/dist/tools/appautomate-utils/native-execution/constants.js +58 -0
  45. package/dist/tools/appautomate-utils/native-execution/types.d.ts +19 -0
  46. package/dist/tools/appautomate-utils/{types.js → native-execution/types.js} +5 -1
  47. package/dist/tools/appautomate.js +40 -42
  48. package/dist/tools/bstack-sdk.d.ts +2 -15
  49. package/dist/tools/bstack-sdk.js +10 -119
  50. package/dist/tools/build-insights.d.ts +7 -0
  51. package/dist/tools/build-insights.js +67 -0
  52. package/dist/tools/list-test-files.d.ts +2 -0
  53. package/dist/tools/list-test-files.js +36 -0
  54. package/dist/tools/percy-sdk.d.ts +4 -0
  55. package/dist/tools/percy-sdk.js +98 -0
  56. package/dist/tools/percy-snapshot-utils/constants.d.ts +16 -0
  57. package/dist/tools/percy-snapshot-utils/constants.js +500 -0
  58. package/dist/tools/percy-snapshot-utils/detect-test-files.d.ts +10 -0
  59. package/dist/tools/percy-snapshot-utils/detect-test-files.js +175 -0
  60. package/dist/tools/percy-snapshot-utils/types.d.ts +15 -0
  61. package/dist/tools/percy-snapshot-utils/utils.d.ts +4 -0
  62. package/dist/tools/percy-snapshot-utils/utils.js +30 -0
  63. package/dist/tools/rca-agent-utils/constants.d.ts +13 -0
  64. package/dist/tools/rca-agent-utils/constants.js +24 -0
  65. package/dist/tools/rca-agent-utils/format-rca.d.ts +1 -0
  66. package/dist/tools/rca-agent-utils/format-rca.js +37 -0
  67. package/dist/tools/rca-agent-utils/get-build-id.d.ts +1 -0
  68. package/dist/tools/rca-agent-utils/get-build-id.js +18 -0
  69. package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +2 -0
  70. package/dist/tools/rca-agent-utils/get-failed-test-id.js +69 -0
  71. package/dist/tools/rca-agent-utils/rca-data.d.ts +9 -0
  72. package/dist/tools/rca-agent-utils/rca-data.js +196 -0
  73. package/dist/tools/rca-agent-utils/types.d.ts +48 -0
  74. package/dist/tools/rca-agent-utils/types.js +20 -0
  75. package/dist/tools/rca-agent.d.ts +14 -0
  76. package/dist/tools/rca-agent.js +119 -0
  77. package/dist/tools/review-agent-utils/build-counts.d.ts +7 -0
  78. package/dist/tools/review-agent-utils/build-counts.js +44 -0
  79. package/dist/tools/review-agent-utils/percy-approve-reject.d.ts +6 -0
  80. package/dist/tools/review-agent-utils/percy-approve-reject.js +39 -0
  81. package/dist/tools/review-agent-utils/percy-diffs.d.ts +9 -0
  82. package/dist/tools/review-agent-utils/percy-diffs.js +35 -0
  83. package/dist/tools/review-agent-utils/percy-snapshots.d.ts +11 -0
  84. package/dist/tools/review-agent-utils/percy-snapshots.js +58 -0
  85. package/dist/tools/review-agent.d.ts +5 -0
  86. package/dist/tools/review-agent.js +56 -0
  87. package/dist/tools/run-percy-scan.d.ts +8 -0
  88. package/dist/tools/run-percy-scan.js +37 -0
  89. package/dist/tools/sdk-utils/{commands.d.ts → bstack/commands.d.ts} +1 -1
  90. package/dist/tools/sdk-utils/bstack/commands.js +88 -0
  91. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +7 -0
  92. package/dist/tools/sdk-utils/bstack/configUtils.js +113 -0
  93. package/dist/tools/sdk-utils/bstack/constants.d.ts +58 -0
  94. package/dist/tools/sdk-utils/{constants.js → bstack/constants.js} +117 -78
  95. package/dist/tools/sdk-utils/{constants.d.ts → bstack/frameworks.d.ts} +1 -1
  96. package/dist/tools/sdk-utils/bstack/frameworks.js +57 -0
  97. package/dist/tools/sdk-utils/bstack/index.d.ts +4 -0
  98. package/dist/tools/sdk-utils/bstack/index.js +5 -0
  99. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +4 -0
  100. package/dist/tools/sdk-utils/bstack/sdkHandler.js +82 -0
  101. package/dist/tools/sdk-utils/common/constants.d.ts +11 -0
  102. package/dist/tools/sdk-utils/common/constants.js +87 -0
  103. package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
  104. package/dist/tools/sdk-utils/common/device-validator.js +368 -0
  105. package/dist/tools/sdk-utils/common/formatUtils.d.ts +5 -0
  106. package/dist/tools/sdk-utils/common/formatUtils.js +27 -0
  107. package/dist/tools/sdk-utils/common/index.d.ts +3 -0
  108. package/dist/tools/sdk-utils/common/index.js +4 -0
  109. package/dist/tools/sdk-utils/common/instructionUtils.d.ts +8 -0
  110. package/dist/tools/sdk-utils/common/instructionUtils.js +20 -0
  111. package/dist/tools/sdk-utils/common/schema.d.ts +93 -0
  112. package/dist/tools/sdk-utils/common/schema.js +105 -0
  113. package/dist/tools/sdk-utils/common/types.d.ts +66 -0
  114. package/dist/tools/sdk-utils/{types.js → common/types.js} +15 -2
  115. package/dist/tools/sdk-utils/common/utils.d.ts +25 -0
  116. package/dist/tools/sdk-utils/common/utils.js +91 -0
  117. package/dist/tools/sdk-utils/handler.d.ts +5 -0
  118. package/dist/tools/sdk-utils/handler.js +147 -0
  119. package/dist/tools/sdk-utils/percy-automate/constants.d.ts +11 -0
  120. package/dist/tools/sdk-utils/percy-automate/constants.js +338 -0
  121. package/dist/tools/sdk-utils/percy-automate/frameworks.d.ts +8 -0
  122. package/dist/tools/sdk-utils/percy-automate/frameworks.js +50 -0
  123. package/dist/tools/sdk-utils/percy-automate/handler.d.ts +3 -0
  124. package/dist/tools/sdk-utils/percy-automate/handler.js +30 -0
  125. package/dist/tools/sdk-utils/percy-automate/index.d.ts +1 -0
  126. package/dist/tools/sdk-utils/percy-automate/index.js +2 -0
  127. package/dist/tools/sdk-utils/percy-automate/types.d.ts +13 -0
  128. package/dist/tools/sdk-utils/percy-automate/types.js +1 -0
  129. package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -0
  130. package/dist/tools/sdk-utils/{percy → percy-bstack}/constants.js +13 -39
  131. package/dist/tools/sdk-utils/percy-bstack/frameworks.d.ts +2 -0
  132. package/dist/tools/sdk-utils/percy-bstack/frameworks.js +27 -0
  133. package/dist/tools/sdk-utils/percy-bstack/handler.d.ts +4 -0
  134. package/dist/tools/sdk-utils/percy-bstack/handler.js +103 -0
  135. package/dist/tools/sdk-utils/percy-bstack/index.d.ts +4 -0
  136. package/dist/tools/sdk-utils/percy-bstack/index.js +4 -0
  137. package/dist/tools/sdk-utils/percy-bstack/instructions.d.ts +7 -0
  138. package/dist/tools/sdk-utils/{percy → percy-bstack}/instructions.js +5 -9
  139. package/dist/tools/sdk-utils/percy-bstack/types.d.ts +13 -0
  140. package/dist/tools/sdk-utils/percy-bstack/types.js +5 -0
  141. package/dist/tools/sdk-utils/percy-web/constants.d.ts +41 -0
  142. package/dist/tools/sdk-utils/percy-web/constants.js +883 -0
  143. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.d.ts +4 -0
  144. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.js +32 -0
  145. package/dist/tools/sdk-utils/percy-web/frameworks.d.ts +7 -0
  146. package/dist/tools/sdk-utils/percy-web/frameworks.js +103 -0
  147. package/dist/tools/sdk-utils/percy-web/handler.d.ts +4 -0
  148. package/dist/tools/sdk-utils/percy-web/handler.js +29 -0
  149. package/dist/tools/sdk-utils/percy-web/index.d.ts +4 -0
  150. package/dist/tools/sdk-utils/percy-web/index.js +4 -0
  151. package/dist/tools/sdk-utils/percy-web/types.d.ts +12 -0
  152. package/dist/tools/sdk-utils/percy-web/types.js +1 -0
  153. package/dist/tools/testmanagement-utils/create-testrun.d.ts +4 -4
  154. package/dist/tools/testmanagement-utils/update-testrun.d.ts +4 -4
  155. package/package.json +3 -2
  156. package/dist/tools/appautomate-utils/types.d.ts +0 -5
  157. package/dist/tools/sdk-utils/commands.js +0 -65
  158. package/dist/tools/sdk-utils/instructions.d.ts +0 -6
  159. package/dist/tools/sdk-utils/instructions.js +0 -99
  160. package/dist/tools/sdk-utils/percy/constants.d.ts +0 -3
  161. package/dist/tools/sdk-utils/percy/instructions.d.ts +0 -10
  162. package/dist/tools/sdk-utils/percy/types.d.ts +0 -5
  163. package/dist/tools/sdk-utils/types.d.ts +0 -40
  164. /package/dist/tools/{sdk-utils/percy → percy-snapshot-utils}/types.js +0 -0
package/README.md CHANGED
@@ -35,10 +35,21 @@ Manage, execute, debug tests, and even fix code using plain English prompts.
35
35
  #### Reduced context switching:
36
36
  Stay in flow—keep all project context in one place and trigger actions directly from your IDE or LLM.
37
37
 
38
- ## ⚡️ One Click MCP Setup
38
+ ## ⚡️ One Click MCP Setup
39
39
 
40
- [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor)
40
+ Click on the buttons below to install MCP in your respective IDE:
41
41
 
42
+ <a href="http://mcp.browserstack.com/one-click-setup?client=vscode"><img src="assets/one-click-vs-code.png" alt="Install in VS Code" width="160" height="80"></a>&nbsp;&nbsp;&nbsp;<a href="http://mcp.browserstack.com/one-click-setup?client=cursor"><img src="assets/one-click-cursor.png" alt="Install in Cursor" width="150" height="70"></a>
43
+
44
+ #### Note : Ensure you are using Node version >= `18.0`
45
+ - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
46
+ - To Upgrade Node :
47
+ - 1. On macOS `(Homebrew) - brew update && brew upgrade node or if using (nvm) - nvm install 22.15.0 && nvm use 22.15.0 && nvm alias default 22.15.0`
48
+ - 2. On Windows `(nvm-windows) : nvm install 22.15.0 && nvm use 22.15.0`
49
+ - 👉 <a href="https://nodejs.org/en/download" target="_blank">Or directly download the Node.js LTS Installer</a>
50
+
51
+ .
52
+
42
53
  ## 💡 Usage Examples
43
54
 
44
55
  ### 📱 Manual App Testing
@@ -142,10 +153,15 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
142
153
 
143
154
  ## 🛠️ Installation
144
155
 
156
+ ### 📋 Prerequisites for MCP Setup
157
+ #### Note : Ensure you are using Node version >= `18.0`
158
+ - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
159
+
145
160
  ### **One Click MCP Setup**
146
161
 
147
- [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode) &nbsp; [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor)
162
+ Click on the buttons below to install MCP in your respective IDE:
148
163
 
164
+ <a href="http://mcp.browserstack.com/one-click-setup?client=vscode"><img src="assets/one-click-vs-code.png" alt="Install in VS Code" width="160" height="80"></a>&nbsp;&nbsp;&nbsp;<a href="http://mcp.browserstack.com/one-click-setup?client=cursor"><img src="assets/one-click-cursor.png" alt="Install in Cursor" width="150" height="70"></a>
149
165
 
150
166
  ### **Alternate ways to Setup MCP server**
151
167
 
@@ -158,7 +174,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
158
174
 
159
175
  - Once you have an account (and purchased appropriate plan), note down your `username` and `access_key` from [Account Settings](https://www.browserstack.com/accounts/profile/details).
160
176
 
161
- 2. Ensure you are using Node version >= `18.0`. Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
177
+ 2. #### Note : Ensure you are using Node version >= `18.0`
178
+ - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
179
+
162
180
 
163
181
  3. **Install the MCP Server**
164
182
 
@@ -404,14 +422,14 @@ As of now we support 20 tools.
404
422
  **Prompt example**
405
423
 
406
424
  ```text
407
- Take a screenshot of my app on Google Pixel 6 with Android 14 while testing on App Automate. App file path: /Users/xyz/app-debug.apk
425
+ Take a screenshot of my app on Google Pixel 6 with Android 12 while testing on App Automate. App file path: /Users/xyz/app-debug.apk
408
426
  ```
409
427
 
410
428
  15. `runAppTestsOnBrowserStack` — Run automated mobile tests (Espresso/XCUITest, etc.) on real devices.
411
429
  **Prompt example**
412
430
 
413
431
  ```text
414
- Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android 14. App path is /apps/beta-release.apk under project 'Checkout Flow'
432
+ Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android 12. App path is /apps/beta-release.apk under project 'Checkout Flow'
415
433
  ```
416
434
 
417
435
  ---
@@ -456,7 +474,79 @@ As of now we support 20 tools.
456
474
  ```text
457
475
  Upload PRD from /Users/xyz/Desktop/login-flow.pdf and use BrowserStack AI to generate test cases
458
476
  ```
477
+ ## 🚀 Remote MCP Server
478
+
479
+ Remote MCP comes with all the functionalities of an MCP server without the hassles of complex setup or local installation.
480
+
481
+ ### Key benefits:
482
+
483
+ - ✅ Works seamlessly in enterprise networks without worrying about firewalls or binaries or where local installation is not allowed.
484
+
485
+ - ✅ Secure OAuth integration – no password sharing or manual credential handling.
486
+
487
+ ### Limitations:
488
+
489
+ - ❌ No Local Testing support (cannot test apps behind VPNs, firewalls, or localhost). If you have to do Local Testing, you would have to use a BrowserStack Local MCP server.
490
+ - ❌ Latency can be slightly higher, but nothing considerable — you generally won’t notice it in normal use.
459
491
 
492
+ ### Installation Steps:
493
+
494
+ - On VSCode (Copilot - Agent Mode): `.vscode/mcp.json`:
495
+
496
+ - Locate or Create the Configuration File:
497
+ - In the root directory of your project, look for a folder named .vscode. This folder is usually hidden so you will need to find it as mentioned in the expand.
498
+ - If this folder doesn't exist, create it.
499
+ - Inside the .vscode folder, create a new file named mcp.json
500
+ - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content :
501
+ <div align="center">
502
+ <img src="assets/remotemcp_json_file.png" alt="Remote MCP JSON file" height="300" width="300">
503
+ </div>
504
+
505
+ ### Alternative way to Setup Remote MCP
506
+
507
+ - Step 1.Click on the gear icon to Select Tools
508
+
509
+ <div align="center">
510
+ <img src="assets/select_tools.png" alt="Select Tools" height="300" width="300">
511
+ </div>
512
+
513
+ - Step 2. A tool menu would appear at the top-centre, scroll down on the menu at the top and then Click on Add MCP Server
514
+
515
+ <div align="center">
516
+ <img src="assets/add_mcp_server.png" alt="Add MCP Server" height="300" width="300">
517
+ </div>
518
+
519
+ - Step 3. Click on HTTP option
520
+ <div align="center">
521
+ <img src="assets/http_option.png" alt="HTTP Option" height="300" width="300">
522
+ </div>
523
+
524
+ - Step 4. Paste Remote MCP Server URL : https://mcp.browserstack.com/mcp
525
+ <div align="center">
526
+ <img src="assets/server_url.png" alt="Remote MCP Server URL" height="300" width="300">
527
+ </div>
528
+
529
+ - Step 5. Give server id as : browserstack
530
+
531
+ <div align="center">
532
+ <img src="assets/server_id.png" alt="Remote MCP Server ID" height="300" width="300">
533
+ </div>
534
+
535
+ - Step 6. In VSCode Click on start MCP Server and then click on "Allow"
536
+
537
+ <div align="center">
538
+ <img src="assets/authentication1.png" alt="authentication1" height="300" width="300">
539
+ </div>
540
+
541
+ <div align="center">
542
+ <img src="assets/authentication2.png" alt="authentication2" height="300" width="300">
543
+ </div>
544
+
545
+ <div align="center">
546
+ <img src="assets/signin_success.png" alt="Sign_in_success" height="300" width="300">
547
+ </div>
548
+
549
+
460
550
 
461
551
  ## 🤝 Recommended MCP Clients
462
552
 
@@ -4,6 +4,7 @@ type RequestOptions = {
4
4
  headers?: Record<string, string>;
5
5
  params?: Record<string, string | number>;
6
6
  body?: any;
7
+ timeout?: number;
7
8
  raise_error?: boolean;
8
9
  };
9
10
  declare class ApiResponse<T = any> {
@@ -20,12 +21,13 @@ declare class ApiResponse<T = any> {
20
21
  declare class ApiClient {
21
22
  private instance;
22
23
  private get axiosAgent();
24
+ private validateUrl;
23
25
  private requestWrapper;
24
- get<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
25
- post<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
26
- put<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
27
- patch<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
28
- delete<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
26
+ get<T = any>({ url, headers, params, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
27
+ post<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
28
+ put<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
29
+ patch<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
30
+ delete<T = any>({ url, headers, params, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
29
31
  }
30
32
  export declare const apiClient: ApiClient;
31
33
  export type { ApiResponse, RequestOptions };
@@ -4,6 +4,7 @@ const { HttpsProxyAgent } = httpsProxyAgentPkg;
4
4
  import * as https from "https";
5
5
  import * as fs from "fs";
6
6
  import config from "../config.js";
7
+ import { isDataUrlPayloadTooLarge } from "../lib/utils.js";
7
8
  class ApiResponse {
8
9
  _response;
9
10
  constructor(response) {
@@ -77,8 +78,38 @@ class ApiClient {
77
78
  get axiosAgent() {
78
79
  return getAxiosAgent();
79
80
  }
80
- async requestWrapper(fn, raise_error = true) {
81
+ validateUrl(url, options) {
81
82
  try {
83
+ const parsedUrl = new URL(url);
84
+ // Default safe limits
85
+ const maxContentLength = options?.maxContentLength ?? 20 * 1024 * 1024; // 20MB
86
+ const maxBodyLength = options?.maxBodyLength ?? 20 * 1024 * 1024; // 20MB
87
+ const maxUrlLength = 8000; // cutoff for URLs
88
+ // Check overall URL length
89
+ if (url.length > maxUrlLength) {
90
+ throw new Error(`URL length exceeds maxUrlLength (${maxUrlLength} chars)`);
91
+ }
92
+ if (parsedUrl.protocol === "data:") {
93
+ // Either reject completely OR check payload size
94
+ if (isDataUrlPayloadTooLarge(url, maxContentLength)) {
95
+ throw new Error("data: URI payload too large or invalid");
96
+ }
97
+ }
98
+ else if (!["http:", "https:"].includes(parsedUrl.protocol)) {
99
+ throw new Error(`Unsupported URL scheme: ${parsedUrl.protocol}`);
100
+ }
101
+ if (options?.data &&
102
+ Buffer.byteLength(JSON.stringify(options.data), "utf8") > maxBodyLength) {
103
+ throw new Error(`Request body exceeds maxBodyLength (${maxBodyLength} bytes)`);
104
+ }
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Invalid URL: ${error.message}`);
108
+ }
109
+ }
110
+ async requestWrapper(fn, url, config, raise_error = true) {
111
+ try {
112
+ this.validateUrl(url, config);
82
113
  const res = await fn(this.axiosAgent);
83
114
  return new ApiResponse(res);
84
115
  }
@@ -89,20 +120,50 @@ class ApiClient {
89
120
  throw error;
90
121
  }
91
122
  }
92
- async get({ url, headers, params, raise_error = true, }) {
93
- return this.requestWrapper((agent) => this.instance.get(url, { headers, params, httpsAgent: agent }), raise_error);
94
- }
95
- async post({ url, headers, body, raise_error = true, }) {
96
- return this.requestWrapper((agent) => this.instance.post(url, body, { headers, httpsAgent: agent }), raise_error);
97
- }
98
- async put({ url, headers, body, raise_error = true, }) {
99
- return this.requestWrapper((agent) => this.instance.put(url, body, { headers, httpsAgent: agent }), raise_error);
100
- }
101
- async patch({ url, headers, body, raise_error = true, }) {
102
- return this.requestWrapper((agent) => this.instance.patch(url, body, { headers, httpsAgent: agent }), raise_error);
103
- }
104
- async delete({ url, headers, params, raise_error = true, }) {
105
- return this.requestWrapper((agent) => this.instance.delete(url, { headers, params, httpsAgent: agent }), raise_error);
123
+ async get({ url, headers, params, timeout, raise_error = true, }) {
124
+ const config = {
125
+ headers,
126
+ params,
127
+ timeout,
128
+ httpsAgent: this.axiosAgent,
129
+ };
130
+ return this.requestWrapper(() => this.instance.get(url, config), url, config, raise_error);
131
+ }
132
+ async post({ url, headers, body, timeout, raise_error = true, }) {
133
+ const config = {
134
+ headers,
135
+ timeout,
136
+ httpsAgent: this.axiosAgent,
137
+ data: body,
138
+ };
139
+ return this.requestWrapper(() => this.instance.post(url, config.data, config), url, config, raise_error);
140
+ }
141
+ async put({ url, headers, body, timeout, raise_error = true, }) {
142
+ const config = {
143
+ headers,
144
+ timeout,
145
+ httpsAgent: this.axiosAgent,
146
+ data: body,
147
+ };
148
+ return this.requestWrapper(() => this.instance.put(url, config.data, config), url, config, raise_error);
149
+ }
150
+ async patch({ url, headers, body, timeout, raise_error = true, }) {
151
+ const config = {
152
+ headers,
153
+ timeout,
154
+ httpsAgent: this.axiosAgent,
155
+ data: body,
156
+ };
157
+ return this.requestWrapper(() => this.instance.patch(url, config.data, config), url, config, raise_error);
158
+ }
159
+ async delete({ url, headers, params, timeout, raise_error = true, }) {
160
+ const config = {
161
+ headers,
162
+ params,
163
+ timeout,
164
+ httpsAgent: this.axiosAgent,
165
+ };
166
+ return this.requestWrapper(() => this.instance.delete(url, config), url, config, raise_error);
106
167
  }
107
168
  }
108
169
  export const apiClient = new ApiClient();
@@ -1,7 +1,9 @@
1
1
  export declare enum BrowserStackProducts {
2
2
  LIVE = "live",
3
3
  APP_LIVE = "app_live",
4
- APP_AUTOMATE = "app_automate"
4
+ APP_AUTOMATE = "app_automate",
5
+ SELENIUM_AUTOMATE = "selenium_automate",
6
+ PLAYWRIGHT_AUTOMATE = "playwright_automate"
5
7
  }
6
8
  /**
7
9
  * Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
@@ -12,11 +12,15 @@ export var BrowserStackProducts;
12
12
  BrowserStackProducts["LIVE"] = "live";
13
13
  BrowserStackProducts["APP_LIVE"] = "app_live";
14
14
  BrowserStackProducts["APP_AUTOMATE"] = "app_automate";
15
+ BrowserStackProducts["SELENIUM_AUTOMATE"] = "selenium_automate";
16
+ BrowserStackProducts["PLAYWRIGHT_AUTOMATE"] = "playwright_automate";
15
17
  })(BrowserStackProducts || (BrowserStackProducts = {}));
16
18
  const URLS = {
17
19
  [BrowserStackProducts.LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
18
20
  [BrowserStackProducts.APP_LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
19
21
  [BrowserStackProducts.APP_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_automate.json",
22
+ [BrowserStackProducts.SELENIUM_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/automate.json",
23
+ [BrowserStackProducts.PLAYWRIGHT_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json",
20
24
  };
21
25
  /**
22
26
  * Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
@@ -26,30 +30,33 @@ export async function getDevicesAndBrowsers(type) {
26
30
  fs.mkdirSync(CACHE_DIR, { recursive: true });
27
31
  }
28
32
  let cache = {};
33
+ // Load existing cache
29
34
  if (fs.existsSync(CACHE_FILE)) {
30
- const stats = fs.statSync(CACHE_FILE);
31
- if (Date.now() - stats.mtimeMs < TTL_MS) {
32
- try {
33
- cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
34
- if (cache[type]) {
35
- return cache[type];
36
- }
37
- }
38
- catch (error) {
39
- console.error("Error parsing cache file:", error);
40
- // Continue with fetching fresh data
41
- }
35
+ try {
36
+ cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
37
+ }
38
+ catch (err) {
39
+ console.error("Error parsing cache file:", err);
40
+ cache = {};
41
+ }
42
+ // Check per-product TTL
43
+ const cachedEntry = cache[type];
44
+ if (cachedEntry?.timestamp && Date.now() - cachedEntry.timestamp < TTL_MS) {
45
+ return cachedEntry.data;
42
46
  }
43
47
  }
48
+ // Fetch fresh data from BrowserStack
44
49
  const liveRes = await apiClient.get({ url: URLS[type], raise_error: false });
45
50
  if (!liveRes.ok) {
46
- throw new Error(`Failed to fetch configuration from BrowserStack : ${type}=${liveRes.statusText}`);
51
+ throw new Error(`Failed to fetch configuration from BrowserStack: ${type} = ${liveRes.statusText}`);
47
52
  }
48
- cache = {
49
- [type]: liveRes.data,
53
+ // Save to cache with timestamp and data directly under product key
54
+ cache[type] = {
55
+ timestamp: Date.now(),
56
+ data: liveRes.data,
50
57
  };
51
- fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
52
- return cache[type];
58
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
59
+ return liveRes.data;
53
60
  }
54
61
  // Rate limiter for started event (3H)
55
62
  export function shouldSendStartedEvent() {
@@ -1 +1,2 @@
1
1
  export declare const signedUrlMap: Map<string, object>;
2
+ export declare const testFilePathsMap: Map<string, string[]>;
@@ -1 +1,2 @@
1
1
  export const signedUrlMap = new Map();
2
+ export const testFilePathsMap = new Map();
@@ -3,7 +3,7 @@ import { getBrowserStackAuth } from "./get-auth.js";
3
3
  import { createRequire } from "module";
4
4
  const require = createRequire(import.meta.url);
5
5
  const packageJson = require("../../package.json");
6
- import axios from "axios";
6
+ import { apiClient } from "./apiClient.js";
7
7
  import globalConfig from "../config.js";
8
8
  export function trackMCP(toolName, clientInfo, error, config) {
9
9
  const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
@@ -38,13 +38,16 @@ export function trackMCP(toolName, clientInfo, error, config) {
38
38
  const authString = getBrowserStackAuth(config);
39
39
  authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
40
40
  }
41
- axios
42
- .post(instrumentationEndpoint, event, {
41
+ apiClient
42
+ .post({
43
+ url: instrumentationEndpoint,
44
+ body: event,
43
45
  headers: {
44
46
  "Content-Type": "application/json",
45
47
  ...(authHeader ? { Authorization: authHeader } : {}),
46
48
  },
47
49
  timeout: 2000,
50
+ raise_error: false,
48
51
  })
49
52
  .catch(() => { });
50
53
  }
@@ -1,4 +1,82 @@
1
1
  import type { ApiResponse } from "./apiClient.js";
2
+ import { BrowserStackConfig } from "./types.js";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
4
  export declare function sanitizeUrlParam(param: string): string;
3
5
  export declare function maybeCompressBase64(base64: string): Promise<string>;
4
6
  export declare function assertOkResponse(response: Response | ApiResponse, action: string): Promise<void>;
7
+ export declare function fetchFromBrowserStackAPI(url: string, config: BrowserStackConfig): Promise<any>;
8
+ export declare function handleMCPError(toolName: string, server: McpServer, config: BrowserStackConfig, error: unknown): {
9
+ [x: string]: unknown;
10
+ content: ({
11
+ [x: string]: unknown;
12
+ type: "text";
13
+ text: string;
14
+ _meta?: {
15
+ [x: string]: unknown;
16
+ } | undefined;
17
+ } | {
18
+ [x: string]: unknown;
19
+ type: "image";
20
+ data: string;
21
+ mimeType: string;
22
+ _meta?: {
23
+ [x: string]: unknown;
24
+ } | undefined;
25
+ } | {
26
+ [x: string]: unknown;
27
+ type: "audio";
28
+ data: string;
29
+ mimeType: string;
30
+ _meta?: {
31
+ [x: string]: unknown;
32
+ } | undefined;
33
+ } | {
34
+ [x: string]: unknown;
35
+ type: "resource_link";
36
+ name: string;
37
+ uri: string;
38
+ _meta?: {
39
+ [x: string]: unknown;
40
+ } | undefined;
41
+ mimeType?: string | undefined;
42
+ title?: string | undefined;
43
+ description?: string | undefined;
44
+ icons?: {
45
+ [x: string]: unknown;
46
+ src: string;
47
+ mimeType?: string | undefined;
48
+ sizes?: string | undefined;
49
+ }[] | undefined;
50
+ } | {
51
+ [x: string]: unknown;
52
+ type: "resource";
53
+ resource: {
54
+ [x: string]: unknown;
55
+ text: string;
56
+ uri: string;
57
+ _meta?: {
58
+ [x: string]: unknown;
59
+ } | undefined;
60
+ mimeType?: string | undefined;
61
+ } | {
62
+ [x: string]: unknown;
63
+ uri: string;
64
+ blob: string;
65
+ _meta?: {
66
+ [x: string]: unknown;
67
+ } | undefined;
68
+ mimeType?: string | undefined;
69
+ };
70
+ _meta?: {
71
+ [x: string]: unknown;
72
+ } | undefined;
73
+ })[];
74
+ _meta?: {
75
+ [x: string]: unknown;
76
+ } | undefined;
77
+ structuredContent?: {
78
+ [x: string]: unknown;
79
+ } | undefined;
80
+ isError?: boolean | undefined;
81
+ };
82
+ export declare function isDataUrlPayloadTooLarge(dataUrl: string, maxBytes: number): boolean;
package/dist/lib/utils.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import sharp from "sharp";
2
+ import { getBrowserStackAuth } from "./get-auth.js";
3
+ import { trackMCP } from "../index.js";
2
4
  export function sanitizeUrlParam(param) {
3
5
  // Remove any characters that could be used for command injection
4
6
  return param.replace(/[;&|`$(){}[\]<>]/g, "");
@@ -24,3 +26,48 @@ export async function assertOkResponse(response, action) {
24
26
  throw new Error(`Failed to fetch logs for ${action}: ${response.statusText}`);
25
27
  }
26
28
  }
29
+ export async function fetchFromBrowserStackAPI(url, config) {
30
+ const authString = getBrowserStackAuth(config);
31
+ const auth = Buffer.from(authString).toString("base64");
32
+ const res = await fetch(url, {
33
+ headers: {
34
+ Authorization: `Basic ${auth}`,
35
+ },
36
+ });
37
+ if (!res.ok) {
38
+ throw new Error(`Failed to fetch from ${url}: ${res.status} ${res.statusText}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ function errorContent(message) {
43
+ return {
44
+ content: [{ type: "text", text: message }],
45
+ isError: true,
46
+ };
47
+ }
48
+ export function handleMCPError(toolName, server, config, error) {
49
+ trackMCP(toolName, server.server.getClientVersion(), error, config);
50
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
51
+ const readableToolName = toolName.replace(/([A-Z])/g, " $1").toLowerCase();
52
+ return errorContent(`Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`);
53
+ }
54
+ export function isDataUrlPayloadTooLarge(dataUrl, maxBytes) {
55
+ const commaIndex = dataUrl.indexOf(",");
56
+ if (commaIndex === -1)
57
+ return true; // malformed
58
+ const meta = dataUrl.slice(0, commaIndex);
59
+ const payload = dataUrl.slice(commaIndex + 1);
60
+ const isBase64 = /;base64$/i.test(meta);
61
+ if (!isBase64) {
62
+ try {
63
+ const decoded = decodeURIComponent(payload);
64
+ return Buffer.byteLength(decoded, "utf8") > maxBytes;
65
+ }
66
+ catch {
67
+ return true;
68
+ }
69
+ }
70
+ const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
71
+ const decodedBytes = Math.floor((payload.length * 3) / 4) - padding;
72
+ return decodedBytes > maxBytes;
73
+ }
@@ -20,26 +20,38 @@ export function resolveVersion(requested, available) {
20
20
  const lex = uniq.slice().sort();
21
21
  return requested === "latest" ? lex[lex.length - 1] : lex[0];
22
22
  }
23
- // exact?
23
+ // exact match?
24
24
  if (uniq.includes(requested)) {
25
25
  return requested;
26
26
  }
27
- // try closest numeric
27
+ // Try major version matching (e.g., "14" matches "14.0", "14.1", etc.)
28
28
  const reqNum = parseFloat(requested);
29
- const nums = uniq
30
- .map((v) => ({ v, n: parseFloat(v) }))
31
- .filter((x) => !isNaN(x.n));
32
- if (!isNaN(reqNum) && nums.length) {
33
- let best = nums[0], bestDiff = Math.abs(nums[0].n - reqNum);
34
- for (const x of nums) {
35
- const d = Math.abs(x.n - reqNum);
36
- if (d < bestDiff) {
37
- best = x;
38
- bestDiff = d;
29
+ if (!isNaN(reqNum)) {
30
+ const majorVersionMatches = uniq.filter((v) => {
31
+ const vNum = parseFloat(v);
32
+ return !isNaN(vNum) && Math.floor(vNum) === Math.floor(reqNum);
33
+ });
34
+ if (majorVersionMatches.length > 0) {
35
+ // If multiple matches, prefer the most common format or latest
36
+ const exactMatch = majorVersionMatches.find((v) => v === `${Math.floor(reqNum)}.0`);
37
+ if (exactMatch) {
38
+ return exactMatch;
39
39
  }
40
+ // Return the first match (usually the most common format)
41
+ return majorVersionMatches[0];
40
42
  }
41
- return best.v;
42
43
  }
43
- // final fallback
44
+ // Fuzzy matching: find the closest version
45
+ const reqNumForFuzzy = parseFloat(requested);
46
+ if (!isNaN(reqNumForFuzzy)) {
47
+ const numericVersions = uniq
48
+ .map((v) => ({ v, n: parseFloat(v) }))
49
+ .filter((x) => !isNaN(x.n))
50
+ .sort((a, b) => Math.abs(a.n - reqNumForFuzzy) - Math.abs(b.n - reqNumForFuzzy));
51
+ if (numericVersions.length > 0) {
52
+ return numericVersions[0].v;
53
+ }
54
+ }
55
+ // Fallback: return the first available version
44
56
  return uniq[0];
45
57
  }
@@ -4,6 +4,7 @@ const require = createRequire(import.meta.url);
4
4
  const packageJson = require("../package.json");
5
5
  import logger from "./logger.js";
6
6
  import addSDKTools from "./tools/bstack-sdk.js";
7
+ import addPercyTools from "./tools/percy-sdk.js";
7
8
  import addBrowserLiveTools from "./tools/live.js";
8
9
  import addAccessibilityTools from "./tools/accessibility.js";
9
10
  import addTestManagementTools from "./tools/testmanagement.js";
@@ -12,7 +13,9 @@ import addFailureLogsTools from "./tools/get-failure-logs.js";
12
13
  import addAutomateTools from "./tools/automate.js";
13
14
  import addSelfHealTools from "./tools/selfheal.js";
14
15
  import addAppLiveTools from "./tools/applive.js";
16
+ import addBuildInsightsTools from "./tools/build-insights.js";
15
17
  import { setupOnInitialized } from "./oninitialized.js";
18
+ import addRCATools from "./tools/rca-agent.js";
16
19
  /**
17
20
  * Wrapper class for BrowserStack MCP Server
18
21
  * Stores a map of registered tools by name
@@ -38,6 +41,7 @@ export class BrowserStackMcpServer {
38
41
  const toolAdders = [
39
42
  addAccessibilityTools,
40
43
  addSDKTools,
44
+ addPercyTools,
41
45
  addAppLiveTools,
42
46
  addBrowserLiveTools,
43
47
  addTestManagementTools,
@@ -45,6 +49,8 @@ export class BrowserStackMcpServer {
45
49
  addFailureLogsTools,
46
50
  addAutomateTools,
47
51
  addSelfHealTools,
52
+ addBuildInsightsTools,
53
+ addRCATools,
48
54
  ];
49
55
  toolAdders.forEach((adder) => {
50
56
  // Each adder now returns a Record<string, Tool>
@@ -0,0 +1,5 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare function updateTestsWithPercyCommands(args: {
3
+ uuid: string;
4
+ index: number;
5
+ }): Promise<CallToolResult>;
@@ -0,0 +1,17 @@
1
+ import { testFilePathsMap } from "../lib/inmemory-store.js";
2
+ import { updateFileAndStep } from "./percy-snapshot-utils/utils.js";
3
+ import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js";
4
+ export async function updateTestsWithPercyCommands(args) {
5
+ const { uuid, index } = args;
6
+ const filePaths = testFilePathsMap.get(uuid);
7
+ if (!filePaths) {
8
+ throw new Error(`No test files found in memory for UUID: ${uuid}`);
9
+ }
10
+ if (index < 0 || index >= filePaths.length) {
11
+ throw new Error(`Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`);
12
+ }
13
+ const result = await updateFileAndStep(filePaths[index], index, filePaths.length, percyWebSetupInstructions);
14
+ return {
15
+ content: result,
16
+ };
17
+ }