@browserstack/mcp-server 1.1.9 → 1.2.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.
Files changed (123) hide show
  1. package/dist/config.d.ts +13 -0
  2. package/dist/config.js +4 -6
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.js +20 -30
  5. package/dist/lib/api.d.ts +2 -0
  6. package/dist/lib/api.js +10 -5
  7. package/dist/lib/apiClient.d.ts +31 -0
  8. package/dist/lib/apiClient.js +108 -0
  9. package/dist/lib/constants.d.ts +17 -0
  10. package/dist/lib/device-cache.d.ts +9 -0
  11. package/dist/lib/device-cache.js +3 -3
  12. package/dist/lib/error.d.ts +7 -0
  13. package/dist/lib/fuzzy.d.ts +1 -0
  14. package/dist/lib/get-auth.d.ts +2 -0
  15. package/dist/lib/get-auth.js +8 -0
  16. package/dist/lib/inmemory-store.d.ts +1 -0
  17. package/dist/lib/instrumentation.d.ts +4 -0
  18. package/dist/lib/instrumentation.js +8 -7
  19. package/dist/lib/local.d.ts +3 -0
  20. package/dist/lib/local.js +3 -3
  21. package/dist/lib/types.d.ts +4 -0
  22. package/dist/lib/types.js +1 -0
  23. package/dist/lib/utils.d.ts +4 -0
  24. package/dist/lib/version-resolver.d.ts +6 -0
  25. package/dist/logger.d.ts +3 -0
  26. package/dist/logger.js +20 -4
  27. package/dist/oninitialized.d.ts +2 -0
  28. package/dist/oninitialized.js +2 -7
  29. package/dist/server-factory.d.ts +3 -0
  30. package/dist/server-factory.js +37 -0
  31. package/dist/tools/accessibility.d.ts +3 -0
  32. package/dist/tools/accessibility.js +17 -10
  33. package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +12 -0
  34. package/dist/tools/accessiblity-utils/accessibility-rag.js +22 -12
  35. package/dist/tools/accessiblity-utils/report-fetcher.d.ts +8 -0
  36. package/dist/tools/accessiblity-utils/report-fetcher.js +26 -16
  37. package/dist/tools/accessiblity-utils/report-parser.d.ts +23 -0
  38. package/dist/tools/accessiblity-utils/report-parser.js +3 -3
  39. package/dist/tools/accessiblity-utils/scanner.d.ts +25 -0
  40. package/dist/tools/accessiblity-utils/scanner.js +43 -24
  41. package/dist/tools/appautomate-utils/appautomate.d.ts +42 -0
  42. package/dist/tools/appautomate-utils/appautomate.js +63 -38
  43. package/dist/tools/appautomate-utils/types.d.ts +5 -0
  44. package/dist/tools/appautomate.d.ts +3 -0
  45. package/dist/tools/appautomate.js +20 -19
  46. package/dist/tools/applive-utils/device-search.d.ts +6 -0
  47. package/dist/tools/applive-utils/start-session.d.ts +15 -0
  48. package/dist/tools/applive-utils/start-session.js +11 -4
  49. package/dist/tools/applive-utils/types.d.ts +7 -0
  50. package/dist/tools/applive-utils/upload-app.d.ts +5 -0
  51. package/dist/tools/applive-utils/upload-app.js +8 -12
  52. package/dist/tools/applive-utils/version-utils.d.ts +4 -0
  53. package/dist/tools/applive.d.ts +13 -0
  54. package/dist/tools/applive.js +6 -6
  55. package/dist/tools/automate-utils/fetch-screenshots.d.ts +6 -0
  56. package/dist/tools/automate-utils/fetch-screenshots.js +16 -12
  57. package/dist/tools/automate.d.ts +9 -0
  58. package/dist/tools/automate.js +6 -6
  59. package/dist/tools/bstack-sdk.d.ts +17 -0
  60. package/dist/tools/bstack-sdk.js +12 -7
  61. package/dist/tools/failurelogs-utils/app-automate.d.ts +7 -0
  62. package/dist/tools/failurelogs-utils/app-automate.js +29 -11
  63. package/dist/tools/failurelogs-utils/automate.d.ts +6 -0
  64. package/dist/tools/failurelogs-utils/automate.js +27 -12
  65. package/dist/tools/failurelogs-utils/utils.d.ts +30 -0
  66. package/dist/tools/getFailureLogs.d.ts +14 -0
  67. package/dist/tools/getFailureLogs.js +11 -11
  68. package/dist/tools/live-utils/desktop-filter.d.ts +2 -0
  69. package/dist/tools/live-utils/mobile-filter.d.ts +2 -0
  70. package/dist/tools/live-utils/start-session.d.ts +6 -0
  71. package/dist/tools/live-utils/start-session.js +18 -5
  72. package/dist/tools/live-utils/types.d.ts +33 -0
  73. package/dist/tools/live.d.ts +3 -0
  74. package/dist/tools/live.js +11 -11
  75. package/dist/tools/observability.d.ts +5 -0
  76. package/dist/tools/observability.js +14 -11
  77. package/dist/tools/sdk-utils/commands.d.ts +3 -0
  78. package/dist/tools/sdk-utils/commands.js +5 -4
  79. package/dist/tools/sdk-utils/constants.d.ts +2 -0
  80. package/dist/tools/sdk-utils/constants.js +82 -21
  81. package/dist/tools/sdk-utils/instructions.d.ts +6 -0
  82. package/dist/tools/sdk-utils/instructions.js +8 -6
  83. package/dist/tools/sdk-utils/percy/constants.d.ts +3 -0
  84. package/dist/tools/sdk-utils/percy/constants.js +1 -0
  85. package/dist/tools/sdk-utils/percy/instructions.d.ts +10 -0
  86. package/dist/tools/sdk-utils/percy/types.d.ts +5 -0
  87. package/dist/tools/sdk-utils/types.d.ts +39 -0
  88. package/dist/tools/sdk-utils/types.js +1 -0
  89. package/dist/tools/selfheal-utils/selfheal.d.ts +11 -0
  90. package/dist/tools/selfheal-utils/selfheal.js +10 -6
  91. package/dist/tools/selfheal.d.ts +7 -0
  92. package/dist/tools/selfheal.js +6 -6
  93. package/dist/tools/testmanagement-utils/TCG-utils/api.d.ts +34 -0
  94. package/dist/tools/testmanagement-utils/TCG-utils/api.js +57 -44
  95. package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +5 -0
  96. package/dist/tools/testmanagement-utils/TCG-utils/helpers.d.ts +13 -0
  97. package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +2 -1
  98. package/dist/tools/testmanagement-utils/TCG-utils/types.d.ts +26 -0
  99. package/dist/tools/testmanagement-utils/add-test-result.d.ts +42 -0
  100. package/dist/tools/testmanagement-utils/add-test-result.js +23 -10
  101. package/dist/tools/testmanagement-utils/create-lca-steps.d.ts +100 -0
  102. package/dist/tools/testmanagement-utils/create-lca-steps.js +64 -55
  103. package/dist/tools/testmanagement-utils/create-project-folder.d.ts +31 -0
  104. package/dist/tools/testmanagement-utils/create-project-folder.js +31 -21
  105. package/dist/tools/testmanagement-utils/create-testcase.d.ts +122 -0
  106. package/dist/tools/testmanagement-utils/create-testcase.js +13 -10
  107. package/dist/tools/testmanagement-utils/create-testrun.d.ts +82 -0
  108. package/dist/tools/testmanagement-utils/create-testrun.js +11 -8
  109. package/dist/tools/testmanagement-utils/list-testcases.d.ts +30 -0
  110. package/dist/tools/testmanagement-utils/list-testcases.js +9 -7
  111. package/dist/tools/testmanagement-utils/list-testruns.d.ts +22 -0
  112. package/dist/tools/testmanagement-utils/list-testruns.js +9 -7
  113. package/dist/tools/testmanagement-utils/poll-lca-status.d.ts +11 -0
  114. package/dist/tools/testmanagement-utils/poll-lca-status.js +12 -8
  115. package/dist/tools/testmanagement-utils/testcase-from-file.d.ts +4 -0
  116. package/dist/tools/testmanagement-utils/testcase-from-file.js +6 -6
  117. package/dist/tools/testmanagement-utils/update-testrun.d.ts +40 -0
  118. package/dist/tools/testmanagement-utils/update-testrun.js +11 -7
  119. package/dist/tools/testmanagement-utils/upload-file.d.ts +20 -0
  120. package/dist/tools/testmanagement-utils/upload-file.js +8 -6
  121. package/dist/tools/testmanagement.d.ts +60 -0
  122. package/dist/tools/testmanagement.js +51 -53
  123. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ export interface RAGChunk {
2
+ url: string;
3
+ content: string;
4
+ }
5
+ import { BrowserStackConfig } from "../../lib/types.js";
6
+ export interface AccessibilityRAGResponse {
7
+ content: Array<{
8
+ type: "text";
9
+ text: string;
10
+ }>;
11
+ }
12
+ export declare function queryAccessibilityRAG(userQuery: string, config: BrowserStackConfig): Promise<any>;
@@ -1,23 +1,27 @@
1
- import fetch from "node-fetch";
2
- import config from "../../config.js";
3
- export async function queryAccessibilityRAG(userQuery) {
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
3
+ export async function queryAccessibilityRAG(userQuery, config) {
4
4
  const url = "https://accessibility.browserstack.com/api/tcg-proxy/search";
5
- const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
6
- const response = await fetch(url, {
7
- method: "POST",
5
+ const authString = getBrowserStackAuth(config);
6
+ const auth = Buffer.from(authString).toString("base64");
7
+ const response = await apiClient.post({
8
+ url,
8
9
  headers: {
9
10
  "Content-Type": "application/json",
10
11
  Authorization: `Basic ${auth}`,
11
12
  },
12
- body: JSON.stringify({
13
+ body: {
13
14
  query: userQuery,
14
- }),
15
+ },
16
+ raise_error: false,
15
17
  });
16
18
  if (!response.ok) {
17
- const errorText = await response.text();
19
+ const errorText = typeof response.data === "string"
20
+ ? response.data
21
+ : JSON.stringify(response.data);
18
22
  throw new Error(`RAG endpoint error: ${response.status} ${errorText}`);
19
23
  }
20
- const responseData = (await response.json());
24
+ const responseData = response.data;
21
25
  if (!responseData.success) {
22
26
  throw new Error("Something went wrong: " + responseData.message);
23
27
  }
@@ -26,8 +30,14 @@ export async function queryAccessibilityRAG(userQuery) {
26
30
  try {
27
31
  parsedData = JSON.parse(responseData.data);
28
32
  }
29
- catch {
30
- throw new Error("Failed to parse RAG response data as JSON");
33
+ catch (err) {
34
+ throw new Error("Failed to parse RAG response data as JSON: " +
35
+ (err instanceof Error ? err.message : String(err)));
36
+ }
37
+ if (!parsedData ||
38
+ !parsedData.data ||
39
+ !Array.isArray(parsedData.data.chunks)) {
40
+ throw new Error("RAG response data is missing expected 'data.chunks' array");
31
41
  }
32
42
  const chunks = parsedData.data.chunks;
33
43
  // Format the response properly
@@ -0,0 +1,8 @@
1
+ export declare class AccessibilityReportFetcher {
2
+ private auth;
3
+ setAuth(auth: {
4
+ username: string;
5
+ password: string;
6
+ }): void;
7
+ getReportLink(scanId: string, scanRunId: string): Promise<string>;
8
+ }
@@ -1,28 +1,38 @@
1
- import axios from "axios";
2
- import config from "../../config.js";
1
+ import { apiClient } from "../../lib/apiClient.js";
3
2
  export class AccessibilityReportFetcher {
4
- auth = {
5
- username: config.browserstackUsername,
6
- password: config.browserstackAccessKey,
7
- };
3
+ auth;
4
+ setAuth(auth) {
5
+ this.auth = auth;
6
+ }
8
7
  async getReportLink(scanId, scanRunId) {
9
8
  // Initiate CSV link generation
10
9
  const initUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?scan_run_id=${scanRunId}`;
11
- const initResp = await axios.get(initUrl, {
12
- auth: this.auth,
10
+ let basicAuthHeader = undefined;
11
+ if (this.auth) {
12
+ const { username, password } = this.auth;
13
+ basicAuthHeader =
14
+ "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
15
+ }
16
+ const initResp = await apiClient.get({
17
+ url: initUrl,
18
+ headers: basicAuthHeader ? { Authorization: basicAuthHeader } : undefined,
13
19
  });
14
- if (!initResp.data.success) {
15
- throw new Error(`Failed to initiate report: ${initResp.data.error || initResp.data.data.message}`);
20
+ const initData = initResp.data;
21
+ if (!initData.success) {
22
+ throw new Error(`Failed to initiate report: ${initData.error || initData.data.message}`);
16
23
  }
17
- const taskId = initResp.data.data.task_id;
24
+ const taskId = initData.data.task_id;
18
25
  // Fetch the generated CSV link
19
26
  const reportUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?task_id=${encodeURIComponent(taskId)}`;
20
- const reportResp = await axios.get(reportUrl, {
21
- auth: this.auth,
27
+ // Use apiClient for the report link request as well
28
+ const reportResp = await apiClient.get({
29
+ url: reportUrl,
30
+ headers: basicAuthHeader ? { Authorization: basicAuthHeader } : undefined,
22
31
  });
23
- if (!reportResp.data.success) {
24
- throw new Error(`Failed to fetch report: ${reportResp.data.error}`);
32
+ const reportData = reportResp.data;
33
+ if (!reportData.success) {
34
+ throw new Error(`Failed to fetch report: ${reportData.error}`);
25
35
  }
26
- return reportResp.data.data.reportLink;
36
+ return reportData.data.reportLink;
27
37
  }
28
38
  }
@@ -0,0 +1,23 @@
1
+ type SimplifiedAccessibilityIssue = {
2
+ issue_type: string;
3
+ component: string;
4
+ issue_description: string;
5
+ HTML_snippet: string;
6
+ how_to_fix: string;
7
+ severity: string;
8
+ };
9
+ type PaginationOptions = {
10
+ /** How many JSON-chars max per “page” (default 10000) */
11
+ maxCharacterLength?: number;
12
+ /** Character offset to start from (default 0) */
13
+ nextPage?: number;
14
+ };
15
+ type PaginatedResult = {
16
+ records: SimplifiedAccessibilityIssue[];
17
+ /** Character offset for the next page, or null if done */
18
+ page_length: number;
19
+ total_issues: number;
20
+ next_page: number | null;
21
+ };
22
+ export declare function parseAccessibilityReportFromCSV(reportLink: string, { maxCharacterLength, nextPage }?: PaginationOptions): Promise<PaginatedResult>;
23
+ export {};
@@ -1,11 +1,11 @@
1
- import fetch from "node-fetch";
1
+ import { apiClient } from "../../lib/apiClient.js";
2
2
  import { parse } from "csv-parse/sync";
3
3
  export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 5000, nextPage = 0 } = {}) {
4
4
  // 1) Download & parse
5
- const res = await fetch(reportLink);
5
+ const res = await apiClient.get({ url: reportLink });
6
6
  if (!res.ok)
7
7
  throw new Error(`Failed to download report: ${res.statusText}`);
8
- const text = await res.text();
8
+ const text = typeof res.data === "string" ? res.data : JSON.stringify(res.data);
9
9
  const all = parse(text, {
10
10
  columns: true,
11
11
  skip_empty_lines: true,
@@ -0,0 +1,25 @@
1
+ export interface AccessibilityScanResponse {
2
+ success: boolean;
3
+ data?: {
4
+ id: string;
5
+ scanRunId: string;
6
+ };
7
+ errors?: string[];
8
+ }
9
+ export interface AccessibilityScanStatus {
10
+ success: boolean;
11
+ data?: {
12
+ status: string;
13
+ };
14
+ errors?: string[];
15
+ }
16
+ export declare class AccessibilityScanner {
17
+ private auth;
18
+ setAuth(auth: {
19
+ username: string;
20
+ password: string;
21
+ }): void;
22
+ startScan(name: string, urlList: string[]): Promise<AccessibilityScanResponse>;
23
+ pollStatus(scanId: string, scanRunId: string): Promise<AccessibilityScanStatus>;
24
+ waitUntilComplete(scanId: string, scanRunId: string, context: any): Promise<string>;
25
+ }
@@ -1,23 +1,30 @@
1
- import axios from "axios";
2
- import config from "../../config.js";
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { randomUUID } from "node:crypto";
3
3
  import logger from "../../logger.js";
4
4
  import { isLocalURL, ensureLocalBinarySetup, killExistingBrowserStackLocalProcesses, } from "../../lib/local.js";
5
+ import config from "../../config.js";
5
6
  export class AccessibilityScanner {
6
- auth = {
7
- username: config.browserstackUsername,
8
- password: config.browserstackAccessKey,
9
- };
7
+ auth;
8
+ setAuth(auth) {
9
+ this.auth = auth;
10
+ }
10
11
  async startScan(name, urlList) {
12
+ if (!this.auth?.username || !this.auth?.password) {
13
+ throw new Error("BrowserStack credentials are not set for AccessibilityScanner.");
14
+ }
11
15
  // Check if any URL is local
12
16
  const hasLocal = urlList.some(isLocalURL);
13
- const localIdentifier = crypto.randomUUID();
17
+ const localIdentifier = randomUUID();
14
18
  const localHosts = new Set(["127.0.0.1", "localhost", "0.0.0.0"]);
15
19
  const BS_LOCAL_DOMAIN = "bs-local.com";
16
20
  if (config.USE_OWN_LOCAL_BINARY_PROCESS && hasLocal) {
17
21
  throw new Error("Cannot start scan with local URLs when using own BrowserStack Local binary process. Please set USE_OWN_LOCAL_BINARY_PROCESS to false.");
18
22
  }
23
+ if (config.REMOTE_MCP && hasLocal) {
24
+ throw new Error("Local URLs are not supported in this remote mcp. Please use a public URL.");
25
+ }
19
26
  if (hasLocal) {
20
- await ensureLocalBinarySetup(localIdentifier);
27
+ await ensureLocalBinarySetup(this.auth.username, this.auth.password, localIdentifier);
21
28
  }
22
29
  else {
23
30
  await killExistingBrowserStackLocalProcesses();
@@ -52,37 +59,49 @@ export class AccessibilityScanner {
52
59
  requestBody = { ...baseRequestBody, ...localConfig };
53
60
  }
54
61
  try {
55
- const { data } = await axios.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", requestBody, { auth: this.auth });
62
+ const response = await apiClient.post({
63
+ url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/scans",
64
+ headers: {
65
+ Authorization: "Basic " +
66
+ Buffer.from(`${this.auth.username}:${this.auth.password}`).toString("base64"),
67
+ "Content-Type": "application/json",
68
+ },
69
+ body: requestBody,
70
+ });
71
+ const data = response.data;
56
72
  if (!data.success)
57
73
  throw new Error(`Unable to start scan: ${data.errors?.join(", ")}`);
58
74
  return data;
59
75
  }
60
76
  catch (err) {
61
- if (axios.isAxiosError(err) && err.response?.data) {
62
- if (err.response.status === 422) {
63
- throw new Error("A scan with this name already exists. please update the name and run again.");
64
- }
65
- const msg = err.response.data.error ||
66
- err.response.data.message ||
67
- err.message;
68
- throw new Error(`Failed to start scan: ${msg}`);
77
+ // apiClient throws generic errors, try to extract message
78
+ if (err?.response?.status === 422) {
79
+ throw new Error("A scan with this name already exists. please update the name and run again.");
69
80
  }
70
- throw err;
81
+ const msg = err?.response?.data?.error ||
82
+ err?.response?.data?.message ||
83
+ err?.message ||
84
+ String(err);
85
+ throw new Error(`Failed to start scan: ${msg}`);
71
86
  }
72
87
  }
73
88
  async pollStatus(scanId, scanRunId) {
74
89
  try {
75
- const { data } = await axios.get(`https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, { auth: this.auth });
90
+ const response = await apiClient.get({
91
+ url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`,
92
+ headers: {
93
+ Authorization: "Basic " +
94
+ Buffer.from(`${this.auth?.username}:${this.auth?.password}`).toString("base64"),
95
+ },
96
+ });
97
+ const data = response.data;
76
98
  if (!data.success)
77
99
  throw new Error(`Failed to get status: ${data.errors?.join(", ")}`);
78
100
  return data;
79
101
  }
80
102
  catch (err) {
81
- if (axios.isAxiosError(err) && err.response?.data) {
82
- const msg = err.response.data.message || err.message;
83
- throw new Error(`Failed to get scan status: ${msg}`);
84
- }
85
- throw err;
103
+ const msg = err?.response?.data?.message || err?.message || String(err);
104
+ throw new Error(`Failed to get scan status: ${msg}`);
86
105
  }
87
106
  }
88
107
  async waitUntilComplete(scanId, scanRunId, context) {
@@ -0,0 +1,42 @@
1
+ import { BrowserStackConfig } from "../../lib/types.js";
2
+ interface Device {
3
+ device: string;
4
+ display_name: string;
5
+ os_version: string;
6
+ real_mobile: boolean;
7
+ }
8
+ /**
9
+ * Finds devices that exactly match the provided display name.
10
+ * Uses fuzzy search first, and then filters for exact case-insensitive match.
11
+ */
12
+ export declare function findMatchingDevice(devices: Device[], deviceName: string): Device[];
13
+ /**
14
+ * Extracts all unique OS versions from a device list and sorts them.
15
+ */
16
+ export declare function getDeviceVersions(devices: Device[]): string[];
17
+ /**
18
+ * Resolves the requested platform version against available versions.
19
+ * Supports 'latest' and 'oldest' as dynamic selectors.
20
+ */
21
+ export declare function resolveVersion(versions: string[], requestedVersion: string): string;
22
+ /**
23
+ * Validates the input arguments for taking app screenshots.
24
+ * Checks for presence and correctness of platform, device, and file types.
25
+ */
26
+ export declare function validateArgs(args: {
27
+ desiredPlatform: string;
28
+ desiredPlatformVersion: string;
29
+ appPath: string;
30
+ desiredPhone: string;
31
+ }): void;
32
+ /**
33
+ * Uploads an application file to AppAutomate and returns the app URL
34
+ */
35
+ export declare function uploadApp(appPath: string, username: string, password: string): Promise<string>;
36
+ export declare function uploadEspressoApp(appPath: string, config: BrowserStackConfig): Promise<string>;
37
+ export declare function uploadEspressoTestSuite(testSuitePath: string, config: BrowserStackConfig): Promise<string>;
38
+ export declare function uploadXcuiApp(appPath: string, config: BrowserStackConfig): Promise<string>;
39
+ export declare function uploadXcuiTestSuite(testSuitePath: string, config: BrowserStackConfig): Promise<string>;
40
+ export declare function triggerEspressoBuild(app_url: string, test_suite_url: string, devices: string[], project: string): Promise<string>;
41
+ export declare function triggerXcuiBuild(app_url: string, test_suite_url: string, devices: string[], project: string, config: BrowserStackConfig): Promise<string>;
42
+ export {};
@@ -1,12 +1,7 @@
1
1
  import fs from "fs";
2
- import axios from "axios";
3
- import config from "../../config.js";
4
2
  import FormData from "form-data";
3
+ import { apiClient } from "../../lib/apiClient.js";
5
4
  import { customFuzzySearch } from "../../lib/fuzzy.js";
6
- const auth = {
7
- username: config.browserstackUsername,
8
- password: config.browserstackAccessKey,
9
- };
10
5
  /**
11
6
  * Finds devices that exactly match the provided display name.
12
7
  * Uses fuzzy search first, and then filters for exact case-insensitive match.
@@ -74,34 +69,44 @@ export function validateArgs(args) {
74
69
  /**
75
70
  * Uploads an application file to AppAutomate and returns the app URL
76
71
  */
77
- export async function uploadApp(appPath) {
72
+ export async function uploadApp(appPath, username, password) {
78
73
  const filePath = appPath;
79
74
  if (!fs.existsSync(filePath)) {
80
75
  throw new Error(`File not found at path: ${filePath}`);
81
76
  }
82
77
  const formData = new FormData();
83
78
  formData.append("file", fs.createReadStream(filePath));
84
- const response = await axios.post("https://api-cloud.browserstack.com/app-automate/upload", formData, {
85
- headers: formData.getHeaders(),
86
- auth,
79
+ const response = await apiClient.post({
80
+ url: "https://api-cloud.browserstack.com/app-automate/upload",
81
+ headers: {
82
+ ...formData.getHeaders(),
83
+ Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
84
+ },
85
+ body: formData,
87
86
  });
88
87
  if (response.data.app_url) {
89
88
  return response.data.app_url;
90
89
  }
91
90
  else {
92
- throw new Error(`Failed to upload app: ${response.data}`);
91
+ throw new Error(`Failed to upload app: ${JSON.stringify(response.data)}`);
93
92
  }
94
93
  }
95
94
  // Helper to upload a file to a given BrowserStack endpoint and return a specific property from the response.
96
- async function uploadFileToBrowserStack(filePath, endpoint, responseKey) {
95
+ async function uploadFileToBrowserStack(filePath, endpoint, responseKey, config) {
97
96
  if (!fs.existsSync(filePath)) {
98
97
  throw new Error(`File not found at path: ${filePath}`);
99
98
  }
100
99
  const formData = new FormData();
101
100
  formData.append("file", fs.createReadStream(filePath));
102
- const response = await axios.post(endpoint, formData, {
103
- headers: formData.getHeaders(),
104
- auth,
101
+ const authHeader = "Basic " +
102
+ Buffer.from(`${config["browserstack-username"]}:${config["browserstack-access-key"]}`).toString("base64");
103
+ const response = await apiClient.post({
104
+ url: endpoint,
105
+ headers: {
106
+ ...formData.getHeaders(),
107
+ Authorization: authHeader,
108
+ },
109
+ body: formData,
105
110
  });
106
111
  if (response.data[responseKey]) {
107
112
  return response.data[responseKey];
@@ -109,30 +114,40 @@ async function uploadFileToBrowserStack(filePath, endpoint, responseKey) {
109
114
  throw new Error(`Failed to upload file: ${JSON.stringify(response.data)}`);
110
115
  }
111
116
  //Uploads an Android app (.apk or .aab) to BrowserStack Espresso endpoint and returns the app_url
112
- export async function uploadEspressoApp(appPath) {
113
- return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/app", "app_url");
117
+ export async function uploadEspressoApp(appPath, config) {
118
+ return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/app", "app_url", config);
114
119
  }
115
120
  //Uploads an Espresso test suite (.apk) to BrowserStack and returns the test_suite_url
116
- export async function uploadEspressoTestSuite(testSuitePath) {
117
- return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite", "test_suite_url");
121
+ export async function uploadEspressoTestSuite(testSuitePath, config) {
122
+ return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite", "test_suite_url", config);
118
123
  }
119
124
  //Uploads an iOS app (.ipa) to BrowserStack XCUITest endpoint and returns the app_url
120
- export async function uploadXcuiApp(appPath) {
121
- return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/app", "app_url");
125
+ export async function uploadXcuiApp(appPath, config) {
126
+ return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/app", "app_url", config);
122
127
  }
123
128
  //Uploads an XCUITest test suite (.zip) to BrowserStack and returns the test_suite_url
124
- export async function uploadXcuiTestSuite(testSuitePath) {
125
- return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite", "test_suite_url");
129
+ export async function uploadXcuiTestSuite(testSuitePath, config) {
130
+ return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite", "test_suite_url", config);
126
131
  }
127
132
  // Triggers an Espresso test run on BrowserStack and returns the build_id
128
133
  export async function triggerEspressoBuild(app_url, test_suite_url, devices, project) {
129
- const response = await axios.post("https://api-cloud.browserstack.com/app-automate/espresso/v2/build", {
130
- app: app_url,
131
- testSuite: test_suite_url,
132
- devices,
133
- project,
134
- }, {
135
- auth,
134
+ const auth = {
135
+ username: process.env.BROWSERSTACK_USERNAME || "",
136
+ password: process.env.BROWSERSTACK_ACCESS_KEY || "",
137
+ };
138
+ const response = await apiClient.post({
139
+ url: "https://api-cloud.browserstack.com/app-automate/espresso/v2/build",
140
+ headers: {
141
+ Authorization: "Basic " +
142
+ Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
143
+ "Content-Type": "application/json",
144
+ },
145
+ body: {
146
+ app: app_url,
147
+ testSuite: test_suite_url,
148
+ devices,
149
+ project,
150
+ },
136
151
  });
137
152
  if (response.data.build_id) {
138
153
  return response.data.build_id;
@@ -140,14 +155,24 @@ export async function triggerEspressoBuild(app_url, test_suite_url, devices, pro
140
155
  throw new Error(`Failed to trigger Espresso build: ${JSON.stringify(response.data)}`);
141
156
  }
142
157
  // Triggers an XCUITest run on BrowserStack and returns the build_id
143
- export async function triggerXcuiBuild(app_url, test_suite_url, devices, project) {
144
- const response = await axios.post("https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build", {
145
- app: app_url,
146
- testSuite: test_suite_url,
147
- devices,
148
- project,
149
- }, {
150
- auth,
158
+ export async function triggerXcuiBuild(app_url, test_suite_url, devices, project, config) {
159
+ const auth = {
160
+ username: config["browserstack-username"],
161
+ password: config["browserstack-access-key"],
162
+ };
163
+ const response = await apiClient.post({
164
+ url: "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build",
165
+ headers: {
166
+ Authorization: "Basic " +
167
+ Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
168
+ "Content-Type": "application/json",
169
+ },
170
+ body: {
171
+ app: app_url,
172
+ testSuite: test_suite_url,
173
+ devices,
174
+ project,
175
+ },
151
176
  });
152
177
  if (response.data.build_id) {
153
178
  return response.data.build_id;
@@ -0,0 +1,5 @@
1
+ export declare enum AppTestPlatform {
2
+ ESPRESSO = "espresso",
3
+ APPIUM = "appium",
4
+ XCUITEST = "xcuitest"
5
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BrowserStackConfig } from "../lib/types.js";
3
+ export default function addAppAutomationTools(server: McpServer, config: BrowserStackConfig): void;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import logger from "../logger.js";
3
- import config from "../config.js";
3
+ import { getBrowserStackAuth } from "../lib/get-auth.js";
4
4
  import { trackMCP } from "../lib/instrumentation.js";
5
5
  import { maybeCompressBase64 } from "../lib/utils.js";
6
6
  import { remote } from "webdriverio";
@@ -19,7 +19,7 @@ async function takeAppScreenshot(args) {
19
19
  let driver;
20
20
  try {
21
21
  validateArgs(args);
22
- const { desiredPlatform, desiredPhone, appPath } = args;
22
+ const { desiredPlatform, desiredPhone, appPath, config } = args;
23
23
  let { desiredPlatformVersion } = args;
24
24
  const platforms = (await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE)).mobile;
25
25
  const platformData = platforms.find((p) => p.os === desiredPlatform.toLowerCase());
@@ -33,7 +33,9 @@ async function takeAppScreenshot(args) {
33
33
  if (!selectedDevice) {
34
34
  throw new Error(`Device "${desiredPhone}" with version ${desiredPlatformVersion} not found.`);
35
35
  }
36
- const app_url = await uploadApp(appPath);
36
+ const authString = getBrowserStackAuth(config);
37
+ const [username, password] = authString.split(":");
38
+ const app_url = await uploadApp(appPath, username, password);
37
39
  logger.info(`App uploaded. URL: ${app_url}`);
38
40
  const capabilities = {
39
41
  platformName: desiredPlatform,
@@ -42,8 +44,8 @@ async function takeAppScreenshot(args) {
42
44
  "appium:app": app_url,
43
45
  "appium:autoGrantPermissions": true,
44
46
  "bstack:options": {
45
- userName: config.browserstackUsername,
46
- accessKey: config.browserstackAccessKey,
47
+ userName: username,
48
+ accessKey: password,
47
49
  appiumVersion: "2.0.1",
48
50
  },
49
51
  };
@@ -86,12 +88,12 @@ async function takeAppScreenshot(args) {
86
88
  }
87
89
  }
88
90
  //Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.
89
- async function runAppTestsOnBrowserStack(args) {
91
+ async function runAppTestsOnBrowserStack(args, config) {
90
92
  switch (args.detectedAutomationFramework) {
91
93
  case AppTestPlatform.ESPRESSO: {
92
94
  try {
93
- const app_url = await uploadEspressoApp(args.appPath);
94
- const test_suite_url = await uploadEspressoTestSuite(args.testSuitePath);
95
+ const app_url = await uploadEspressoApp(args.appPath, config);
96
+ const test_suite_url = await uploadEspressoTestSuite(args.testSuitePath, config);
95
97
  const build_id = await triggerEspressoBuild(app_url, test_suite_url, args.devices, args.project);
96
98
  return {
97
99
  content: [
@@ -109,9 +111,9 @@ async function runAppTestsOnBrowserStack(args) {
109
111
  }
110
112
  case AppTestPlatform.XCUITEST: {
111
113
  try {
112
- const app_url = await uploadXcuiApp(args.appPath);
113
- const test_suite_url = await uploadXcuiTestSuite(args.testSuitePath);
114
- const build_id = await triggerXcuiBuild(app_url, test_suite_url, args.devices, args.project);
114
+ const app_url = await uploadXcuiApp(args.appPath, config);
115
+ const test_suite_url = await uploadXcuiTestSuite(args.testSuitePath, config);
116
+ const build_id = await triggerXcuiBuild(app_url, test_suite_url, args.devices, args.project, config);
115
117
  return {
116
118
  content: [
117
119
  {
@@ -130,8 +132,7 @@ async function runAppTestsOnBrowserStack(args) {
130
132
  throw new Error(`Unsupported automation framework: ${args.detectedAutomationFramework}. If you need support for this framework, please open an issue at Github`);
131
133
  }
132
134
  }
133
- // Registers automation tools with the MCP server.
134
- export default function addAppAutomationTools(server) {
135
+ export default function addAppAutomationTools(server, config) {
135
136
  server.tool("takeAppScreenshot", "Use this tool to take a screenshot of an app running on a BrowserStack device. This is useful for visual testing and debugging.", {
136
137
  desiredPhone: z
137
138
  .string()
@@ -147,11 +148,11 @@ export default function addAppAutomationTools(server) {
147
148
  .describe("The path to the .apk or .ipa file. Required for app installation."),
148
149
  }, async (args) => {
149
150
  try {
150
- trackMCP("takeAppScreenshot", server.server.getClientVersion());
151
- return await takeAppScreenshot(args);
151
+ trackMCP("takeAppScreenshot", server.server.getClientVersion(), undefined, config);
152
+ return await takeAppScreenshot({ ...args, config });
152
153
  }
153
154
  catch (error) {
154
- trackMCP("takeAppScreenshot", server.server.getClientVersion(), error);
155
+ trackMCP("takeAppScreenshot", server.server.getClientVersion(), error, config);
155
156
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
156
157
  return {
157
158
  content: [
@@ -197,11 +198,11 @@ export default function addAppAutomationTools(server) {
197
198
  .describe("The automation framework used in the project, such as 'espresso' (Android) or 'xcuitest' (iOS)."),
198
199
  }, async (args) => {
199
200
  try {
200
- trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion());
201
- return await runAppTestsOnBrowserStack(args);
201
+ trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), undefined, config);
202
+ return await runAppTestsOnBrowserStack(args, config);
202
203
  }
203
204
  catch (error) {
204
- trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), error);
205
+ trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), error, config);
205
206
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
206
207
  return {
207
208
  content: [
@@ -0,0 +1,6 @@
1
+ import { DeviceEntry } from "./types.js";
2
+ /**
3
+ * Find matching devices by name with exact match preference.
4
+ * Throws if none or multiple exact matches.
5
+ */
6
+ export declare function findDeviceByName(devices: DeviceEntry[], desiredPhone: string): DeviceEntry[];
@@ -0,0 +1,15 @@
1
+ import { BrowserStackConfig } from "../../lib/types.js";
2
+ interface StartSessionArgs {
3
+ appPath: string;
4
+ desiredPlatform: "android" | "ios";
5
+ desiredPhone: string;
6
+ desiredPlatformVersion: string;
7
+ }
8
+ interface StartSessionOptions {
9
+ config: BrowserStackConfig;
10
+ }
11
+ /**
12
+ * Start an App Live session: filter, select, upload, and open.
13
+ */
14
+ export declare function startSession(args: StartSessionArgs, options: StartSessionOptions): Promise<string>;
15
+ export {};