@browserstack/mcp-server 1.0.12 → 1.0.14

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 (52) hide show
  1. package/README.md +22 -0
  2. package/dist/config.js +2 -6
  3. package/dist/index.js +31 -30
  4. package/dist/lib/api.js +3 -57
  5. package/dist/lib/constants.js +14 -0
  6. package/dist/lib/device-cache.js +32 -33
  7. package/dist/lib/error.js +3 -6
  8. package/dist/lib/fuzzy.js +1 -4
  9. package/dist/lib/inmemory-store.js +1 -0
  10. package/dist/lib/instrumentation.js +12 -18
  11. package/dist/lib/local.js +27 -35
  12. package/dist/lib/utils.js +29 -4
  13. package/dist/logger.js +4 -9
  14. package/dist/tools/accessibility.js +9 -12
  15. package/dist/tools/accessiblity-utils/accessibility.js +14 -22
  16. package/dist/tools/appautomate-utils/appautomate.js +95 -0
  17. package/dist/tools/appautomate.js +116 -0
  18. package/dist/tools/applive-utils/fuzzy-search.js +3 -6
  19. package/dist/tools/applive-utils/start-session.js +14 -20
  20. package/dist/tools/applive-utils/upload-app.js +12 -51
  21. package/dist/tools/applive.js +18 -25
  22. package/dist/tools/automate-utils/fetch-screenshots.js +59 -0
  23. package/dist/tools/automate.js +44 -37
  24. package/dist/tools/bstack-sdk.js +14 -18
  25. package/dist/tools/failurelogs-utils/app-automate.js +88 -0
  26. package/dist/tools/failurelogs-utils/automate.js +97 -0
  27. package/dist/tools/getFailureLogs.js +173 -0
  28. package/dist/tools/live-utils/desktop-filter.js +8 -11
  29. package/dist/tools/live-utils/mobile-filter.js +7 -10
  30. package/dist/tools/live-utils/start-session.js +17 -23
  31. package/dist/tools/live-utils/types.js +2 -5
  32. package/dist/tools/live-utils/version-resolver.js +1 -4
  33. package/dist/tools/live.js +23 -29
  34. package/dist/tools/observability.js +12 -19
  35. package/dist/tools/sdk-utils/constants.js +3 -9
  36. package/dist/tools/sdk-utils/instructions.js +4 -9
  37. package/dist/tools/sdk-utils/types.js +1 -2
  38. package/dist/tools/testmanagement-utils/TCG-utils/api.js +259 -0
  39. package/dist/tools/testmanagement-utils/TCG-utils/config.js +5 -0
  40. package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +53 -0
  41. package/dist/tools/testmanagement-utils/TCG-utils/types.js +8 -0
  42. package/dist/tools/testmanagement-utils/add-test-result.js +61 -0
  43. package/dist/tools/testmanagement-utils/create-project-folder.js +21 -28
  44. package/dist/tools/testmanagement-utils/create-testcase.js +30 -38
  45. package/dist/tools/testmanagement-utils/create-testrun.js +23 -30
  46. package/dist/tools/testmanagement-utils/list-testcases.js +16 -23
  47. package/dist/tools/testmanagement-utils/list-testruns.js +13 -20
  48. package/dist/tools/testmanagement-utils/testcase-from-file.js +42 -0
  49. package/dist/tools/testmanagement-utils/update-testrun.js +16 -23
  50. package/dist/tools/testmanagement-utils/upload-file.js +101 -0
  51. package/dist/tools/testmanagement.js +115 -46
  52. package/package.json +10 -6
@@ -1,37 +1,31 @@
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.startBrowserSession = startBrowserSession;
7
- const logger_1 = __importDefault(require("../../logger"));
8
- const child_process_1 = __importDefault(require("child_process"));
9
- const desktop_filter_1 = require("./desktop-filter");
10
- const mobile_filter_1 = require("./mobile-filter");
11
- const types_1 = require("./types");
12
- const local_1 = require("../../lib/local");
1
+ import logger from "../../logger.js";
2
+ import childProcess from "child_process";
3
+ import { filterDesktop } from "./desktop-filter.js";
4
+ import { filterMobile } from "./mobile-filter.js";
5
+ import { PlatformType, } from "./types.js";
6
+ import { isLocalURL, ensureLocalBinarySetup, killExistingBrowserStackLocalProcesses, } from "../../lib/local.js";
13
7
  /**
14
8
  * Prepares local tunnel setup based on URL type
15
9
  */
16
10
  async function prepareLocalTunnel(url) {
17
- const isLocal = (0, local_1.isLocalURL)(url);
11
+ const isLocal = isLocalURL(url);
18
12
  if (isLocal) {
19
- await (0, local_1.ensureLocalBinarySetup)();
13
+ await ensureLocalBinarySetup();
20
14
  }
21
15
  else {
22
- await (0, local_1.killExistingBrowserStackLocalProcesses)();
16
+ await killExistingBrowserStackLocalProcesses();
23
17
  }
24
18
  return isLocal;
25
19
  }
26
20
  /**
27
21
  * Entrypoint: detects platformType & delegates.
28
22
  */
29
- async function startBrowserSession(args) {
30
- const entry = args.platformType === types_1.PlatformType.DESKTOP
31
- ? await (0, desktop_filter_1.filterDesktop)(args)
32
- : await (0, mobile_filter_1.filterMobile)(args);
23
+ export async function startBrowserSession(args) {
24
+ const entry = args.platformType === PlatformType.DESKTOP
25
+ ? await filterDesktop(args)
26
+ : await filterMobile(args);
33
27
  const isLocal = await prepareLocalTunnel(args.url);
34
- const url = args.platformType === types_1.PlatformType.DESKTOP
28
+ const url = args.platformType === PlatformType.DESKTOP
35
29
  ? buildDesktopUrl(args, entry, isLocal)
36
30
  : buildMobileUrl(args, entry, isLocal);
37
31
  openBrowser(url);
@@ -81,14 +75,14 @@ function openBrowser(launchUrl) {
81
75
  ? ["cmd", "/c", "start", launchUrl]
82
76
  : ["xdg-open", launchUrl];
83
77
  // nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
84
- const child = child_process_1.default.spawn(command[0], command.slice(1), {
78
+ const child = childProcess.spawn(command[0], command.slice(1), {
85
79
  stdio: "ignore",
86
80
  detached: true,
87
81
  });
88
- child.on("error", (err) => logger_1.default.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
82
+ child.on("error", (err) => logger.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
89
83
  child.unref();
90
84
  }
91
85
  catch (err) {
92
- logger_1.default.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
86
+ logger.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
93
87
  }
94
88
  }
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlatformType = void 0;
4
- var PlatformType;
1
+ export var PlatformType;
5
2
  (function (PlatformType) {
6
3
  PlatformType["DESKTOP"] = "desktop";
7
4
  PlatformType["MOBILE"] = "mobile";
8
- })(PlatformType || (exports.PlatformType = PlatformType = {}));
5
+ })(PlatformType || (PlatformType = {}));
@@ -1,12 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveVersion = resolveVersion;
4
1
  /**
5
2
  * If req === "latest" or "oldest", returns max/min numeric (or lex)
6
3
  * Else if exact match, returns that
7
4
  * Else picks the numerically closest (or first)
8
5
  */
9
- function resolveVersion(requested, available) {
6
+ export function resolveVersion(requested, available) {
10
7
  // strip duplicates & sort
11
8
  const uniq = Array.from(new Set(available));
12
9
  // pick min/max
@@ -1,36 +1,30 @@
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.default = addBrowserLiveTools;
7
- const zod_1 = require("zod");
8
- const logger_1 = __importDefault(require("../logger"));
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");
1
+ import { z } from "zod";
2
+ import logger from "../logger.js";
3
+ import { startBrowserSession } from "./live-utils/start-session.js";
4
+ import { PlatformType } from "./live-utils/types.js";
5
+ import { trackMCP } from "../lib/instrumentation.js";
12
6
  // Define the schema shape
13
7
  const LiveArgsShape = {
14
- platformType: zod_1.z
15
- .nativeEnum(types_1.PlatformType)
8
+ platformType: z
9
+ .nativeEnum(PlatformType)
16
10
  .describe("Must be 'desktop' or 'mobile'"),
17
- desiredURL: zod_1.z.string().url().describe("The URL to test"),
18
- desiredOS: zod_1.z
11
+ desiredURL: z.string().url().describe("The URL to test"),
12
+ desiredOS: z
19
13
  .enum(["Windows", "OS X", "android", "ios", "winphone"])
20
14
  .describe("Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')"),
21
- desiredOSVersion: zod_1.z
15
+ desiredOSVersion: z
22
16
  .string()
23
17
  .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
18
+ desiredBrowser: z
25
19
  .enum(["chrome", "firefox", "safari", "edge", "ie"])
26
20
  .describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"),
27
- desiredBrowserVersion: zod_1.z
21
+ desiredBrowserVersion: z
28
22
  .string()
29
23
  .optional()
30
24
  .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"),
25
+ desiredDevice: z.string().optional().describe("Device name for mobile"),
32
26
  };
33
- const LiveArgsSchema = zod_1.z.object(LiveArgsShape);
27
+ const LiveArgsSchema = z.object(LiveArgsShape);
34
28
  /**
35
29
  * Launches a desktop browser session
36
30
  */
@@ -39,8 +33,8 @@ async function launchDesktopSession(args) {
39
33
  throw new Error("You must provide a desiredBrowser");
40
34
  if (!args.desiredBrowserVersion)
41
35
  throw new Error("You must provide a desiredBrowserVersion");
42
- return (0, start_session_1.startBrowserSession)({
43
- platformType: types_1.PlatformType.DESKTOP,
36
+ return startBrowserSession({
37
+ platformType: PlatformType.DESKTOP,
44
38
  url: args.desiredURL,
45
39
  os: args.desiredOS,
46
40
  osVersion: args.desiredOSVersion,
@@ -54,8 +48,8 @@ async function launchDesktopSession(args) {
54
48
  async function launchMobileSession(args) {
55
49
  if (!args.desiredDevice)
56
50
  throw new Error("You must provide a desiredDevice");
57
- return (0, start_session_1.startBrowserSession)({
58
- platformType: types_1.PlatformType.MOBILE,
51
+ return startBrowserSession({
52
+ platformType: PlatformType.MOBILE,
59
53
  browser: args.desiredBrowser,
60
54
  url: args.desiredURL,
61
55
  os: args.desiredOS,
@@ -70,7 +64,7 @@ async function runBrowserSession(rawArgs) {
70
64
  // Validate and narrow
71
65
  const args = LiveArgsSchema.parse(rawArgs);
72
66
  // Branch desktop vs mobile and delegate
73
- const launchUrl = args.platformType === types_1.PlatformType.DESKTOP
67
+ const launchUrl = args.platformType === PlatformType.DESKTOP
74
68
  ? await launchDesktopSession(args)
75
69
  : await launchMobileSession(args);
76
70
  return {
@@ -82,15 +76,15 @@ async function runBrowserSession(rawArgs) {
82
76
  ],
83
77
  };
84
78
  }
85
- function addBrowserLiveTools(server) {
79
+ export default function addBrowserLiveTools(server) {
86
80
  server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape, async (args) => {
87
81
  try {
88
- (0, instrumentation_1.trackMCP)("runBrowserLiveSession", server.server.getClientVersion());
82
+ trackMCP("runBrowserLiveSession", server.server.getClientVersion());
89
83
  return await runBrowserSession(args);
90
84
  }
91
85
  catch (error) {
92
- logger_1.default.error("Live session failed: %s", error);
93
- (0, instrumentation_1.trackMCP)("runBrowserLiveSession", server.server.getClientVersion(), error);
86
+ logger.error("Live session failed: %s", error);
87
+ trackMCP("runBrowserLiveSession", server.server.getClientVersion(), error);
94
88
  return {
95
89
  content: [
96
90
  {
@@ -1,16 +1,9 @@
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.getFailuresInLastRun = getFailuresInLastRun;
7
- exports.default = addObservabilityTools;
8
- const zod_1 = require("zod");
9
- const api_1 = require("../lib/api");
10
- const instrumentation_1 = require("../lib/instrumentation");
11
- const logger_1 = __importDefault(require("../logger"));
12
- async function getFailuresInLastRun(buildName, projectName) {
13
- const buildsData = await (0, api_1.getLatestO11YBuildInfo)(buildName, projectName);
1
+ import { z } from "zod";
2
+ import { getLatestO11YBuildInfo } from "../lib/api.js";
3
+ import { trackMCP } from "../lib/instrumentation.js";
4
+ import logger from "../logger.js";
5
+ export async function getFailuresInLastRun(buildName, projectName) {
6
+ const buildsData = await getLatestO11YBuildInfo(buildName, projectName);
14
7
  const observabilityUrl = buildsData.observability_url;
15
8
  if (!observabilityUrl) {
16
9
  throw new Error("No observability URL found in build data, this is likely because the build is not yet available on BrowserStack Observability.");
@@ -35,22 +28,22 @@ async function getFailuresInLastRun(buildName, projectName) {
35
28
  ],
36
29
  };
37
30
  }
38
- function addObservabilityTools(server) {
31
+ export default function addObservabilityTools(server) {
39
32
  server.tool("getFailuresInLastRun", "Use this tool to debug failures in the last run of the test suite on BrowserStack. Use only when browserstack.yml file is present in the project root.", {
40
- buildName: zod_1.z
33
+ buildName: z
41
34
  .string()
42
35
  .describe("Name of the build to get failures for. This is the 'build' key in the browserstack.yml file. If not sure, ask the user for the build name."),
43
- projectName: zod_1.z
36
+ projectName: z
44
37
  .string()
45
38
  .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."),
46
39
  }, async (args) => {
47
40
  try {
48
- (0, instrumentation_1.trackMCP)("getFailuresInLastRun", server.server.getClientVersion());
41
+ trackMCP("getFailuresInLastRun", server.server.getClientVersion());
49
42
  return await getFailuresInLastRun(args.buildName, args.projectName);
50
43
  }
51
44
  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);
45
+ logger.error("Failed to get failures in the last run: %s", error);
46
+ trackMCP("getFailuresInLastRun", server.server.getClientVersion(), error);
54
47
  return {
55
48
  content: [
56
49
  {
@@ -1,10 +1,4 @@
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.SUPPORTED_CONFIGURATIONS = void 0;
7
- const config_1 = __importDefault(require("../../config"));
1
+ import config from "../../config.js";
8
2
  const nodejsInstructions = `
9
3
  - Ensure that \`browserstack-node-sdk\` is present in package.json, use the latest version.
10
4
  - Add new scripts to package.json for running tests on BrowserStack (use \`npx\` to trigger the sdk):
@@ -27,7 +21,7 @@ python3 -m pip install browserstack-sdk
27
21
 
28
22
  Run the following command to setup the browserstack-sdk:
29
23
  \`\`\`bash
30
- browserstack-sdk setup --username "${config_1.default.browserstackUsername}" --key "${config_1.default.browserstackAccessKey}"
24
+ browserstack-sdk setup --username "${config.browserstackUsername}" --key "${config.browserstackAccessKey}"
31
25
  \`\`\`
32
26
 
33
27
  In order to run tests on BrowserStack, run the following command:
@@ -35,7 +29,7 @@ In order to run tests on BrowserStack, run the following command:
35
29
  browserstack-sdk python <path-to-test-file>
36
30
  \`\`\`
37
31
  `;
38
- exports.SUPPORTED_CONFIGURATIONS = {
32
+ export const SUPPORTED_CONFIGURATIONS = {
39
33
  nodejs: {
40
34
  playwright: {
41
35
  jest: { instructions: nodejsInstructions },
@@ -1,11 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInstructionsForProjectConfiguration = void 0;
4
- exports.generateBrowserStackYMLInstructions = generateBrowserStackYMLInstructions;
5
- const constants_1 = require("./constants");
1
+ import { SUPPORTED_CONFIGURATIONS } from "./constants.js";
6
2
  const errorMessageSuffix = "Please open an issue at our Github repo: https://github.com/browserstack/browserstack-mcp-server/issues to request support for your project configuration";
7
- const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage) => {
8
- const configuration = constants_1.SUPPORTED_CONFIGURATIONS[detectedLanguage];
3
+ export const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage) => {
4
+ const configuration = SUPPORTED_CONFIGURATIONS[detectedLanguage];
9
5
  if (!configuration) {
10
6
  throw new Error(`BrowserStack MCP Server currently does not support ${detectedLanguage}, ${errorMessageSuffix}`);
11
7
  }
@@ -17,8 +13,7 @@ const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramewo
17
13
  }
18
14
  return configuration[detectedBrowserAutomationFramework][detectedTestingFramework].instructions;
19
15
  };
20
- exports.getInstructionsForProjectConfiguration = getInstructionsForProjectConfiguration;
21
- function generateBrowserStackYMLInstructions(desiredPlatforms) {
16
+ export function generateBrowserStackYMLInstructions(desiredPlatforms) {
22
17
  return `
23
18
  Create a browserstack.yml file in the project root. The file should be in the following format:
24
19
 
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
@@ -0,0 +1,259 @@
1
+ import axios from "axios";
2
+ import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, } from "./config.js";
3
+ import { createTestCasePayload } from "./helpers.js";
4
+ import config from "../../../config.js";
5
+ /**
6
+ * Fetch default and custom form fields for a project.
7
+ */
8
+ export async function fetchFormFields(projectId) {
9
+ const res = await axios.get(FORM_FIELDS_URL(projectId), {
10
+ headers: {
11
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
12
+ },
13
+ });
14
+ return res.data;
15
+ }
16
+ /**
17
+ * Trigger AI-based test case generation for a document.
18
+ */
19
+ export async function triggerTestCaseGeneration(document, documentId, folderId, projectId, source) {
20
+ const res = await axios.post(TCG_TRIGGER_URL, {
21
+ document,
22
+ documentId,
23
+ folderId,
24
+ projectId,
25
+ source,
26
+ webhookUrl: `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
27
+ }, {
28
+ headers: {
29
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
30
+ "Content-Type": "application/json",
31
+ "request-source": source,
32
+ },
33
+ });
34
+ if (res.status !== 200) {
35
+ throw new Error(`Trigger failed: ${res.statusText}`);
36
+ }
37
+ return res.data["x-bstack-traceRequestId"];
38
+ }
39
+ /**
40
+ * Initiate a fetch for test-case details; returns the traceRequestId for polling.
41
+ */
42
+ export async function fetchTestCaseDetails(documentId, folderId, projectId, testCaseIds, source) {
43
+ if (testCaseIds.length === 0) {
44
+ throw new Error("No testCaseIds provided to fetchTestCaseDetails");
45
+ }
46
+ const res = await axios.post(FETCH_DETAILS_URL, {
47
+ document_id: documentId,
48
+ folder_id: folderId,
49
+ project_id: projectId,
50
+ test_case_ids: testCaseIds,
51
+ }, {
52
+ headers: {
53
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
54
+ "request-source": source,
55
+ "Content-Type": "application/json",
56
+ },
57
+ });
58
+ if (res.data.data.success !== true) {
59
+ throw new Error(`Fetch details failed: ${res.data.data.message}`);
60
+ }
61
+ return res.data.request_trace_id;
62
+ }
63
+ /**
64
+ * Poll for a given traceRequestId until all test-case details are returned.
65
+ */
66
+ export async function pollTestCaseDetails(traceRequestId) {
67
+ const detailMap = {};
68
+ let done = false;
69
+ while (!done) {
70
+ // add a bit of jitter to avoid synchronized polling storms
71
+ await new Promise((r) => setTimeout(r, 10000 + Math.random() * 5000));
72
+ const poll = await axios.post(`${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`, {}, {
73
+ headers: {
74
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
75
+ },
76
+ });
77
+ if (!poll.data.data.success) {
78
+ throw new Error(`Polling failed: ${poll.data.data.message}`);
79
+ }
80
+ for (const msg of poll.data.data.message) {
81
+ if (msg.type === "termination") {
82
+ done = true;
83
+ }
84
+ if (msg.type === "testcase_details") {
85
+ for (const test of msg.data.testcase_details) {
86
+ detailMap[test.id] = {
87
+ steps: test.steps,
88
+ preconditions: test.preconditions,
89
+ };
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return detailMap;
95
+ }
96
+ /**
97
+ * Poll for scenarios & testcases, trigger detail fetches, then poll all details in parallel.
98
+ */
99
+ export async function pollScenariosTestDetails(args, traceId, context, documentId, source) {
100
+ const { folderId, projectReferenceId } = args;
101
+ const scenariosMap = {};
102
+ const detailPromises = [];
103
+ let iteratorCount = 0;
104
+ // Promisify interval-style polling using a wrapper
105
+ await new Promise((resolve, reject) => {
106
+ const intervalId = setInterval(async () => {
107
+ try {
108
+ const poll = await axios.post(`${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`, {}, {
109
+ headers: {
110
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
111
+ },
112
+ });
113
+ if (poll.status !== 200) {
114
+ clearInterval(intervalId);
115
+ reject(new Error(`Polling error: ${poll.statusText}`));
116
+ return;
117
+ }
118
+ for (const msg of poll.data.data.message) {
119
+ if (msg.type === "scenario") {
120
+ msg.data.scenarios.forEach((sc) => {
121
+ scenariosMap[sc.id] = { id: sc.id, name: sc.name, testcases: [] };
122
+ });
123
+ const count = Object.keys(scenariosMap).length;
124
+ await context.sendNotification({
125
+ method: "notifications/progress",
126
+ params: {
127
+ progressToken: context._meta?.progressToken ?? traceId,
128
+ progress: count,
129
+ total: count,
130
+ message: `Fetched ${count} scenarios`,
131
+ },
132
+ });
133
+ }
134
+ if (msg.type === "testcase") {
135
+ const sc = msg.data.scenario;
136
+ if (sc) {
137
+ const array = Array.isArray(msg.data.testcases)
138
+ ? msg.data.testcases
139
+ : msg.data.testcases
140
+ ? [msg.data.testcases]
141
+ : [];
142
+ const ids = array.map((tc) => tc.id || tc.test_case_id);
143
+ const reqId = await fetchTestCaseDetails(documentId, folderId, projectReferenceId, ids, source);
144
+ detailPromises.push(pollTestCaseDetails(reqId));
145
+ scenariosMap[sc.id] ||= {
146
+ id: sc.id,
147
+ name: sc.name,
148
+ testcases: [],
149
+ traceId,
150
+ };
151
+ scenariosMap[sc.id].testcases.push(...array);
152
+ iteratorCount++;
153
+ const total = Object.keys(scenariosMap).length;
154
+ await context.sendNotification({
155
+ method: "notifications/progress",
156
+ params: {
157
+ progressToken: context._meta?.progressToken ?? traceId,
158
+ progress: iteratorCount,
159
+ total,
160
+ message: `Fetched ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
161
+ },
162
+ });
163
+ }
164
+ }
165
+ if (msg.type === "termination") {
166
+ clearInterval(intervalId);
167
+ resolve();
168
+ }
169
+ }
170
+ }
171
+ catch (err) {
172
+ clearInterval(intervalId);
173
+ reject(err);
174
+ }
175
+ }, 10000); // 10 second interval
176
+ });
177
+ // once all detail fetches are triggered, wait for them to complete
178
+ const detailsList = await Promise.all(detailPromises);
179
+ const allDetails = detailsList.reduce((acc, cur) => ({ ...acc, ...cur }), {});
180
+ // attach the fetched detail objects back to each testcase
181
+ for (const scenario of Object.values(scenariosMap)) {
182
+ scenario.testcases = scenario.testcases.map((tc) => ({
183
+ ...tc,
184
+ ...(allDetails[tc.id || tc.test_case_id] ?? {}),
185
+ }));
186
+ }
187
+ return scenariosMap;
188
+ }
189
+ /**
190
+ * Bulk-create generated test cases in BrowserStack.
191
+ */
192
+ export async function bulkCreateTestCases(scenariosMap, projectId, folderId, fieldMaps, booleanFieldId, traceId, context, documentId) {
193
+ const results = {};
194
+ const total = Object.keys(scenariosMap).length;
195
+ let doneCount = 0;
196
+ let testCaseCount = 0;
197
+ for (const { id, testcases } of Object.values(scenariosMap)) {
198
+ const testCaseLength = testcases.length;
199
+ testCaseCount += testCaseLength;
200
+ if (testCaseLength === 0)
201
+ continue;
202
+ const payload = {
203
+ test_cases: testcases.map((tc) => createTestCasePayload(tc, id, folderId, fieldMaps, documentId, booleanFieldId, traceId)),
204
+ };
205
+ try {
206
+ const resp = await axios.post(BULK_CREATE_URL(projectId, folderId), payload, {
207
+ headers: {
208
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
209
+ "Content-Type": "application/json",
210
+ },
211
+ });
212
+ results[id] = resp.data;
213
+ await context.sendNotification({
214
+ method: "notifications/progress",
215
+ params: {
216
+ progressToken: context._meta?.progressToken ?? "bulk-create",
217
+ message: `Bulk create done for scenario ${doneCount} of ${total}`,
218
+ total,
219
+ progress: doneCount,
220
+ },
221
+ });
222
+ }
223
+ catch (error) {
224
+ //send notification
225
+ await context.sendNotification({
226
+ method: "notifications/progress",
227
+ params: {
228
+ progressToken: context._meta?.progressToken ?? traceId,
229
+ message: `Bulk create failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
230
+ total,
231
+ progress: doneCount,
232
+ },
233
+ });
234
+ //continue to next scenario
235
+ continue;
236
+ }
237
+ doneCount++;
238
+ }
239
+ const resultString = `Total of ${testCaseCount} test cases created in ${total} scenarios.`;
240
+ return resultString;
241
+ }
242
+ export async function projectIdentifierToId(projectId) {
243
+ const url = `https://test-management.browserstack.com/api/v1/projects/?q=${projectId}`;
244
+ const response = await axios.get(url, {
245
+ headers: {
246
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
247
+ accept: "application/json, text/plain, */*",
248
+ },
249
+ });
250
+ if (response.data.success !== true) {
251
+ throw new Error(`Failed to fetch project ID: ${response.statusText}`);
252
+ }
253
+ for (const project of response.data.projects) {
254
+ if (project.identifier === projectId) {
255
+ return project.id;
256
+ }
257
+ }
258
+ throw new Error(`Project with identifier ${projectId} not found.`);
259
+ }
@@ -0,0 +1,5 @@
1
+ export const TCG_TRIGGER_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/suggest-test-cases";
2
+ export const TCG_POLL_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/test-cases-polling";
3
+ export const FETCH_DETAILS_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/fetch-test-case-details";
4
+ export const FORM_FIELDS_URL = (projectId) => `https://test-management.browserstack.com/api/v1/projects/${projectId}/form-fields-v2`;
5
+ export const BULK_CREATE_URL = (projectId, folderId) => `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Build mappings for default fields for priority, status, and case type.
3
+ */
4
+ export function buildDefaultFieldMaps(defaultFields) {
5
+ const priority = Object.fromEntries(defaultFields.priority.values.map((v) => [
6
+ v.name.toLowerCase(),
7
+ v.value,
8
+ ]));
9
+ const status = Object.fromEntries(defaultFields.status.values.map((v) => [v.internal_name, v.value]));
10
+ const caseType = Object.fromEntries(defaultFields.case_type.values.map((v) => [v.internal_name, v.value]));
11
+ return { priority, status, caseType };
12
+ }
13
+ /**
14
+ * Find a boolean custom field ID if present.
15
+ */
16
+ export function findBooleanFieldId(customFields) {
17
+ const boolField = customFields.find((f) => f.field_type === "field_boolean");
18
+ return boolField?.id;
19
+ }
20
+ /**
21
+ * Construct payload for creating a single test case in bulk.
22
+ */
23
+ export function createTestCasePayload(tc, scenarioId, folderId, fieldMaps, documentId, booleanFieldId, traceId) {
24
+ const pri = tc.priority ?? "Medium";
25
+ const stat = fieldMaps.status["active"];
26
+ const ct = fieldMaps.caseType["functional"];
27
+ return {
28
+ attachments: [documentId],
29
+ name: tc.name,
30
+ description: tc.description,
31
+ test_case_folder_id: folderId,
32
+ priority: pri,
33
+ status: stat,
34
+ case_type: ct,
35
+ automation_status: "not_automated",
36
+ fetch_ai_test_case_details: true,
37
+ template: "test_case_steps",
38
+ metadata: JSON.stringify({
39
+ ai_prompt: {
40
+ attachment_id: documentId,
41
+ rich_text_id: null,
42
+ scenario: scenarioId,
43
+ test_case_count: tc.test_case_count || 1,
44
+ uuid: tc.uuid || crypto.randomUUID?.() || "unknown-uuid",
45
+ "x-bstack-traceRequestId": traceId,
46
+ },
47
+ }),
48
+ tags: ["AI Generated", "MCP Generated"],
49
+ custom_fields: booleanFieldId ? { [booleanFieldId]: false } : undefined,
50
+ test_case_steps: tc.steps,
51
+ preconditions: tc.preconditions,
52
+ };
53
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const CreateTestCasesFromFileSchema = z.object({
3
+ documentId: z.string().describe("Internal document identifier"),
4
+ folderId: z.string().describe("BrowserStack folder ID"),
5
+ projectReferenceId: z
6
+ .string()
7
+ .describe("The BrowserStack project reference ID is a unique identifier found in the project URL within the BrowserStack Test Management Platform. This ID is also returned by the Upload Document tool."),
8
+ });