@browserstack/mcp-server 1.2.4 → 1.2.6-beta.1

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 (64) hide show
  1. package/README.md +9 -5
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/lib/apiClient.d.ts +8 -5
  5. package/dist/lib/apiClient.js +77 -15
  6. package/dist/lib/device-cache.d.ts +3 -1
  7. package/dist/lib/device-cache.js +4 -0
  8. package/dist/lib/inmemory-store.d.ts +5 -1
  9. package/dist/lib/inmemory-store.js +10 -1
  10. package/dist/lib/instrumentation.js +6 -3
  11. package/dist/lib/utils.d.ts +75 -2
  12. package/dist/lib/utils.js +20 -0
  13. package/dist/lib/version-resolver.js +30 -14
  14. package/dist/tools/add-percy-snapshots.d.ts +0 -1
  15. package/dist/tools/add-percy-snapshots.js +11 -6
  16. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -1
  17. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +46 -26
  18. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
  19. package/dist/tools/appautomate-utils/appium-sdk/constants.js +24 -3
  20. package/dist/tools/appautomate-utils/appium-sdk/handler.js +16 -2
  21. package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +2 -0
  22. package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +63 -29
  23. package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -1
  24. package/dist/tools/appautomate-utils/appium-sdk/types.js +10 -1
  25. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
  26. package/dist/tools/appautomate-utils/native-execution/constants.js +24 -2
  27. package/dist/tools/appautomate.js +15 -2
  28. package/dist/tools/automate-utils/fetch-screenshots.js +4 -1
  29. package/dist/tools/list-test-files.d.ts +1 -1
  30. package/dist/tools/list-test-files.js +43 -19
  31. package/dist/tools/live-utils/start-session.js +1 -1
  32. package/dist/tools/percy-sdk.js +33 -6
  33. package/dist/tools/percy-snapshot-utils/constants.d.ts +0 -6
  34. package/dist/tools/percy-snapshot-utils/constants.js +0 -15
  35. package/dist/tools/rca-agent-utils/constants.d.ts +1 -1
  36. package/dist/tools/rca-agent-utils/constants.js +2 -2
  37. package/dist/tools/rca-agent-utils/rca-data.d.ts +1 -1
  38. package/dist/tools/rca-agent-utils/rca-data.js +2 -2
  39. package/dist/tools/rca-agent-utils/types.d.ts +3 -3
  40. package/dist/tools/rca-agent.d.ts +1 -1
  41. package/dist/tools/run-percy-scan.js +51 -10
  42. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +8 -4
  43. package/dist/tools/sdk-utils/bstack/configUtils.js +74 -20
  44. package/dist/tools/sdk-utils/bstack/constants.d.ts +1 -1
  45. package/dist/tools/sdk-utils/bstack/constants.js +7 -9
  46. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +1 -1
  47. package/dist/tools/sdk-utils/bstack/sdkHandler.js +19 -9
  48. package/dist/tools/sdk-utils/common/constants.d.ts +6 -5
  49. package/dist/tools/sdk-utils/common/constants.js +8 -7
  50. package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
  51. package/dist/tools/sdk-utils/common/device-validator.js +375 -0
  52. package/dist/tools/sdk-utils/common/schema.d.ts +32 -8
  53. package/dist/tools/sdk-utils/common/schema.js +62 -3
  54. package/dist/tools/sdk-utils/common/utils.d.ts +1 -1
  55. package/dist/tools/sdk-utils/common/utils.js +14 -2
  56. package/dist/tools/sdk-utils/handler.d.ts +1 -0
  57. package/dist/tools/sdk-utils/handler.js +59 -14
  58. package/dist/tools/sdk-utils/percy-automate/constants.d.ts +4 -4
  59. package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -4
  60. package/dist/tools/sdk-utils/percy-bstack/handler.js +5 -1
  61. package/dist/tools/sdk-utils/percy-web/constants.d.ts +22 -20
  62. package/dist/tools/sdk-utils/percy-web/constants.js +39 -0
  63. package/dist/tools/sdk-utils/percy-web/handler.js +3 -1
  64. package/package.json +2 -2
package/README.md CHANGED
@@ -35,9 +35,12 @@ 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
+
40
+ Click on the buttons below to install MCP in your respective IDE:
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>
39
43
 
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) &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)
41
44
  #### Note : Ensure you are using Node version >= `18.0`
42
45
  - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
43
46
  - To Upgrade Node :
@@ -156,8 +159,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
156
159
 
157
160
  ### **One Click MCP Setup**
158
161
 
159
- [![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:
160
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>
161
165
 
162
166
  ### **Alternate ways to Setup MCP server**
163
167
 
@@ -418,14 +422,14 @@ As of now we support 20 tools.
418
422
  **Prompt example**
419
423
 
420
424
  ```text
421
- 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
422
426
  ```
423
427
 
424
428
  15. `runAppTestsOnBrowserStack` — Run automated mobile tests (Espresso/XCUITest, etc.) on real devices.
425
429
  **Prompt example**
426
430
 
427
431
  ```text
428
- 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'
429
433
  ```
430
434
 
431
435
  ---
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ import "dotenv/config";
3
3
  export { setLogger } from "./logger.js";
4
4
  export { BrowserStackMcpServer } from "./server-factory.js";
5
5
  export { trackMCP } from "./lib/instrumentation.js";
6
+ export declare const PackageJsonVersion: any;
package/dist/index.js CHANGED
@@ -36,3 +36,4 @@ process.on("exit", () => {
36
36
  export { setLogger } from "./logger.js";
37
37
  export { BrowserStackMcpServer } from "./server-factory.js";
38
38
  export { trackMCP } from "./lib/instrumentation.js";
39
+ export const PackageJsonVersion = packageJson.version;
@@ -4,6 +4,8 @@ type RequestOptions = {
4
4
  headers?: Record<string, string>;
5
5
  params?: Record<string, string | number>;
6
6
  body?: any;
7
+ timeout?: number;
8
+ responseType?: AxiosRequestConfig["responseType"];
7
9
  raise_error?: boolean;
8
10
  };
9
11
  declare class ApiResponse<T = any> {
@@ -20,12 +22,13 @@ declare class ApiResponse<T = any> {
20
22
  declare class ApiClient {
21
23
  private instance;
22
24
  private get axiosAgent();
25
+ private validateUrl;
23
26
  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>>;
27
+ get<T = any>({ url, headers, params, timeout, responseType, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
28
+ post<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
29
+ put<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
30
+ patch<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
31
+ delete<T = any>({ url, headers, params, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
29
32
  }
30
33
  export declare const apiClient: ApiClient;
31
34
  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,51 @@ 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, responseType, raise_error = true, }) {
124
+ const config = {
125
+ headers,
126
+ params,
127
+ timeout,
128
+ responseType,
129
+ httpsAgent: this.axiosAgent,
130
+ };
131
+ return this.requestWrapper(() => this.instance.get(url, config), url, config, raise_error);
132
+ }
133
+ async post({ url, headers, body, timeout, raise_error = true, }) {
134
+ const config = {
135
+ headers,
136
+ timeout,
137
+ httpsAgent: this.axiosAgent,
138
+ data: body,
139
+ };
140
+ return this.requestWrapper(() => this.instance.post(url, config.data, config), url, config, raise_error);
141
+ }
142
+ async put({ url, headers, body, timeout, raise_error = true, }) {
143
+ const config = {
144
+ headers,
145
+ timeout,
146
+ httpsAgent: this.axiosAgent,
147
+ data: body,
148
+ };
149
+ return this.requestWrapper(() => this.instance.put(url, config.data, config), url, config, raise_error);
150
+ }
151
+ async patch({ url, headers, body, timeout, raise_error = true, }) {
152
+ const config = {
153
+ headers,
154
+ timeout,
155
+ httpsAgent: this.axiosAgent,
156
+ data: body,
157
+ };
158
+ return this.requestWrapper(() => this.instance.patch(url, config.data, config), url, config, raise_error);
159
+ }
160
+ async delete({ url, headers, params, timeout, raise_error = true, }) {
161
+ const config = {
162
+ headers,
163
+ params,
164
+ timeout,
165
+ httpsAgent: this.axiosAgent,
166
+ };
167
+ return this.requestWrapper(() => this.instance.delete(url, config), url, config, raise_error);
106
168
  }
107
169
  }
108
170
  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.
@@ -1,2 +1,6 @@
1
1
  export declare const signedUrlMap: Map<string, object>;
2
- export declare const testFilePathsMap: Map<string, string[]>;
2
+ export declare const storedPercyResults: {
3
+ get: () => any;
4
+ set: (value: any) => void;
5
+ clear: () => void;
6
+ };
@@ -1,2 +1,11 @@
1
1
  export const signedUrlMap = new Map();
2
- export const testFilePathsMap = new Map();
2
+ let _storedPercyResults = null;
3
+ export const storedPercyResults = {
4
+ get: () => _storedPercyResults,
5
+ set: (value) => {
6
+ _storedPercyResults = value;
7
+ },
8
+ clear: () => {
9
+ _storedPercyResults = null;
10
+ },
11
+ };
@@ -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,9 +1,82 @@
1
1
  import type { ApiResponse } from "./apiClient.js";
2
2
  import { BrowserStackConfig } from "./types.js";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5
4
  export declare function sanitizeUrlParam(param: string): string;
6
5
  export declare function maybeCompressBase64(base64: string): Promise<string>;
7
6
  export declare function assertOkResponse(response: Response | ApiResponse, action: string): Promise<void>;
8
7
  export declare function fetchFromBrowserStackAPI(url: string, config: BrowserStackConfig): Promise<any>;
9
- export declare function handleMCPError(toolName: string, server: McpServer, config: BrowserStackConfig, error: unknown): CallToolResult;
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
@@ -51,3 +51,23 @@ export function handleMCPError(toolName, server, config, error) {
51
51
  const readableToolName = toolName.replace(/([A-Z])/g, " $1").toLowerCase();
52
52
  return errorContent(`Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`);
53
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,42 @@ 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
+ const caseInsensitiveMatch = uniq.find((v) => v.toLowerCase() === requested.toLowerCase());
28
+ if (caseInsensitiveMatch) {
29
+ return caseInsensitiveMatch;
30
+ }
31
+ // Try major version matching (e.g., "14" matches "14.0", "14.1", etc.)
28
32
  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;
33
+ if (!isNaN(reqNum)) {
34
+ const majorVersionMatches = uniq.filter((v) => {
35
+ const vNum = parseFloat(v);
36
+ return !isNaN(vNum) && Math.floor(vNum) === Math.floor(reqNum);
37
+ });
38
+ if (majorVersionMatches.length > 0) {
39
+ // If multiple matches, prefer the most common format or latest
40
+ const exactMatch = majorVersionMatches.find((v) => v === `${Math.floor(reqNum)}.0`);
41
+ if (exactMatch) {
42
+ return exactMatch;
39
43
  }
44
+ // Return the first match (usually the most common format)
45
+ return majorVersionMatches[0];
46
+ }
47
+ }
48
+ // Fuzzy matching: find the closest version
49
+ const reqNumForFuzzy = parseFloat(requested);
50
+ if (!isNaN(reqNumForFuzzy)) {
51
+ const numericVersions = uniq
52
+ .map((v) => ({ v, n: parseFloat(v) }))
53
+ .filter((x) => !isNaN(x.n))
54
+ .sort((a, b) => Math.abs(a.n - reqNumForFuzzy) - Math.abs(b.n - reqNumForFuzzy));
55
+ if (numericVersions.length > 0) {
56
+ return numericVersions[0].v;
40
57
  }
41
- return best.v;
42
58
  }
43
- // final fallback
59
+ // Fallback: return the first available version
44
60
  return uniq[0];
45
61
  }
@@ -1,5 +1,4 @@
1
1
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  export declare function updateTestsWithPercyCommands(args: {
3
- uuid: string;
4
3
  index: number;
5
4
  }): Promise<CallToolResult>;
@@ -1,16 +1,21 @@
1
- import { testFilePathsMap } from "../lib/inmemory-store.js";
1
+ import { storedPercyResults } from "../lib/inmemory-store.js";
2
2
  import { updateFileAndStep } from "./percy-snapshot-utils/utils.js";
3
3
  import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js";
4
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}`);
5
+ const { index } = args;
6
+ const stored = storedPercyResults.get();
7
+ if (!stored || !stored.testFiles) {
8
+ throw new Error(`No test files found in memory. Please call listTestFiles first.`);
9
9
  }
10
+ const fileStatusMap = stored.testFiles;
11
+ const filePaths = Object.keys(fileStatusMap);
10
12
  if (index < 0 || index >= filePaths.length) {
11
- throw new Error(`Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`);
13
+ throw new Error(`Invalid index: ${index}. There are ${filePaths.length} files available.`);
12
14
  }
13
15
  const result = await updateFileAndStep(filePaths[index], index, filePaths.length, percyWebSetupInstructions);
16
+ const updatedStored = { ...stored };
17
+ updatedStored.testFiles[filePaths[index]] = true; // true = updated
18
+ storedPercyResults.set(updatedStored);
14
19
  return {
15
20
  content: result,
16
21
  };
@@ -1 +1,7 @@
1
- export declare function generateAppBrowserStackYMLInstructions(platforms: string[], username: string, accessKey: string, appPath: string | undefined, testingFramework: string): string;
1
+ import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js";
2
+ export declare function generateAppBrowserStackYMLInstructions(config: {
3
+ validatedEnvironments?: ValidatedEnvironment[];
4
+ platforms?: string[];
5
+ testingFramework?: string;
6
+ projectName?: string;
7
+ }, username: string, accessKey: string, appPath?: string): string;
@@ -1,26 +1,17 @@
1
- // Configuration utilities for BrowserStack App SDK
2
1
  import { APP_DEVICE_CONFIGS, AppSDKSupportedTestingFrameworkEnum, DEFAULT_APP_PATH, createStep, } from "./index.js";
3
- export function generateAppBrowserStackYMLInstructions(platforms, username, accessKey, appPath = DEFAULT_APP_PATH, testingFramework) {
4
- if (testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch ||
5
- testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio ||
6
- testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby) {
2
+ export function generateAppBrowserStackYMLInstructions(config, username, accessKey, appPath = DEFAULT_APP_PATH) {
3
+ if (config.testingFramework ===
4
+ AppSDKSupportedTestingFrameworkEnum.nightwatch ||
5
+ config.testingFramework ===
6
+ AppSDKSupportedTestingFrameworkEnum.webdriverio ||
7
+ config.testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby) {
7
8
  return "";
8
9
  }
9
- // Generate platform and device configurations
10
- const platformConfigs = platforms
11
- .map((platform) => {
12
- const devices = APP_DEVICE_CONFIGS[platform];
13
- if (!devices)
14
- return "";
15
- return devices
16
- .map((device) => ` - platformName: ${platform}
17
- deviceName: ${device.deviceName}
18
- platformVersion: "${device.platformVersion}"`)
19
- .join("\n");
20
- })
21
- .filter(Boolean)
22
- .join("\n");
23
- // Construct YAML content
10
+ const platformConfigs = generatePlatformConfigs(config);
11
+ const projectName = config.projectName || "BrowserStack Sample";
12
+ const buildName = config.projectName
13
+ ? `${config.projectName}-AppAutomate-Build`
14
+ : "bstack-demo";
24
15
  const configContent = `\`\`\`yaml
25
16
  userName: ${username}
26
17
  accessKey: ${accessKey}
@@ -29,8 +20,9 @@ platforms:
29
20
  ${platformConfigs}
30
21
  parallelsPerPlatform: 1
31
22
  browserstackLocal: true
32
- buildName: bstack-demo
33
- projectName: BrowserStack Sample
23
+ // TODO: replace projectName and buildName according to actual project
24
+ projectName: ${projectName}
25
+ buildName: ${buildName}
34
26
  debug: true
35
27
  networkLogs: true
36
28
  percy: false
@@ -43,8 +35,36 @@ accessibility: false
43
35
  - You can upload your app using BrowserStack's App Upload API or manually through the dashboard
44
36
  - Set \`browserstackLocal: true\` if you need to test with local/staging servers
45
37
  - Adjust \`parallelsPerPlatform\` based on your subscription limits`;
46
- // Return formatted step for instructions
47
- return createStep("Update browserstack.yml file with App Automate configuration:", `Create or update the browserstack.yml file in your project root with the following content:
48
-
49
- ${configContent}`);
38
+ const stepTitle = "Update browserstack.yml file with App Automate configuration:";
39
+ const stepDescription = `Create or update the browserstack.yml file in your project root with the following content:
40
+ ${configContent}`;
41
+ return createStep(stepTitle, stepDescription);
42
+ }
43
+ function generatePlatformConfigs(config) {
44
+ if (config.validatedEnvironments && config.validatedEnvironments.length > 0) {
45
+ return config.validatedEnvironments
46
+ .filter((env) => env.platform === "android" || env.platform === "ios")
47
+ .map((env) => {
48
+ return ` - platformName: ${env.platform}
49
+ deviceName: "${env.deviceName}"
50
+ platformVersion: "${env.osVersion}"`;
51
+ })
52
+ .join("\n");
53
+ }
54
+ else if (config.platforms && config.platforms.length > 0) {
55
+ return config.platforms
56
+ .map((platform) => {
57
+ const devices = APP_DEVICE_CONFIGS[platform];
58
+ if (!devices)
59
+ return "";
60
+ return devices
61
+ .map((device) => ` - platformName: ${platform}
62
+ deviceName: ${device.deviceName}
63
+ platformVersion: "${device.platformVersion}"`)
64
+ .join("\n");
65
+ })
66
+ .filter(Boolean)
67
+ .join("\n");
68
+ }
69
+ return "";
50
70
  }
@@ -17,7 +17,7 @@ export declare const SETUP_APP_AUTOMATE_SCHEMA: {
17
17
  detectedFramework: z.ZodNativeEnum<typeof AppSDKSupportedFrameworkEnum>;
18
18
  detectedTestingFramework: z.ZodNativeEnum<typeof AppSDKSupportedTestingFrameworkEnum>;
19
19
  detectedLanguage: z.ZodNativeEnum<typeof AppSDKSupportedLanguageEnum>;
20
- desiredPlatforms: z.ZodArray<z.ZodNativeEnum<typeof AppSDKSupportedPlatformEnum>, "many">;
20
+ devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodLiteral<AppSDKSupportedPlatformEnum.android>, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<AppSDKSupportedPlatformEnum.ios>, z.ZodString, z.ZodString], null>]>, "many">>;
21
21
  appPath: z.ZodString;
22
22
  project: z.ZodDefault<z.ZodOptional<z.ZodString>>;
23
23
  };
@@ -29,9 +29,30 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
29
29
  detectedLanguage: z
30
30
  .nativeEnum(AppSDKSupportedLanguageEnum)
31
31
  .describe("The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'"),
32
- desiredPlatforms: z
33
- .array(z.nativeEnum(AppSDKSupportedPlatformEnum))
34
- .describe("The mobile platforms the user wants to test on. Always ask this to the user, do not try to infer this. Example: ['android', 'ios']"),
32
+ devices: z
33
+ .array(z.union([
34
+ // Android: [android, deviceName, osVersion]
35
+ z.tuple([
36
+ z
37
+ .literal(AppSDKSupportedPlatformEnum.android)
38
+ .describe("Platform identifier: 'android'"),
39
+ z
40
+ .string()
41
+ .describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
42
+ z.string().describe("Android version, e.g. '14', '16', 'latest'"),
43
+ ]),
44
+ // iOS: [ios, deviceName, osVersion]
45
+ z.tuple([
46
+ z
47
+ .literal(AppSDKSupportedPlatformEnum.ios)
48
+ .describe("Platform identifier: 'ios'"),
49
+ z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
50
+ z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
51
+ ]),
52
+ ]))
53
+ .max(3)
54
+ .default([])
55
+ .describe("Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]"),
35
56
  appPath: z
36
57
  .string()
37
58
  .describe("Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)"),