@browserstack/mcp-server 1.2.1 → 1.2.2-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 (113) hide show
  1. package/README.md +227 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/lib/device-cache.d.ts +1 -0
  5. package/dist/lib/device-cache.js +28 -0
  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 +2 -0
  9. package/dist/oninitialized.js +4 -1
  10. package/dist/server-factory.js +3 -1
  11. package/dist/tools/accessibility.js +238 -78
  12. package/dist/tools/accessiblity-utils/auth-config.d.ts +39 -0
  13. package/dist/tools/accessiblity-utils/auth-config.js +125 -0
  14. package/dist/tools/accessiblity-utils/scanner.d.ts +1 -1
  15. package/dist/tools/accessiblity-utils/scanner.js +2 -1
  16. package/dist/tools/add-percy-snapshots.d.ts +5 -0
  17. package/dist/tools/add-percy-snapshots.js +17 -0
  18. package/dist/tools/appautomate-utils/appautomate.d.ts +2 -1
  19. package/dist/tools/appautomate-utils/appautomate.js +12 -9
  20. package/dist/tools/appautomate.js +56 -7
  21. package/dist/tools/applive-utils/start-session.d.ts +2 -1
  22. package/dist/tools/applive-utils/start-session.js +17 -6
  23. package/dist/tools/applive.d.ts +2 -1
  24. package/dist/tools/applive.js +21 -17
  25. package/dist/tools/bstack-sdk.d.ts +2 -15
  26. package/dist/tools/bstack-sdk.js +7 -124
  27. package/dist/tools/list-test-files.d.ts +2 -0
  28. package/dist/tools/list-test-files.js +33 -0
  29. package/dist/tools/percy-sdk.d.ts +4 -0
  30. package/dist/tools/percy-sdk.js +88 -0
  31. package/dist/tools/percy-snapshot-utils/constants.d.ts +16 -0
  32. package/dist/tools/percy-snapshot-utils/constants.js +500 -0
  33. package/dist/tools/percy-snapshot-utils/detect-test-files.d.ts +10 -0
  34. package/dist/tools/percy-snapshot-utils/detect-test-files.js +194 -0
  35. package/dist/tools/percy-snapshot-utils/types.d.ts +15 -0
  36. package/dist/tools/percy-snapshot-utils/utils.d.ts +4 -0
  37. package/dist/tools/percy-snapshot-utils/utils.js +30 -0
  38. package/dist/tools/sdk-utils/{commands.d.ts → bstack/commands.d.ts} +1 -1
  39. package/dist/tools/sdk-utils/bstack/commands.js +88 -0
  40. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +4 -0
  41. package/dist/tools/sdk-utils/bstack/configUtils.js +66 -0
  42. package/dist/tools/sdk-utils/bstack/constants.d.ts +58 -0
  43. package/dist/tools/sdk-utils/{constants.js → bstack/constants.js} +128 -76
  44. package/dist/tools/sdk-utils/{constants.d.ts → bstack/frameworks.d.ts} +1 -1
  45. package/dist/tools/sdk-utils/bstack/frameworks.js +57 -0
  46. package/dist/tools/sdk-utils/bstack/index.d.ts +4 -0
  47. package/dist/tools/sdk-utils/bstack/index.js +5 -0
  48. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +4 -0
  49. package/dist/tools/sdk-utils/bstack/sdkHandler.js +74 -0
  50. package/dist/tools/sdk-utils/common/constants.d.ts +10 -0
  51. package/dist/tools/sdk-utils/common/constants.js +86 -0
  52. package/dist/tools/sdk-utils/common/formatUtils.d.ts +5 -0
  53. package/dist/tools/sdk-utils/common/formatUtils.js +27 -0
  54. package/dist/tools/sdk-utils/common/index.d.ts +3 -0
  55. package/dist/tools/sdk-utils/common/index.js +4 -0
  56. package/dist/tools/sdk-utils/common/instructionUtils.d.ts +8 -0
  57. package/dist/tools/sdk-utils/common/instructionUtils.js +20 -0
  58. package/dist/tools/sdk-utils/common/schema.d.ts +61 -0
  59. package/dist/tools/sdk-utils/common/schema.js +28 -0
  60. package/dist/tools/sdk-utils/common/types.d.ts +66 -0
  61. package/dist/tools/sdk-utils/common/types.js +50 -0
  62. package/dist/tools/sdk-utils/common/utils.d.ts +25 -0
  63. package/dist/tools/sdk-utils/common/utils.js +84 -0
  64. package/dist/tools/sdk-utils/handler.d.ts +5 -0
  65. package/dist/tools/sdk-utils/handler.js +144 -0
  66. package/dist/tools/sdk-utils/percy-automate/constants.d.ts +11 -0
  67. package/dist/tools/sdk-utils/percy-automate/constants.js +365 -0
  68. package/dist/tools/sdk-utils/percy-automate/frameworks.d.ts +8 -0
  69. package/dist/tools/sdk-utils/percy-automate/frameworks.js +50 -0
  70. package/dist/tools/sdk-utils/percy-automate/handler.d.ts +3 -0
  71. package/dist/tools/sdk-utils/percy-automate/handler.js +30 -0
  72. package/dist/tools/sdk-utils/percy-automate/index.d.ts +1 -0
  73. package/dist/tools/sdk-utils/percy-automate/index.js +2 -0
  74. package/dist/tools/sdk-utils/percy-automate/types.d.ts +13 -0
  75. package/dist/tools/sdk-utils/percy-automate/types.js +1 -0
  76. package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -0
  77. package/dist/tools/sdk-utils/{percy → percy-bstack}/constants.js +13 -39
  78. package/dist/tools/sdk-utils/percy-bstack/frameworks.d.ts +2 -0
  79. package/dist/tools/sdk-utils/percy-bstack/frameworks.js +27 -0
  80. package/dist/tools/sdk-utils/percy-bstack/handler.d.ts +4 -0
  81. package/dist/tools/sdk-utils/percy-bstack/handler.js +99 -0
  82. package/dist/tools/sdk-utils/percy-bstack/index.d.ts +4 -0
  83. package/dist/tools/sdk-utils/percy-bstack/index.js +4 -0
  84. package/dist/tools/sdk-utils/percy-bstack/instructions.d.ts +7 -0
  85. package/dist/tools/sdk-utils/{percy → percy-bstack}/instructions.js +5 -9
  86. package/dist/tools/sdk-utils/percy-bstack/types.d.ts +13 -0
  87. package/dist/tools/sdk-utils/percy-bstack/types.js +5 -0
  88. package/dist/tools/sdk-utils/percy-web/constants.d.ts +41 -0
  89. package/dist/tools/sdk-utils/percy-web/constants.js +941 -0
  90. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.d.ts +4 -0
  91. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.js +28 -0
  92. package/dist/tools/sdk-utils/percy-web/frameworks.d.ts +7 -0
  93. package/dist/tools/sdk-utils/percy-web/frameworks.js +103 -0
  94. package/dist/tools/sdk-utils/percy-web/handler.d.ts +4 -0
  95. package/dist/tools/sdk-utils/percy-web/handler.js +27 -0
  96. package/dist/tools/sdk-utils/percy-web/index.d.ts +4 -0
  97. package/dist/tools/sdk-utils/percy-web/index.js +4 -0
  98. package/dist/tools/sdk-utils/percy-web/types.d.ts +12 -0
  99. package/dist/tools/sdk-utils/percy-web/types.js +1 -0
  100. package/dist/tools/sdk-utils/types.d.ts +2 -1
  101. package/dist/tools/sdk-utils/types.js +1 -0
  102. package/dist/tools/testmanagement-utils/create-testcase.d.ts +4 -0
  103. package/dist/tools/testmanagement-utils/create-testcase.js +6 -0
  104. package/package.json +1 -1
  105. package/dist/tools/sdk-utils/commands.js +0 -65
  106. package/dist/tools/sdk-utils/instructions.d.ts +0 -6
  107. package/dist/tools/sdk-utils/instructions.js +0 -99
  108. package/dist/tools/sdk-utils/percy/constants.d.ts +0 -3
  109. package/dist/tools/sdk-utils/percy/instructions.d.ts +0 -10
  110. package/dist/tools/sdk-utils/percy/types.d.ts +0 -5
  111. /package/dist/tools/{getFailureLogs.d.ts → get-failure-logs.d.ts} +0 -0
  112. /package/dist/tools/{getFailureLogs.js → get-failure-logs.js} +0 -0
  113. /package/dist/tools/{sdk-utils/percy → percy-snapshot-utils}/types.js +0 -0
@@ -1,66 +1,211 @@
1
1
  import { z } from "zod";
2
2
  import { AccessibilityScanner } from "./accessiblity-utils/scanner.js";
3
3
  import { AccessibilityReportFetcher } from "./accessiblity-utils/report-fetcher.js";
4
+ import { AccessibilityAuthConfig } from "./accessiblity-utils/auth-config.js";
4
5
  import { trackMCP } from "../lib/instrumentation.js";
5
6
  import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
6
7
  import { queryAccessibilityRAG } from "./accessiblity-utils/accessibility-rag.js";
7
8
  import { getBrowserStackAuth } from "../lib/get-auth.js";
8
- async function runAccessibilityScan(name, pageURL, context, config) {
9
- // Create scanner and set auth on the go
10
- const scanner = new AccessibilityScanner();
9
+ import logger from "../logger.js";
10
+ function setupAuth(config) {
11
11
  const authString = getBrowserStackAuth(config);
12
12
  const [username, password] = authString.split(":");
13
- scanner.setAuth({ username, password });
14
- // Start scan
15
- const startResp = await scanner.startScan(name, [pageURL]);
16
- const scanId = startResp.data.id;
17
- const scanRunId = startResp.data.scanRunId;
18
- // Notify scan start
13
+ return { username, password };
14
+ }
15
+ function createErrorResponse(message, isError = true) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: message,
21
+ isError,
22
+ },
23
+ ],
24
+ isError,
25
+ };
26
+ }
27
+ function createSuccessResponse(messages) {
28
+ return {
29
+ content: messages.map((text) => ({
30
+ type: "text",
31
+ text,
32
+ })),
33
+ };
34
+ }
35
+ function handleMCPError(toolName, server, config, error) {
36
+ trackMCP(toolName, server.server.getClientVersion(), error, config);
37
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
38
+ return createErrorResponse(`Failed to ${toolName.replace(/([A-Z])/g, " $1").toLowerCase()}: ${errorMessage}. Please open an issue on GitHub if the problem persists`);
39
+ }
40
+ async function notifyScanProgress(context, message, progress = 0) {
19
41
  await context.sendNotification({
20
42
  method: "notifications/progress",
21
43
  params: {
22
- progressToken: context._meta?.progressToken ?? "NOT_FOUND",
23
- message: `Accessibility scan "${name}" started`,
24
- progress: 0,
44
+ progressToken: context._meta?.progressToken?.toString() ?? "NOT_FOUND",
45
+ message,
46
+ progress,
25
47
  total: 100,
26
48
  },
27
49
  });
28
- // Wait until scan completes
50
+ }
51
+ async function initializeScanner(config) {
52
+ const scanner = new AccessibilityScanner();
53
+ const auth = setupAuth(config);
54
+ scanner.setAuth(auth);
55
+ return scanner;
56
+ }
57
+ async function initializeReportFetcher(config) {
58
+ const reportFetcher = new AccessibilityReportFetcher();
59
+ const auth = setupAuth(config);
60
+ reportFetcher.setAuth(auth);
61
+ return reportFetcher;
62
+ }
63
+ async function executeAccessibilityRAG(args, server, config) {
64
+ try {
65
+ trackMCP("accessibilityExpert", server.server.getClientVersion(), undefined, config);
66
+ return await queryAccessibilityRAG(args.query, config);
67
+ }
68
+ catch (error) {
69
+ return handleMCPError("accessibilityExpert", server, config, error);
70
+ }
71
+ }
72
+ async function executeFetchAccessibilityIssues(args, server, config) {
73
+ try {
74
+ trackMCP("fetchAccessibilityIssues", server.server.getClientVersion(), undefined, config);
75
+ return await fetchAccessibilityIssues(args.scanId, args.scanRunId, config, args.cursor);
76
+ }
77
+ catch (error) {
78
+ return handleMCPError("fetchAccessibilityIssues", server, config, error);
79
+ }
80
+ }
81
+ async function fetchAccessibilityIssues(scanId, scanRunId, config, cursor = 0) {
82
+ const reportFetcher = await initializeReportFetcher(config);
83
+ const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
84
+ const { records, page_length, total_issues, next_page } = await parseAccessibilityReportFromCSV(reportLink, { nextPage: cursor });
85
+ const currentlyShown = cursor === 0
86
+ ? page_length
87
+ : Math.floor(cursor / JSON.stringify(records[0] || {}).length) +
88
+ page_length;
89
+ const remainingIssues = total_issues - currentlyShown;
90
+ const messages = [
91
+ `Retrieved ${page_length} accessibility issues (Total: ${total_issues})`,
92
+ `Issues: ${JSON.stringify(records, null, 2)}`,
93
+ ];
94
+ if (next_page !== null) {
95
+ messages.push(`${remainingIssues} more issues available. Use fetchAccessibilityIssues with cursor: ${next_page} to get the next batch.`);
96
+ }
97
+ else {
98
+ messages.push(`✅ All issues retrieved.`);
99
+ }
100
+ return createSuccessResponse(messages);
101
+ }
102
+ async function executeAccessibilityScan(args, context, server, config) {
103
+ try {
104
+ trackMCP("startAccessibilityScan", server.server.getClientVersion(), undefined, config);
105
+ return await runAccessibilityScan(args.name, args.pageURL, context, config, args.authConfigId);
106
+ }
107
+ catch (error) {
108
+ return handleMCPError("startAccessibilityScan", server, config, error);
109
+ }
110
+ }
111
+ function validateFormAuthArgs(args) {
112
+ return (args.type === "form" &&
113
+ "usernameSelector" in args &&
114
+ "passwordSelector" in args &&
115
+ "submitSelector" in args &&
116
+ !!args.usernameSelector &&
117
+ !!args.passwordSelector &&
118
+ !!args.submitSelector);
119
+ }
120
+ async function createAuthConfig(args, config) {
121
+ const authConfig = new AccessibilityAuthConfig();
122
+ const auth = setupAuth(config);
123
+ authConfig.setAuth(auth);
124
+ if (args.type === "form") {
125
+ if (!validateFormAuthArgs(args)) {
126
+ throw new Error("Form authentication requires usernameSelector, passwordSelector, and submitSelector");
127
+ }
128
+ return await authConfig.createFormAuthConfig(args.name, {
129
+ username: args.username,
130
+ usernameSelector: args.usernameSelector,
131
+ password: args.password,
132
+ passwordSelector: args.passwordSelector,
133
+ submitSelector: args.submitSelector,
134
+ url: args.url,
135
+ });
136
+ }
137
+ else {
138
+ return await authConfig.createBasicAuthConfig(args.name, {
139
+ url: args.url,
140
+ username: args.username,
141
+ password: args.password,
142
+ });
143
+ }
144
+ }
145
+ async function executeCreateAuthConfig(args, server, config) {
146
+ try {
147
+ trackMCP("createAccessibilityAuthConfig", server.server.getClientVersion(), undefined, config);
148
+ logger.info(`Creating auth config: ${JSON.stringify(args)}`);
149
+ const result = await createAuthConfig(args, config);
150
+ return createSuccessResponse([
151
+ `✅ Auth config "${args.name}" created successfully with ID: ${result.data?.id}`,
152
+ `Auth config details: ${JSON.stringify(result.data, null, 2)}`,
153
+ ]);
154
+ }
155
+ catch (error) {
156
+ if (error instanceof Error &&
157
+ error.message.includes("Form authentication requires")) {
158
+ return createErrorResponse(error.message);
159
+ }
160
+ return handleMCPError("createAccessibilityAuthConfig", server, config, error);
161
+ }
162
+ }
163
+ async function executeGetAuthConfig(args, server, config) {
164
+ try {
165
+ trackMCP("getAccessibilityAuthConfig", server.server.getClientVersion(), undefined, config);
166
+ const authConfig = new AccessibilityAuthConfig();
167
+ const auth = setupAuth(config);
168
+ authConfig.setAuth(auth);
169
+ const result = await authConfig.getAuthConfig(args.configId);
170
+ return createSuccessResponse([
171
+ `✅ Auth config retrieved successfully`,
172
+ `Auth config details: ${JSON.stringify(result.data, null, 2)}`,
173
+ ]);
174
+ }
175
+ catch (error) {
176
+ return handleMCPError("getAccessibilityAuthConfig", server, config, error);
177
+ }
178
+ }
179
+ function createScanFailureResponse(name, status) {
180
+ return createErrorResponse(`❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`);
181
+ }
182
+ function createScanSuccessResponse(name, totalIssues, pageLength, records, scanId, scanRunId, reportUrl, cursor) {
183
+ const messages = [
184
+ `Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
185
+ `Scan ID: ${scanId} and Scan Run ID: ${scanRunId}`,
186
+ `You can also download the full report from the following link: ${reportUrl}`,
187
+ `We found ${totalIssues} issues. Below are the details of the ${pageLength} most critical issues.`,
188
+ `Scan results: ${JSON.stringify(records, null, 2)}`,
189
+ ];
190
+ if (cursor !== null) {
191
+ messages.push(`More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and cursor: ${cursor} to get the next batch.`);
192
+ }
193
+ return createSuccessResponse(messages);
194
+ }
195
+ async function runAccessibilityScan(name, pageURL, context, config, authConfigId) {
196
+ const scanner = await initializeScanner(config);
197
+ const startResp = await scanner.startScan(name, [pageURL], authConfigId);
198
+ const scanId = startResp.data.id;
199
+ const scanRunId = startResp.data.scanRunId;
200
+ await notifyScanProgress(context, `Accessibility scan "${name}" started`, 0);
29
201
  const status = await scanner.waitUntilComplete(scanId, scanRunId, context);
30
202
  if (status !== "completed") {
31
- return {
32
- content: [
33
- {
34
- type: "text",
35
- text: `❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
36
- isError: true,
37
- },
38
- ],
39
- isError: true,
40
- };
41
- }
42
- // Create report fetcher and set auth on the go
43
- const reportFetcher = new AccessibilityReportFetcher();
44
- reportFetcher.setAuth({ username, password });
45
- // Fetch CSV report link
203
+ return createScanFailureResponse(name, status);
204
+ }
205
+ const reportFetcher = await initializeReportFetcher(config);
46
206
  const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
47
- const { records, page_length, total_issues } = await parseAccessibilityReportFromCSV(reportLink);
48
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
53
- },
54
- {
55
- type: "text",
56
- text: `We found ${total_issues} issues. Below are the details of the ${page_length} most critical issues.`,
57
- },
58
- {
59
- type: "text",
60
- text: `Scan results: ${JSON.stringify(records, null, 2)}`,
61
- },
62
- ],
63
- };
207
+ const { records, page_length, total_issues, next_page } = await parseAccessibilityReportFromCSV(reportLink);
208
+ return createScanSuccessResponse(name, total_issues, page_length, records, scanId, scanRunId, reportLink, next_page);
64
209
  }
65
210
  export default function addAccessibilityTools(server, config) {
66
211
  const tools = {};
@@ -69,44 +214,59 @@ export default function addAccessibilityTools(server, config) {
69
214
  .string()
70
215
  .describe("Any accessibility, a11y, WCAG, or web accessibility question"),
71
216
  }, async (args) => {
72
- try {
73
- trackMCP("accessibilityExpert", server.server.getClientVersion(), undefined, config);
74
- return await queryAccessibilityRAG(args.query, config);
75
- }
76
- catch (error) {
77
- trackMCP("accessibilityExpert", server.server.getClientVersion(), error, config);
78
- return {
79
- content: [
80
- {
81
- type: "text",
82
- text: `Failed to query accessibility RAG: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
83
- },
84
- ],
85
- isError: true,
86
- };
87
- }
217
+ return await executeAccessibilityRAG(args, server, config);
88
218
  });
89
219
  tools.startAccessibilityScan = server.tool("startAccessibilityScan", "Start an accessibility scan via BrowserStack and retrieve a local CSV report path.", {
90
220
  name: z.string().describe("Name of the accessibility scan"),
91
221
  pageURL: z.string().describe("The URL to scan for accessibility issues"),
222
+ authConfigId: z
223
+ .number()
224
+ .optional()
225
+ .describe("Optional auth config ID for authenticated scans"),
92
226
  }, async (args, context) => {
93
- try {
94
- trackMCP("startAccessibilityScan", server.server.getClientVersion(), undefined, config);
95
- return await runAccessibilityScan(args.name, args.pageURL, context, config);
96
- }
97
- catch (error) {
98
- trackMCP("startAccessibilityScan", server.server.getClientVersion(), error, config);
99
- return {
100
- content: [
101
- {
102
- type: "text",
103
- text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
104
- isError: true,
105
- },
106
- ],
107
- isError: true,
108
- };
109
- }
227
+ return await executeAccessibilityScan(args, context, server, config);
228
+ });
229
+ tools.createAccessibilityAuthConfig = server.tool("createAccessibilityAuthConfig", "Create an authentication configuration for accessibility scans. Supports both form-based and basic authentication.", {
230
+ name: z.string().describe("Name for the auth configuration"),
231
+ type: z
232
+ .enum(["form", "basic"])
233
+ .describe("Authentication type: 'form' for form-based auth, 'basic' for HTTP basic auth"),
234
+ url: z.string().describe("URL of the authentication page"),
235
+ username: z.string().describe("Username for authentication"),
236
+ password: z.string().describe("Password for authentication"),
237
+ usernameSelector: z
238
+ .string()
239
+ .optional()
240
+ .describe("CSS selector for username field (required for form auth)"),
241
+ passwordSelector: z
242
+ .string()
243
+ .optional()
244
+ .describe("CSS selector for password field (required for form auth)"),
245
+ submitSelector: z
246
+ .string()
247
+ .optional()
248
+ .describe("CSS selector for submit button (required for form auth)"),
249
+ }, async (args) => {
250
+ return await executeCreateAuthConfig(args, server, config);
251
+ });
252
+ tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID.", {
253
+ configId: z.number().describe("ID of the auth configuration to retrieve"),
254
+ }, async (args) => {
255
+ return await executeGetAuthConfig(args, server, config);
256
+ });
257
+ tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results.", {
258
+ scanId: z
259
+ .string()
260
+ .describe("The scan ID from a completed accessibility scan"),
261
+ scanRunId: z
262
+ .string()
263
+ .describe("The scan run ID from a completed accessibility scan"),
264
+ cursor: z
265
+ .number()
266
+ .optional()
267
+ .describe("Character offset for pagination (default: 0)"),
268
+ }, async (args) => {
269
+ return await executeFetchAccessibilityIssues(args, server, config);
110
270
  });
111
271
  return tools;
112
272
  }
@@ -0,0 +1,39 @@
1
+ export interface AuthConfigResponse {
2
+ success: boolean;
3
+ data?: {
4
+ id: number;
5
+ name: string;
6
+ type: string;
7
+ username?: string;
8
+ password?: string;
9
+ url?: string;
10
+ usernameSelector?: string;
11
+ passwordSelector?: string;
12
+ submitSelector?: string;
13
+ };
14
+ errors?: string[];
15
+ }
16
+ export interface FormAuthData {
17
+ username: string;
18
+ usernameSelector: string;
19
+ password: string;
20
+ passwordSelector: string;
21
+ submitSelector: string;
22
+ url: string;
23
+ }
24
+ export interface BasicAuthData {
25
+ url: string;
26
+ username: string;
27
+ password: string;
28
+ }
29
+ export declare class AccessibilityAuthConfig {
30
+ private auth;
31
+ setAuth(auth: {
32
+ username: string;
33
+ password: string;
34
+ }): void;
35
+ private transformLocalUrl;
36
+ createFormAuthConfig(name: string, authData: FormAuthData): Promise<AuthConfigResponse>;
37
+ createBasicAuthConfig(name: string, authData: BasicAuthData): Promise<AuthConfigResponse>;
38
+ getAuthConfig(configId: number): Promise<AuthConfigResponse>;
39
+ }
@@ -0,0 +1,125 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import logger from "../../logger.js";
3
+ export class AccessibilityAuthConfig {
4
+ auth;
5
+ setAuth(auth) {
6
+ this.auth = auth;
7
+ }
8
+ transformLocalUrl(url) {
9
+ try {
10
+ const parsed = new URL(url);
11
+ const localHosts = new Set(["127.0.0.1", "localhost", "0.0.0.0"]);
12
+ const BS_LOCAL_DOMAIN = "bs-local.com";
13
+ if (localHosts.has(parsed.hostname)) {
14
+ parsed.hostname = BS_LOCAL_DOMAIN;
15
+ return parsed.toString();
16
+ }
17
+ return url;
18
+ }
19
+ catch {
20
+ return url;
21
+ }
22
+ }
23
+ async createFormAuthConfig(name, authData) {
24
+ if (!this.auth?.username || !this.auth?.password) {
25
+ throw new Error("BrowserStack credentials are not set for AccessibilityAuthConfig.");
26
+ }
27
+ const transformedAuthData = {
28
+ ...authData,
29
+ url: this.transformLocalUrl(authData.url),
30
+ };
31
+ const requestBody = {
32
+ name,
33
+ type: "form",
34
+ authData: transformedAuthData,
35
+ };
36
+ try {
37
+ const response = await apiClient.post({
38
+ url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs",
39
+ headers: {
40
+ Authorization: "Basic " +
41
+ Buffer.from(`${this.auth.username}:${this.auth.password}`).toString("base64"),
42
+ "Content-Type": "application/json",
43
+ },
44
+ body: requestBody,
45
+ });
46
+ const data = response.data;
47
+ logger.info(`The data returned from the API is: ${JSON.stringify(data)}`);
48
+ if (!data.success) {
49
+ throw new Error(`Unable to create auth config: ${data.errors?.join(", ")}`);
50
+ }
51
+ return data;
52
+ }
53
+ catch (err) {
54
+ logger.error(`Error creating form auth config: ${JSON.stringify(err?.response?.data)}`);
55
+ const msg = err?.response?.data?.error ||
56
+ err?.response?.data?.message ||
57
+ err?.message ||
58
+ String(err);
59
+ throw new Error(`Failed to create form auth config: ${msg}`);
60
+ }
61
+ }
62
+ async createBasicAuthConfig(name, authData) {
63
+ if (!this.auth?.username || !this.auth?.password) {
64
+ throw new Error("BrowserStack credentials are not set for AccessibilityAuthConfig.");
65
+ }
66
+ const transformedAuthData = {
67
+ ...authData,
68
+ url: this.transformLocalUrl(authData.url),
69
+ };
70
+ const requestBody = {
71
+ name,
72
+ type: "basic",
73
+ authData: transformedAuthData,
74
+ };
75
+ try {
76
+ const response = await apiClient.post({
77
+ url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs",
78
+ headers: {
79
+ Authorization: "Basic " +
80
+ Buffer.from(`${this.auth.username}:${this.auth.password}`).toString("base64"),
81
+ "Content-Type": "application/json",
82
+ },
83
+ body: requestBody,
84
+ });
85
+ const data = response.data;
86
+ if (!data.success) {
87
+ throw new Error(`Unable to create auth config: ${data.errors?.join(", ")}`);
88
+ }
89
+ return data;
90
+ }
91
+ catch (err) {
92
+ const msg = err?.response?.data?.error ||
93
+ err?.response?.data?.message ||
94
+ err?.message ||
95
+ String(err);
96
+ throw new Error(`Failed to create basic auth config: ${msg}`);
97
+ }
98
+ }
99
+ async getAuthConfig(configId) {
100
+ if (!this.auth?.username || !this.auth?.password) {
101
+ throw new Error("BrowserStack credentials are not set for AccessibilityAuthConfig.");
102
+ }
103
+ try {
104
+ const response = await apiClient.get({
105
+ url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs/${configId}`,
106
+ headers: {
107
+ Authorization: "Basic " +
108
+ Buffer.from(`${this.auth.username}:${this.auth.password}`).toString("base64"),
109
+ },
110
+ });
111
+ const data = response.data;
112
+ if (!data.success) {
113
+ throw new Error(`Unable to get auth config: ${data.errors?.join(", ")}`);
114
+ }
115
+ return data;
116
+ }
117
+ catch (err) {
118
+ const msg = err?.response?.data?.error ||
119
+ err?.response?.data?.message ||
120
+ err?.message ||
121
+ String(err);
122
+ throw new Error(`Failed to get auth config: ${msg}`);
123
+ }
124
+ }
125
+ }
@@ -19,7 +19,7 @@ export declare class AccessibilityScanner {
19
19
  username: string;
20
20
  password: string;
21
21
  }): void;
22
- startScan(name: string, urlList: string[]): Promise<AccessibilityScanResponse>;
22
+ startScan(name: string, urlList: string[], authConfigId?: number): Promise<AccessibilityScanResponse>;
23
23
  pollStatus(scanId: string, scanRunId: string): Promise<AccessibilityScanStatus>;
24
24
  waitUntilComplete(scanId: string, scanRunId: string, context: any): Promise<string>;
25
25
  }
@@ -8,7 +8,7 @@ export class AccessibilityScanner {
8
8
  setAuth(auth) {
9
9
  this.auth = auth;
10
10
  }
11
- async startScan(name, urlList) {
11
+ async startScan(name, urlList, authConfigId) {
12
12
  if (!this.auth?.username || !this.auth?.password) {
13
13
  throw new Error("BrowserStack credentials are not set for AccessibilityScanner.");
14
14
  }
@@ -47,6 +47,7 @@ export class AccessibilityScanner {
47
47
  name,
48
48
  urlList: transformedUrlList,
49
49
  recurring: false,
50
+ ...(authConfigId && { authConfigId }),
50
51
  };
51
52
  let requestBody = baseRequestBody;
52
53
  if (hasLocal) {
@@ -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
+ }
@@ -26,8 +26,9 @@ export declare function resolveVersion(versions: string[], requestedVersion: str
26
26
  export declare function validateArgs(args: {
27
27
  desiredPlatform: string;
28
28
  desiredPlatformVersion: string;
29
- appPath: string;
29
+ appPath?: string;
30
30
  desiredPhone: string;
31
+ browserstackAppUrl?: string;
31
32
  }): void;
32
33
  /**
33
34
  * Uploads an application file to AppAutomate and returns the app URL
@@ -49,21 +49,24 @@ export function resolveVersion(versions, requestedVersion) {
49
49
  * Checks for presence and correctness of platform, device, and file types.
50
50
  */
51
51
  export function validateArgs(args) {
52
- const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone } = args;
52
+ const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone, browserstackAppUrl, } = args;
53
53
  if (!desiredPlatform || !desiredPhone) {
54
54
  throw new Error("Missing required arguments: desiredPlatform and desiredPhone are required");
55
55
  }
56
56
  if (!desiredPlatformVersion) {
57
57
  throw new Error("Missing required arguments: desiredPlatformVersion is required");
58
58
  }
59
- if (!appPath) {
60
- throw new Error("You must provide an appPath.");
61
- }
62
- if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
63
- throw new Error("You must provide a valid Android app path (.apk).");
64
- }
65
- if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
66
- throw new Error("You must provide a valid iOS app path (.ipa).");
59
+ if (!appPath && !browserstackAppUrl) {
60
+ throw new Error("Either appPath or browserstackAppUrl must be provided");
61
+ }
62
+ // Only validate app path format if appPath is provided
63
+ if (appPath) {
64
+ if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
65
+ throw new Error("You must provide a valid Android app path (.apk).");
66
+ }
67
+ if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
68
+ throw new Error("You must provide a valid iOS app path (.ipa).");
69
+ }
67
70
  }
68
71
  }
69
72
  /**