@browserstack/mcp-server 1.1.1 → 1.1.3

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.
package/dist/config.js CHANGED
@@ -1,7 +1,3 @@
1
- if (!process.env.BROWSERSTACK_ACCESS_KEY ||
2
- !process.env.BROWSERSTACK_USERNAME) {
3
- throw new Error("Unable to start MCP server. Please set the BROWSERSTACK_ACCESS_KEY and BROWSERSTACK_USERNAME environment variables. Go to https://www.browserstack.com/accounts/profile/details to access them");
4
- }
5
1
  export class Config {
6
2
  browserstackUsername;
7
3
  browserstackAccessKey;
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import packageJson from "../package.json" with { type: "json" };
4
+ import { createRequire } from "module";
5
+ const require = createRequire(import.meta.url);
6
+ const packageJson = require("../package.json");
5
7
  import "dotenv/config";
6
8
  import logger from "./logger.js";
7
9
  import addSDKTools from "./tools/bstack-sdk.js";
@@ -12,7 +14,7 @@ import addTestManagementTools from "./tools/testmanagement.js";
12
14
  import addAppAutomationTools from "./tools/appautomate.js";
13
15
  import addFailureLogsTools from "./tools/getFailureLogs.js";
14
16
  import addAutomateTools from "./tools/automate.js";
15
- import { trackMCP } from "./lib/instrumentation.js";
17
+ import { setupOnInitialized } from "./oninitialized.js";
16
18
  function registerTools(server) {
17
19
  addSDKTools(server);
18
20
  addAppLiveTools(server);
@@ -28,14 +30,13 @@ const server = new McpServer({
28
30
  name: "BrowserStack MCP Server",
29
31
  version: packageJson.version,
30
32
  });
33
+ setupOnInitialized(server);
31
34
  registerTools(server);
32
35
  async function main() {
33
36
  logger.info("Launching BrowserStack MCP server, version %s", packageJson.version);
34
37
  // Start receiving messages on stdin and sending messages on stdout
35
38
  const transport = new StdioServerTransport();
36
39
  await server.connect(transport);
37
- logger.info("MCP server started successfully");
38
- trackMCP("started", server.server.getClientVersion());
39
40
  }
40
41
  main().catch(console.error);
41
42
  // Ensure logs are flushed before exit
@@ -1,6 +1,8 @@
1
1
  import logger from "../logger.js";
2
2
  import config from "../config.js";
3
- import packageJson from "../../package.json" with { type: "json" };
3
+ import { createRequire } from "module";
4
+ const require = createRequire(import.meta.url);
5
+ const packageJson = require("../../package.json");
4
6
  import axios from "axios";
5
7
  export function trackMCP(toolName, clientInfo, error) {
6
8
  if (config.DEV_MODE) {
package/dist/lib/utils.js CHANGED
@@ -24,9 +24,3 @@ export async function assertOkResponse(response, action) {
24
24
  throw new Error(`Failed to fetch logs for ${action}: ${response.statusText}`);
25
25
  }
26
26
  }
27
- export function filterLinesByKeywords(logText, keywords) {
28
- return logText
29
- .split(/\r?\n/)
30
- .map((line) => line.trim())
31
- .filter((line) => keywords.some((keyword) => line.toLowerCase().includes(keyword)));
32
- }
@@ -0,0 +1,16 @@
1
+ import config from "./config.js";
2
+ import { trackMCP } from "./lib/instrumentation.js";
3
+ export function setupOnInitialized(server) {
4
+ const nodeVersion = process.versions.node;
5
+ // Check for Node.js version
6
+ if (nodeVersion < "18.0.0") {
7
+ throw new Error("Node version is not supported. Please upgrade to 18.0.0 or later.");
8
+ }
9
+ // Check for BrowserStack credentials
10
+ if (!config.browserstackUsername || !config.browserstackAccessKey) {
11
+ throw new Error("BrowserStack credentials are missing. Please provide a valid username and access key.");
12
+ }
13
+ server.server.oninitialized = () => {
14
+ trackMCP("started", server.server.getClientVersion());
15
+ };
16
+ }
@@ -36,13 +36,17 @@ async function runAccessibilityScan(name, pageURL, context) {
36
36
  }
37
37
  // Fetch CSV report link
38
38
  const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
39
- const { records } = await parseAccessibilityReportFromCSV(reportLink);
39
+ const { records, page_length, total_issues } = await parseAccessibilityReportFromCSV(reportLink);
40
40
  return {
41
41
  content: [
42
42
  {
43
43
  type: "text",
44
44
  text: `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
45
45
  },
46
+ {
47
+ type: "text",
48
+ text: `We found ${total_issues} issues. Below are the details of the ${page_length} most critical issues.`,
49
+ },
46
50
  {
47
51
  type: "text",
48
52
  text: `Scan results: ${JSON.stringify(records, null, 2)}`,
@@ -17,6 +17,7 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
17
17
  how_to_fix: row["How to fix this issue"],
18
18
  severity: (row["Severity"] || "unknown").trim(),
19
19
  }));
20
+ const totalIssues = all.length;
20
21
  // 2) Sort by severity
21
22
  const order = {
22
23
  critical: 0,
@@ -43,9 +44,12 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
43
44
  page.push(all[i]);
44
45
  charCursor += recStr.length;
45
46
  }
46
- const hasMore = idx + page.length < all.length;
47
+ const pageLength = page.length;
48
+ const hasMore = idx + pageLength < totalIssues;
47
49
  return {
48
50
  records: page,
49
51
  next_page: hasMore ? charCursor : null,
52
+ page_length: pageLength,
53
+ total_issues: totalIssues,
50
54
  };
51
55
  }
@@ -1,5 +1,5 @@
1
1
  import config from "../../config.js";
2
- import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
2
+ import { filterLinesByKeywords, validateLogResponse } from "./utils.js";
3
3
  const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
4
4
  // DEVICE LOGS
5
5
  export async function retrieveDeviceLogs(sessionId, buildId) {
@@ -10,9 +10,14 @@ export async function retrieveDeviceLogs(sessionId, buildId) {
10
10
  Authorization: `Basic ${auth}`,
11
11
  },
12
12
  });
13
- await assertOkResponse(response, "device logs");
13
+ const validationError = validateLogResponse(response, "device logs");
14
+ if (validationError)
15
+ return validationError.message;
14
16
  const logText = await response.text();
15
- return filterDeviceFailures(logText);
17
+ const logs = filterDeviceFailures(logText);
18
+ return logs.length > 0
19
+ ? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
20
+ : "No device failures found";
16
21
  }
17
22
  // APPIUM LOGS
18
23
  export async function retrieveAppiumLogs(sessionId, buildId) {
@@ -23,9 +28,14 @@ export async function retrieveAppiumLogs(sessionId, buildId) {
23
28
  Authorization: `Basic ${auth}`,
24
29
  },
25
30
  });
26
- await assertOkResponse(response, "Appium logs");
31
+ const validationError = validateLogResponse(response, "Appium logs");
32
+ if (validationError)
33
+ return validationError.message;
27
34
  const logText = await response.text();
28
- return filterAppiumFailures(logText);
35
+ const logs = filterAppiumFailures(logText);
36
+ return logs.length > 0
37
+ ? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
38
+ : "No Appium failures found";
29
39
  }
30
40
  // CRASH LOGS
31
41
  export async function retrieveCrashLogs(sessionId, buildId) {
@@ -36,9 +46,14 @@ export async function retrieveCrashLogs(sessionId, buildId) {
36
46
  Authorization: `Basic ${auth}`,
37
47
  },
38
48
  });
39
- await assertOkResponse(response, "crash logs");
49
+ const validationError = validateLogResponse(response, "crash logs");
50
+ if (validationError)
51
+ return validationError.message;
40
52
  const logText = await response.text();
41
- return filterCrashFailures(logText);
53
+ const logs = filterCrashFailures(logText);
54
+ return logs.length > 0
55
+ ? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
56
+ : "No crash failures found";
42
57
  }
43
58
  // FILTER HELPERS
44
59
  export function filterDeviceFailures(logText) {
@@ -1,5 +1,5 @@
1
1
  import config from "../../config.js";
2
- import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
2
+ import { filterLinesByKeywords, validateLogResponse, } from "./utils.js";
3
3
  const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
4
4
  // NETWORK LOGS
5
5
  export async function retrieveNetworkFailures(sessionId) {
@@ -11,30 +11,30 @@ export async function retrieveNetworkFailures(sessionId) {
11
11
  Authorization: `Basic ${auth}`,
12
12
  },
13
13
  });
14
- await assertOkResponse(response, "network logs");
14
+ const validationError = validateLogResponse(response, "network logs");
15
+ if (validationError)
16
+ return validationError.message;
15
17
  const networklogs = await response.json();
16
- // Filter for failure logs
17
- const failureEntries = networklogs.log.entries.filter((entry) => {
18
- return (entry.response.status === 0 ||
19
- entry.response.status >= 400 ||
20
- entry.response._error !== undefined);
21
- });
22
- // Return only the failure entries with some context
23
- return failureEntries.map((entry) => ({
24
- startedDateTime: entry.startedDateTime,
25
- request: {
26
- method: entry.request?.method,
27
- url: entry.request?.url,
28
- queryString: entry.request?.queryString,
29
- },
30
- response: {
31
- status: entry.response?.status,
32
- statusText: entry.response?.statusText,
33
- _error: entry.response?._error,
34
- },
35
- serverIPAddress: entry.serverIPAddress,
36
- time: entry.time,
37
- }));
18
+ const failureEntries = networklogs.log.entries.filter((entry) => entry.response.status === 0 ||
19
+ entry.response.status >= 400 ||
20
+ entry.response._error !== undefined);
21
+ return failureEntries.length > 0
22
+ ? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(failureEntries.map((entry) => ({
23
+ startedDateTime: entry.startedDateTime,
24
+ request: {
25
+ method: entry.request?.method,
26
+ url: entry.request?.url,
27
+ queryString: entry.request?.queryString,
28
+ },
29
+ response: {
30
+ status: entry.response?.status,
31
+ statusText: entry.response?.statusText,
32
+ _error: entry.response?._error,
33
+ },
34
+ serverIPAddress: entry.serverIPAddress,
35
+ time: entry.time,
36
+ })), null, 2)}`
37
+ : "No network failures found";
38
38
  }
39
39
  // SESSION LOGS
40
40
  export async function retrieveSessionFailures(sessionId) {
@@ -45,9 +45,14 @@ export async function retrieveSessionFailures(sessionId) {
45
45
  Authorization: `Basic ${auth}`,
46
46
  },
47
47
  });
48
- await assertOkResponse(response, "session logs");
48
+ const validationError = validateLogResponse(response, "session logs");
49
+ if (validationError)
50
+ return validationError.message;
49
51
  const logText = await response.text();
50
- return filterSessionFailures(logText);
52
+ const logs = filterSessionFailures(logText);
53
+ return logs.length > 0
54
+ ? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
55
+ : "No session failures found";
51
56
  }
52
57
  // CONSOLE LOGS
53
58
  export async function retrieveConsoleFailures(sessionId) {
@@ -58,9 +63,14 @@ export async function retrieveConsoleFailures(sessionId) {
58
63
  Authorization: `Basic ${auth}`,
59
64
  },
60
65
  });
61
- await assertOkResponse(response, "console logs");
66
+ const validationError = validateLogResponse(response, "console logs");
67
+ if (validationError)
68
+ return validationError.message;
62
69
  const logText = await response.text();
63
- return filterConsoleFailures(logText);
70
+ const logs = filterConsoleFailures(logText);
71
+ return logs.length > 0
72
+ ? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
73
+ : "No console failures found";
64
74
  }
65
75
  // FILTER: session logs
66
76
  export function filterSessionFailures(logText) {
@@ -0,0 +1,20 @@
1
+ export function validateLogResponse(response, logType) {
2
+ if (!response.ok) {
3
+ if (response.status === 404) {
4
+ return { message: `No ${logType} available for this session` };
5
+ }
6
+ if (response.status === 401 || response.status === 403) {
7
+ return {
8
+ message: `Unable to access ${logType} - please check your credentials`,
9
+ };
10
+ }
11
+ return { message: `Unable to fetch ${logType} at this time` };
12
+ }
13
+ return null;
14
+ }
15
+ export function filterLinesByKeywords(logText, keywords) {
16
+ return logText
17
+ .split(/\r?\n/)
18
+ .map((line) => line.trim())
19
+ .filter((line) => keywords.some((keyword) => line.toLowerCase().includes(keyword)));
20
+ }
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import logger from "../logger.js";
3
+ import { trackMCP } from "../lib/instrumentation.js";
3
4
  import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
4
5
  import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
5
- import { trackMCP } from "../lib/instrumentation.js";
6
6
  import { AppAutomateLogType, AutomateLogType, SessionType, } from "../lib/constants.js";
7
7
  // Main log fetcher function
8
8
  export async function getFailureLogs(args) {
@@ -47,68 +47,39 @@ export async function getFailureLogs(args) {
47
47
  ],
48
48
  };
49
49
  }
50
+ let response;
50
51
  // eslint-disable-next-line no-useless-catch
51
52
  try {
52
53
  for (const logType of validLogTypes) {
53
54
  switch (logType) {
54
55
  case AutomateLogType.NetworkLogs: {
55
- const logs = await retrieveNetworkFailures(args.sessionId);
56
- results.push({
57
- type: "text",
58
- text: logs.length > 0
59
- ? `Network Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
60
- : "No network failures found",
61
- });
56
+ response = await retrieveNetworkFailures(args.sessionId);
57
+ results.push({ type: "text", text: response });
62
58
  break;
63
59
  }
64
60
  case AutomateLogType.SessionLogs: {
65
- const logs = await retrieveSessionFailures(args.sessionId);
66
- results.push({
67
- type: "text",
68
- text: logs.length > 0
69
- ? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
70
- : "No session failures found",
71
- });
61
+ response = await retrieveSessionFailures(args.sessionId);
62
+ results.push({ type: "text", text: response });
72
63
  break;
73
64
  }
74
65
  case AutomateLogType.ConsoleLogs: {
75
- const logs = await retrieveConsoleFailures(args.sessionId);
76
- results.push({
77
- type: "text",
78
- text: logs.length > 0
79
- ? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
80
- : "No console failures found",
81
- });
66
+ response = await retrieveConsoleFailures(args.sessionId);
67
+ results.push({ type: "text", text: response });
82
68
  break;
83
69
  }
84
70
  case AppAutomateLogType.DeviceLogs: {
85
- const logs = await retrieveDeviceLogs(args.sessionId, args.buildId);
86
- results.push({
87
- type: "text",
88
- text: logs.length > 0
89
- ? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
90
- : "No device failures found",
91
- });
71
+ response = await retrieveDeviceLogs(args.sessionId, args.buildId);
72
+ results.push({ type: "text", text: response });
92
73
  break;
93
74
  }
94
75
  case AppAutomateLogType.AppiumLogs: {
95
- const logs = await retrieveAppiumLogs(args.sessionId, args.buildId);
96
- results.push({
97
- type: "text",
98
- text: logs.length > 0
99
- ? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
100
- : "No Appium failures found",
101
- });
76
+ response = await retrieveAppiumLogs(args.sessionId, args.buildId);
77
+ results.push({ type: "text", text: response });
102
78
  break;
103
79
  }
104
80
  case AppAutomateLogType.CrashLogs: {
105
- const logs = await retrieveCrashLogs(args.sessionId, args.buildId);
106
- results.push({
107
- type: "text",
108
- text: logs.length > 0
109
- ? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
110
- : "No crash failures found",
111
- });
81
+ response = await retrieveCrashLogs(args.sessionId, args.buildId);
82
+ results.push({ type: "text", text: response });
112
83
  break;
113
84
  }
114
85
  }
@@ -2,6 +2,7 @@ import axios from "axios";
2
2
  import config from "../../config.js";
3
3
  import { z } from "zod";
4
4
  import { formatAxiosError } from "../../lib/error.js"; // or correct path
5
+ import { projectIdentifierToId } from "../testmanagement-utils/TCG-utils/api.js";
5
6
  // Schema for combined project/folder creation
6
7
  export const CreateProjFoldSchema = z.object({
7
8
  project_name: z
@@ -78,11 +79,16 @@ export async function createProjectOrFolder(args) {
78
79
  }
79
80
  // Folder created successfully
80
81
  const folder = res.data.folder;
82
+ const projectId = await projectIdentifierToId(projId);
81
83
  return {
82
84
  content: [
83
85
  {
84
86
  type: "text",
85
- text: `Folder created: ID=${folder.id}, name=${folder.name} in project with identifier ${projId}`,
87
+ text: `Folder successfully created:
88
+ - ID: ${folder.id}
89
+ - Name: ${folder.name}
90
+ - Project Identifier: ${projId}
91
+ Access it here: https://test-management.browserstack.com/projects/${projectId}/folder/${folder.id}/`,
86
92
  },
87
93
  ],
88
94
  };
@@ -2,6 +2,7 @@ import axios from "axios";
2
2
  import config from "../../config.js";
3
3
  import { z } from "zod";
4
4
  import { formatAxiosError } from "../../lib/error.js"; // or correct
5
+ import { projectIdentifierToId } from "./TCG-utils/api.js";
5
6
  export const CreateTestCaseSchema = z.object({
6
7
  project_identifier: z
7
8
  .string()
@@ -90,13 +91,21 @@ export async function createTestCase(params) {
90
91
  };
91
92
  }
92
93
  const tc = data.test_case;
94
+ const projectId = await projectIdentifierToId(params.project_identifier);
93
95
  return {
94
96
  content: [
95
97
  {
96
98
  type: "text",
97
- text: `Successfully created test case ${tc.identifier}: ${tc.title}`,
99
+ text: `Test case successfully created:
100
+ - Identifier: ${tc.identifier}
101
+ - Title: ${tc.title}
102
+
103
+ You can view it here: https://test-management.browserstack.com/projects/${projectId}/folder/search?q=${tc.identifier}`,
104
+ },
105
+ {
106
+ type: "text",
107
+ text: JSON.stringify(tc, null, 2),
98
108
  },
99
- { type: "text", text: JSON.stringify(tc, null, 2) },
100
109
  ],
101
110
  };
102
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "repository": {