@browserstack/mcp-server 1.0.10 → 1.0.12

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.
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVersion = resolveVersion;
4
+ /**
5
+ * If req === "latest" or "oldest", returns max/min numeric (or lex)
6
+ * Else if exact match, returns that
7
+ * Else picks the numerically closest (or first)
8
+ */
9
+ function resolveVersion(requested, available) {
10
+ // strip duplicates & sort
11
+ const uniq = Array.from(new Set(available));
12
+ // pick min/max
13
+ if (requested === "latest" || requested === "oldest") {
14
+ // try numeric
15
+ const nums = uniq
16
+ .map((v) => ({ v, n: parseFloat(v) }))
17
+ .filter((x) => !isNaN(x.n))
18
+ .sort((a, b) => a.n - b.n);
19
+ if (nums.length) {
20
+ return requested === "latest" ? nums[nums.length - 1].v : nums[0].v;
21
+ }
22
+ // fallback lex
23
+ const lex = uniq.slice().sort();
24
+ return requested === "latest" ? lex[lex.length - 1] : lex[0];
25
+ }
26
+ // exact?
27
+ if (uniq.includes(requested)) {
28
+ return requested;
29
+ }
30
+ // try closest numeric
31
+ const reqNum = parseFloat(requested);
32
+ const nums = uniq
33
+ .map((v) => ({ v, n: parseFloat(v) }))
34
+ .filter((x) => !isNaN(x.n));
35
+ if (!isNaN(reqNum) && nums.length) {
36
+ let best = nums[0], bestDiff = Math.abs(nums[0].n - reqNum);
37
+ for (const x of nums) {
38
+ const d = Math.abs(x.n - reqNum);
39
+ if (d < bestDiff) {
40
+ best = x;
41
+ bestDiff = d;
42
+ }
43
+ }
44
+ return best.v;
45
+ }
46
+ // final fallback
47
+ return uniq[0];
48
+ }
@@ -3,90 +3,99 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.startBrowserLiveSession = startBrowserLiveSession;
7
6
  exports.default = addBrowserLiveTools;
8
7
  const zod_1 = require("zod");
9
- const start_session_1 = require("./live-utils/start-session");
10
8
  const logger_1 = __importDefault(require("../logger"));
11
- const local_1 = require("../lib/local");
9
+ const start_session_1 = require("./live-utils/start-session");
10
+ const types_1 = require("./live-utils/types");
11
+ const instrumentation_1 = require("../lib/instrumentation");
12
+ // Define the schema shape
13
+ const LiveArgsShape = {
14
+ platformType: zod_1.z
15
+ .nativeEnum(types_1.PlatformType)
16
+ .describe("Must be 'desktop' or 'mobile'"),
17
+ desiredURL: zod_1.z.string().url().describe("The URL to test"),
18
+ desiredOS: zod_1.z
19
+ .enum(["Windows", "OS X", "android", "ios", "winphone"])
20
+ .describe("Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')"),
21
+ desiredOSVersion: zod_1.z
22
+ .string()
23
+ .describe("The OS version must be specified as a version number (e.g., '10', '14.0') or as a keyword such as 'latest' or 'oldest'. Normalize variations like 'newest' or 'most recent' to 'latest', and terms like 'earliest' or 'first' to 'oldest'. For macOS, version names (e.g., 'Sequoia') must be used instead of numeric versions."),
24
+ desiredBrowser: zod_1.z
25
+ .enum(["chrome", "firefox", "safari", "edge", "ie"])
26
+ .describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"),
27
+ desiredBrowserVersion: zod_1.z
28
+ .string()
29
+ .optional()
30
+ .describe("Browser version for desktop (e.g. '133.2', 'latest'). If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
31
+ desiredDevice: zod_1.z.string().optional().describe("Device name for mobile"),
32
+ };
33
+ const LiveArgsSchema = zod_1.z.object(LiveArgsShape);
12
34
  /**
13
- * Launches a Browser Live Session on BrowserStack.
35
+ * Launches a desktop browser session
14
36
  */
15
- async function startBrowserLiveSession(args) {
16
- if (!args.desiredBrowser) {
17
- throw new Error("You must provide a desiredBrowser.");
18
- }
19
- if (!args.desiredURL) {
20
- throw new Error("You must provide a desiredURL.");
21
- }
22
- if (!args.desiredOS) {
23
- throw new Error("You must provide a desiredOS.");
24
- }
25
- if (!args.desiredOSVersion) {
26
- throw new Error("You must provide a desiredOSVersion.");
27
- }
28
- if (!args.desiredBrowserVersion) {
29
- throw new Error("You must provide a desiredBrowserVersion.");
30
- }
31
- // Validate URL format
32
- try {
33
- new URL(args.desiredURL);
34
- }
35
- catch (error) {
36
- logger_1.default.error("Invalid URL format: %s", error);
37
- throw new Error("The provided URL is invalid.");
38
- }
39
- const isLocal = (0, local_1.isLocalURL)(args.desiredURL);
40
- if (isLocal) {
41
- await (0, local_1.ensureLocalBinarySetup)();
42
- }
43
- else {
44
- await (0, local_1.killExistingBrowserStackLocalProcesses)();
45
- }
46
- const launchUrl = await (0, start_session_1.startBrowserSession)({
47
- browser: args.desiredBrowser,
37
+ async function launchDesktopSession(args) {
38
+ if (!args.desiredBrowser)
39
+ throw new Error("You must provide a desiredBrowser");
40
+ if (!args.desiredBrowserVersion)
41
+ throw new Error("You must provide a desiredBrowserVersion");
42
+ return (0, start_session_1.startBrowserSession)({
43
+ platformType: types_1.PlatformType.DESKTOP,
44
+ url: args.desiredURL,
48
45
  os: args.desiredOS,
49
46
  osVersion: args.desiredOSVersion,
50
- url: args.desiredURL,
47
+ browser: args.desiredBrowser,
51
48
  browserVersion: args.desiredBrowserVersion,
52
- isLocal,
53
49
  });
50
+ }
51
+ /**
52
+ * Launches a mobile browser session
53
+ */
54
+ async function launchMobileSession(args) {
55
+ if (!args.desiredDevice)
56
+ throw new Error("You must provide a desiredDevice");
57
+ return (0, start_session_1.startBrowserSession)({
58
+ platformType: types_1.PlatformType.MOBILE,
59
+ browser: args.desiredBrowser,
60
+ url: args.desiredURL,
61
+ os: args.desiredOS,
62
+ osVersion: args.desiredOSVersion,
63
+ device: args.desiredDevice,
64
+ });
65
+ }
66
+ /**
67
+ * Handles the core logic for running a browser session
68
+ */
69
+ async function runBrowserSession(rawArgs) {
70
+ // Validate and narrow
71
+ const args = LiveArgsSchema.parse(rawArgs);
72
+ // Branch desktop vs mobile and delegate
73
+ const launchUrl = args.platformType === types_1.PlatformType.DESKTOP
74
+ ? await launchDesktopSession(args)
75
+ : await launchMobileSession(args);
54
76
  return {
55
77
  content: [
56
78
  {
57
79
  type: "text",
58
- text: `Successfully started a browser session. If a browser window did not open automatically, use the following URL to start the session: ${launchUrl}`,
80
+ text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
59
81
  },
60
82
  ],
61
83
  };
62
84
  }
63
85
  function addBrowserLiveTools(server) {
64
- server.tool("runBrowserLiveSession", "Use this tool when user wants to manually check their website on a particular browser and OS combination using BrowserStack's cloud infrastructure. Can be used to debug layout issues, compatibility problems, etc.", {
65
- desiredBrowser: zod_1.z
66
- .enum(["Chrome", "IE", "Firefox", "Safari", "Edge"])
67
- .describe("The browser to run the test on. Example: 'Chrome', 'IE', 'Safari', 'Edge'. Always ask the user for the browser they want to use, do not assume it."),
68
- desiredOSVersion: zod_1.z
69
- .string()
70
- .describe("The OS version to run the browser on. Example: '10' for Windows, '12' for macOS, '14' for iOS"),
71
- desiredOS: zod_1.z
72
- .enum(["Windows", "OS X"])
73
- .describe("The operating system to run the browser on. Example: 'Windows', 'OS X'"),
74
- desiredURL: zod_1.z
75
- .string()
76
- .describe("The URL of the website to test. This can be a local URL (e.g., http://localhost:3000) or a public URL. Always ask the user for the URL, do not assume it."),
77
- desiredBrowserVersion: zod_1.z
78
- .string()
79
- .describe("The version of the browser to use. Example: 133.0, 134.0, 87.0"),
80
- }, async (args) => {
86
+ server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape, async (args) => {
81
87
  try {
82
- return startBrowserLiveSession(args);
88
+ (0, instrumentation_1.trackMCP)("runBrowserLiveSession", server.server.getClientVersion());
89
+ return await runBrowserSession(args);
83
90
  }
84
91
  catch (error) {
92
+ logger_1.default.error("Live session failed: %s", error);
93
+ (0, instrumentation_1.trackMCP)("runBrowserLiveSession", server.server.getClientVersion(), error);
85
94
  return {
86
95
  content: [
87
96
  {
88
97
  type: "text",
89
- text: `Failed to start a browser live session. Error: ${error}. Please open an issue on GitHub if the problem persists`,
98
+ text: `Failed to start a browser live session. Error: ${error}`,
90
99
  isError: true,
91
100
  },
92
101
  ],
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.getFailuresInLastRun = getFailuresInLastRun;
4
7
  exports.default = addObservabilityTools;
5
8
  const zod_1 = require("zod");
6
9
  const api_1 = require("../lib/api");
10
+ const instrumentation_1 = require("../lib/instrumentation");
11
+ const logger_1 = __importDefault(require("../logger"));
7
12
  async function getFailuresInLastRun(buildName, projectName) {
8
13
  const buildsData = await (0, api_1.getLatestO11YBuildInfo)(buildName, projectName);
9
14
  const observabilityUrl = buildsData.observability_url;
@@ -40,9 +45,12 @@ function addObservabilityTools(server) {
40
45
  .describe("Name of the project to get failures for. This is the 'projectName' key in the browserstack.yml file. If not sure, ask the user for the project name."),
41
46
  }, async (args) => {
42
47
  try {
43
- return getFailuresInLastRun(args.buildName, args.projectName);
48
+ (0, instrumentation_1.trackMCP)("getFailuresInLastRun", server.server.getClientVersion());
49
+ return await getFailuresInLastRun(args.buildName, args.projectName);
44
50
  }
45
51
  catch (error) {
52
+ logger_1.default.error("Failed to get failures in the last run: %s", error);
53
+ (0, instrumentation_1.trackMCP)("getFailuresInLastRun", server.server.getClientVersion(), error);
46
54
  return {
47
55
  content: [
48
56
  {
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CreateTestRunSchema = void 0;
7
+ exports.createTestRun = createTestRun;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = __importDefault(require("../../config"));
10
+ const zod_1 = require("zod");
11
+ const error_1 = require("../../lib/error");
12
+ /**
13
+ * Schema for creating a test run.
14
+ */
15
+ exports.CreateTestRunSchema = zod_1.z.object({
16
+ project_identifier: zod_1.z
17
+ .string()
18
+ .describe("Identifier of the project in which to create the test run."),
19
+ test_run: zod_1.z.object({
20
+ name: zod_1.z.string().describe("Name of the test run"),
21
+ description: zod_1.z
22
+ .string()
23
+ .optional()
24
+ .describe("Brief information about the test run"),
25
+ run_state: zod_1.z
26
+ .enum([
27
+ "new_run",
28
+ "in_progress",
29
+ "under_review",
30
+ "rejected",
31
+ "done",
32
+ "closed",
33
+ ])
34
+ .optional()
35
+ .describe("State of the test run. One of new_run, in_progress, under_review, rejected, done, closed"),
36
+ issues: zod_1.z.array(zod_1.z.string()).optional().describe("Linked issue IDs"),
37
+ issue_tracker: zod_1.z
38
+ .object({ name: zod_1.z.string(), host: zod_1.z.string().url() })
39
+ .optional()
40
+ .describe("Issue tracker configuration"),
41
+ test_cases: zod_1.z
42
+ .array(zod_1.z.string())
43
+ .optional()
44
+ .describe("List of test case IDs"),
45
+ folder_ids: zod_1.z
46
+ .array(zod_1.z.number())
47
+ .optional()
48
+ .describe("Folder IDs to include"),
49
+ }),
50
+ });
51
+ /**
52
+ * Creates a test run via BrowserStack Test Management API.
53
+ */
54
+ async function createTestRun(rawArgs) {
55
+ try {
56
+ const inputArgs = {
57
+ ...rawArgs,
58
+ test_run: {
59
+ ...rawArgs.test_run,
60
+ include_all: false,
61
+ },
62
+ };
63
+ const args = exports.CreateTestRunSchema.parse(inputArgs);
64
+ const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-runs`;
65
+ const response = await axios_1.default.post(url, { test_run: args.test_run }, {
66
+ auth: {
67
+ username: config_1.default.browserstackUsername,
68
+ password: config_1.default.browserstackAccessKey,
69
+ },
70
+ headers: { "Content-Type": "application/json" },
71
+ });
72
+ const data = response.data;
73
+ if (!data.success) {
74
+ throw new Error(`API returned unsuccessful response: ${JSON.stringify(data)}`);
75
+ }
76
+ // Assume data.test_run contains created run info
77
+ const created = data.test_run || data;
78
+ const runId = created.identifier || created.id || created.name;
79
+ const summary = `Successfully created test run ${runId}`;
80
+ return {
81
+ content: [
82
+ { type: "text", text: summary },
83
+ { type: "text", text: JSON.stringify(created, null, 2) },
84
+ ],
85
+ };
86
+ }
87
+ catch (err) {
88
+ return (0, error_1.formatAxiosError)(err, "Failed to create test run");
89
+ }
90
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ListTestCasesSchema = void 0;
7
+ exports.listTestCases = listTestCases;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = __importDefault(require("../../config"));
10
+ const zod_1 = require("zod");
11
+ const error_1 = require("../../lib/error");
12
+ /**
13
+ * Schema for listing test cases with optional filters.
14
+ */
15
+ exports.ListTestCasesSchema = zod_1.z.object({
16
+ project_identifier: zod_1.z
17
+ .string()
18
+ .describe("Identifier of the project to fetch test cases from. Example: PR-12345"),
19
+ folder_id: zod_1.z
20
+ .string()
21
+ .optional()
22
+ .describe("If provided, only return cases in this folder."),
23
+ case_type: zod_1.z
24
+ .string()
25
+ .optional()
26
+ .describe("Comma-separated list of case types (e.g. functional,regression)."),
27
+ priority: zod_1.z
28
+ .string()
29
+ .optional()
30
+ .describe("Comma-separated list of priorities (e.g. critical,medium,low)."),
31
+ p: zod_1.z.number().optional().describe("Page number."),
32
+ });
33
+ /**
34
+ * Calls BrowserStack Test Management to list test cases with filters.
35
+ */
36
+ async function listTestCases(args) {
37
+ try {
38
+ // Build query string
39
+ const params = new URLSearchParams();
40
+ if (args.folder_id)
41
+ params.append("folder_id", args.folder_id);
42
+ if (args.case_type)
43
+ params.append("case_type", args.case_type);
44
+ if (args.priority)
45
+ params.append("priority", args.priority);
46
+ if (args.p !== undefined)
47
+ params.append("p", args.p.toString());
48
+ const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-cases?${params.toString()}`;
49
+ const resp = await axios_1.default.get(url, {
50
+ auth: {
51
+ username: config_1.default.browserstackUsername,
52
+ password: config_1.default.browserstackAccessKey,
53
+ },
54
+ });
55
+ const resp_data = resp.data;
56
+ if (!resp_data.success) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: `Failed to list test cases: ${JSON.stringify(resp_data)}`,
62
+ isError: true,
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+ const { test_cases, info } = resp_data;
69
+ const count = info?.count ?? test_cases.length;
70
+ // Summary for more focused output
71
+ const summary = test_cases
72
+ .map((tc) => `• ${tc.identifier}: ${tc.title} [${tc.case_type} | ${tc.priority}]`)
73
+ .join("\n");
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `Found ${count} test case(s):\n\n${summary}`,
79
+ },
80
+ {
81
+ type: "text",
82
+ text: JSON.stringify(test_cases, null, 2),
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ catch (err) {
88
+ return (0, error_1.formatAxiosError)(err, "Failed to list test cases");
89
+ }
90
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ListTestRunsSchema = void 0;
7
+ exports.listTestRuns = listTestRuns;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = __importDefault(require("../../config"));
10
+ const zod_1 = require("zod");
11
+ const error_1 = require("../../lib/error");
12
+ /**
13
+ * Schema for listing test runs with optional filters.
14
+ */
15
+ exports.ListTestRunsSchema = zod_1.z.object({
16
+ project_identifier: zod_1.z
17
+ .string()
18
+ .describe("Identifier of the project to fetch test runs from (e.g., PR-12345)"),
19
+ run_state: zod_1.z
20
+ .string()
21
+ .optional()
22
+ .describe("Return all test runs with this state (comma-separated)"),
23
+ });
24
+ /**
25
+ * Fetches and formats the list of test runs for a project.
26
+ */
27
+ async function listTestRuns(args) {
28
+ try {
29
+ const params = new URLSearchParams();
30
+ if (args.run_state) {
31
+ params.set("run_state", args.run_state);
32
+ }
33
+ const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-runs?` + params.toString();
34
+ const resp = await axios_1.default.get(url, {
35
+ auth: {
36
+ username: config_1.default.browserstackUsername,
37
+ password: config_1.default.browserstackAccessKey,
38
+ },
39
+ });
40
+ const data = resp.data;
41
+ if (!data.success) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: `Failed to list test runs: ${JSON.stringify(data)}`,
47
+ isError: true,
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ const runs = data.test_runs;
54
+ const count = runs.length;
55
+ const summary = runs
56
+ .map((tr) => `• ${tr.identifier}: ${tr.name} [${tr.run_state}]`)
57
+ .join("\n");
58
+ return {
59
+ content: [
60
+ { type: "text", text: `Found ${count} test run(s):\n\n${summary}` },
61
+ { type: "text", text: JSON.stringify(runs, null, 2) },
62
+ ],
63
+ };
64
+ }
65
+ catch (err) {
66
+ return (0, error_1.formatAxiosError)(err, "Failed to list test runs");
67
+ }
68
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UpdateTestRunSchema = void 0;
7
+ exports.updateTestRun = updateTestRun;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = __importDefault(require("../../config"));
10
+ const zod_1 = require("zod");
11
+ const error_1 = require("../../lib/error");
12
+ /**
13
+ * Schema for updating a test run with partial fields.
14
+ */
15
+ exports.UpdateTestRunSchema = zod_1.z.object({
16
+ project_identifier: zod_1.z
17
+ .string()
18
+ .describe("Project identifier (e.g., PR-12345)"),
19
+ test_run_id: zod_1.z.string().describe("Test run identifier (e.g., TR-678)"),
20
+ test_run: zod_1.z.object({
21
+ name: zod_1.z.string().optional().describe("New name of the test run"),
22
+ run_state: zod_1.z
23
+ .enum([
24
+ "new_run",
25
+ "in_progress",
26
+ "under_review",
27
+ "rejected",
28
+ "done",
29
+ "closed",
30
+ ])
31
+ .optional()
32
+ .describe("Updated state of the test run"),
33
+ }),
34
+ });
35
+ /**
36
+ * Partially updates an existing test run.
37
+ */
38
+ async function updateTestRun(args) {
39
+ try {
40
+ const body = { test_run: args.test_run };
41
+ const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(args.project_identifier)}/test-runs/${encodeURIComponent(args.test_run_id)}/update`;
42
+ const resp = await axios_1.default.patch(url, body, {
43
+ auth: {
44
+ username: config_1.default.browserstackUsername,
45
+ password: config_1.default.browserstackAccessKey,
46
+ },
47
+ });
48
+ const data = resp.data;
49
+ if (!data.success) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: `Failed to update test run: ${JSON.stringify(data)}`,
55
+ isError: true,
56
+ },
57
+ ],
58
+ isError: true,
59
+ };
60
+ }
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: `Successfully updated test run ${args.test_run_id}`,
66
+ },
67
+ { type: "text", text: JSON.stringify(data.testrun || data, null, 2) },
68
+ ],
69
+ };
70
+ }
71
+ catch (err) {
72
+ return (0, error_1.formatAxiosError)(err, "Failed to update test run");
73
+ }
74
+ }