@browserstack/mcp-server 1.5.0-beta.20 → 1.5.0-beta.21

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 (95) hide show
  1. package/README.md +48 -32
  2. package/dist/config.d.ts +4 -1
  3. package/dist/config.js +23 -2
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +7 -3
  6. package/dist/lib/constants.d.ts +3 -0
  7. package/dist/lib/constants.js +3 -0
  8. package/dist/lib/instrumentation.d.ts +14 -2
  9. package/dist/lib/instrumentation.js +104 -29
  10. package/dist/lib/node-version-notice.d.ts +1 -0
  11. package/dist/lib/node-version-notice.js +9 -0
  12. package/dist/lib/tool-latency.d.ts +4 -0
  13. package/dist/lib/tool-latency.js +16 -0
  14. package/dist/lib/untrusted-content.d.ts +15 -0
  15. package/dist/lib/untrusted-content.js +24 -0
  16. package/dist/lib/upload-validator.d.ts +3 -2
  17. package/dist/lib/upload-validator.js +18 -15
  18. package/dist/lib/utils.js +10 -3
  19. package/dist/lib/version-resolver.d.ts +0 -5
  20. package/dist/lib/version-resolver.js +8 -2
  21. package/dist/logger.js +1 -10
  22. package/dist/server-factory.js +10 -6
  23. package/dist/tools/accessibility.js +9 -5
  24. package/dist/tools/accessiblity-utils/accessibility-rag.js +3 -1
  25. package/dist/tools/appautomate-utils/native-execution/constants.js +4 -2
  26. package/dist/tools/appautomate.js +3 -1
  27. package/dist/tools/applive.js +1 -1
  28. package/dist/tools/ask-browserstack/central-oauth.d.ts +120 -0
  29. package/dist/tools/ask-browserstack/central-oauth.js +277 -0
  30. package/dist/tools/ask-browserstack/config.d.ts +102 -0
  31. package/dist/tools/ask-browserstack/config.js +140 -0
  32. package/dist/tools/ask-browserstack/egress.d.ts +34 -0
  33. package/dist/tools/ask-browserstack/egress.js +31 -0
  34. package/dist/tools/ask-browserstack/register.d.ts +61 -0
  35. package/dist/tools/ask-browserstack/register.js +416 -0
  36. package/dist/tools/ask-browserstack/relay.d.ts +201 -0
  37. package/dist/tools/ask-browserstack/relay.js +577 -0
  38. package/dist/tools/ask-browserstack/stream.d.ts +116 -0
  39. package/dist/tools/ask-browserstack/stream.js +236 -0
  40. package/dist/tools/ask-browserstack/types.d.ts +196 -0
  41. package/dist/tools/ask-browserstack/types.js +14 -0
  42. package/dist/tools/automate-utils/list-session-ids.d.ts +28 -0
  43. package/dist/tools/automate-utils/list-session-ids.js +87 -0
  44. package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +30 -0
  45. package/dist/tools/automate-utils/resolve-hashed-build-id.js +124 -0
  46. package/dist/tools/automate.d.ts +7 -0
  47. package/dist/tools/automate.js +104 -1
  48. package/dist/tools/bstack-sdk.js +2 -2
  49. package/dist/tools/build-insights.js +41 -2
  50. package/dist/tools/failurelogs-utils/app-automate.js +4 -3
  51. package/dist/tools/failurelogs-utils/automate.js +5 -4
  52. package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +2 -0
  53. package/dist/tools/failurelogs-utils/resolve-app-build-id.js +5 -0
  54. package/dist/tools/failurelogs-utils/video.d.ts +3 -0
  55. package/dist/tools/failurelogs-utils/video.js +25 -0
  56. package/dist/tools/get-failure-logs.js +30 -11
  57. package/dist/tools/observability.js +3 -1
  58. package/dist/tools/percy-sdk.js +9 -9
  59. package/dist/tools/rca-agent-utils/format-rca.js +5 -4
  60. package/dist/tools/rca-agent-utils/get-failed-test-id.js +12 -0
  61. package/dist/tools/rca-agent-utils/types.d.ts +1 -0
  62. package/dist/tools/rca-agent.js +5 -2
  63. package/dist/tools/review-agent.js +2 -1
  64. package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
  65. package/dist/tools/sdk-utils/common/constants.js +2 -1
  66. package/dist/tools/selfheal.js +6 -3
  67. package/dist/tools/testmanagement-utils/TCG-utils/api.js +26 -7
  68. package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +2 -0
  69. package/dist/tools/testmanagement-utils/TCG-utils/config.js +5 -0
  70. package/dist/tools/testmanagement-utils/testcase-from-file.js +2 -1
  71. package/dist/tools/testmanagement-utils/upload-file.js +16 -2
  72. package/dist/tools/testmanagement.js +39 -17
  73. package/dist/tools/tool-handoff.d.ts +66 -0
  74. package/dist/tools/tool-handoff.js +78 -0
  75. package/package.json +5 -11
  76. package/capability/loadtesting.capability-index.json +0 -1870
  77. package/capability/tm.capability-index.json +0 -50104
  78. package/dist/tools/capability-registry/bind.d.ts +0 -29
  79. package/dist/tools/capability-registry/bind.js +0 -329
  80. package/dist/tools/capability-registry/config.d.ts +0 -62
  81. package/dist/tools/capability-registry/config.js +0 -218
  82. package/dist/tools/capability-registry/discovery.d.ts +0 -44
  83. package/dist/tools/capability-registry/discovery.js +0 -99
  84. package/dist/tools/capability-registry/egress.d.ts +0 -44
  85. package/dist/tools/capability-registry/egress.js +0 -174
  86. package/dist/tools/capability-registry/index-loader.d.ts +0 -133
  87. package/dist/tools/capability-registry/index-loader.js +0 -404
  88. package/dist/tools/capability-registry/register.d.ts +0 -48
  89. package/dist/tools/capability-registry/register.js +0 -604
  90. package/dist/tools/capability-registry/resolve.d.ts +0 -38
  91. package/dist/tools/capability-registry/resolve.js +0 -45
  92. package/dist/tools/capability-registry/search.d.ts +0 -170
  93. package/dist/tools/capability-registry/search.js +0 -709
  94. package/dist/tools/capability-registry/types.d.ts +0 -282
  95. package/dist/tools/capability-registry/types.js +0 -33
@@ -1,8 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { trackMCP } from "../lib/instrumentation.js";
3
+ import { NEEDS_SESSION_ID } from "./tool-handoff.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 { AppAutomateLogType, AutomateLogType, SessionType, } from "../lib/constants.js";
6
+ import { resolveAppAutomateBuildId } from "./failurelogs-utils/resolve-app-build-id.js";
7
+ import { retrieveSessionVideo } from "./failurelogs-utils/video.js";
8
+ import { AppAutomateLogType, AutomateLogType, SessionType, SessionVideoLogType, } from "../lib/constants.js";
6
9
  // Main log fetcher function
7
10
  export async function getFailureLogs(args, config) {
8
11
  const results = [];
@@ -11,17 +14,16 @@ export async function getFailureLogs(args, config) {
11
14
  if (!args.sessionId) {
12
15
  throw new Error("Session ID is required");
13
16
  }
14
- if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
15
- throw new Error("Build ID is required for app-automate sessions");
16
- }
17
17
  // Validate log types and collect errors
18
18
  validLogTypes = args.logTypes.filter((logType) => {
19
19
  const isAutomate = Object.values(AutomateLogType).includes(logType);
20
20
  const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
21
21
  if (!isAutomate && !isAppAutomate) {
22
22
  errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
23
- ...Object.values(AutomateLogType),
24
- ...Object.values(AppAutomateLogType),
23
+ ...new Set([
24
+ ...Object.values(AutomateLogType),
25
+ ...Object.values(AppAutomateLogType),
26
+ ]),
25
27
  ].join(", ")}`);
26
28
  return false;
27
29
  }
@@ -46,11 +48,26 @@ export async function getFailureLogs(args, config) {
46
48
  isError: true,
47
49
  };
48
50
  }
51
+ let buildId = args.buildId;
52
+ const needsBuildId = validLogTypes.some((logType) => logType !== SessionVideoLogType);
53
+ if (args.sessionType === SessionType.AppAutomate &&
54
+ needsBuildId &&
55
+ !buildId) {
56
+ buildId = await resolveAppAutomateBuildId(args.sessionId, config);
57
+ if (!buildId) {
58
+ throw new Error("Build ID is required for app-automate sessions");
59
+ }
60
+ }
49
61
  let response;
50
62
  // eslint-disable-next-line no-useless-catch
51
63
  try {
52
64
  for (const logType of validLogTypes) {
53
65
  switch (logType) {
66
+ case SessionVideoLogType: {
67
+ response = await retrieveSessionVideo(args.sessionId, args.sessionType, config);
68
+ results.push({ type: "text", text: response });
69
+ break;
70
+ }
54
71
  case AutomateLogType.NetworkLogs: {
55
72
  response = await retrieveNetworkFailures(args.sessionId, config);
56
73
  results.push({ type: "text", text: response });
@@ -67,17 +84,17 @@ export async function getFailureLogs(args, config) {
67
84
  break;
68
85
  }
69
86
  case AppAutomateLogType.DeviceLogs: {
70
- response = await retrieveDeviceLogs(args.sessionId, args.buildId, config);
87
+ response = await retrieveDeviceLogs(args.sessionId, buildId, config);
71
88
  results.push({ type: "text", text: response });
72
89
  break;
73
90
  }
74
91
  case AppAutomateLogType.AppiumLogs: {
75
- response = await retrieveAppiumLogs(args.sessionId, args.buildId, config);
92
+ response = await retrieveAppiumLogs(args.sessionId, buildId, config);
76
93
  results.push({ type: "text", text: response });
77
94
  break;
78
95
  }
79
96
  case AppAutomateLogType.CrashLogs: {
80
- response = await retrieveCrashLogs(args.sessionId, args.buildId, config);
97
+ response = await retrieveCrashLogs(args.sessionId, buildId, config);
81
98
  results.push({ type: "text", text: response });
82
99
  break;
83
100
  }
@@ -98,7 +115,8 @@ export async function getFailureLogs(args, config) {
98
115
  // Register tool with the MCP server
99
116
  export default function registerGetFailureLogs(server, config) {
100
117
  const tools = {};
101
- tools.getFailureLogs = server.tool("getFailureLogs", "Fetch various types of logs from a BrowserStack session. Supports both automate and app-automate sessions.", {
118
+ tools.getFailureLogs = server.tool("getFailureLogs", "Fetch logs, or the session video URL, for an Automate/App Automate session." +
119
+ NEEDS_SESSION_ID, {
102
120
  sessionType: z
103
121
  .enum([SessionType.Automate, SessionType.AppAutomate])
104
122
  .describe("Type of BrowserStack session. Must be explicitly provided by the user."),
@@ -108,7 +126,7 @@ export default function registerGetFailureLogs(server, config) {
108
126
  buildId: z
109
127
  .string()
110
128
  .optional()
111
- .describe("Required only when sessionType is 'app-automate'. If sessionType is 'app-automate', always ask the user to provide the build ID before proceeding."),
129
+ .describe("App Automate build ID. Optional — resolved from the session if omitted."),
112
130
  logTypes: z
113
131
  .array(z.enum([
114
132
  AutomateLogType.NetworkLogs,
@@ -117,6 +135,7 @@ export default function registerGetFailureLogs(server, config) {
117
135
  AppAutomateLogType.DeviceLogs,
118
136
  AppAutomateLogType.AppiumLogs,
119
137
  AppAutomateLogType.CrashLogs,
138
+ SessionVideoLogType,
120
139
  ]))
121
140
  .describe("The types of logs to fetch."),
122
141
  }, {
@@ -1,3 +1,4 @@
1
+ import { wrapUntrusted } from "../lib/untrusted-content.js";
1
2
  import { z } from "zod";
2
3
  import { getLatestO11YBuildInfo } from "../lib/api.js";
3
4
  import { trackMCP } from "../lib/instrumentation.js";
@@ -26,7 +27,8 @@ export async function getFailuresInLastRun(buildName, projectName, config) {
26
27
  content: [
27
28
  {
28
29
  type: "text",
29
- text: `Observability URL: ${observabilityUrl}\nOverview: ${overview}\nError Details: ${details}`,
30
+ text: `Observability URL: ${observabilityUrl}\n` +
31
+ wrapUntrusted("observability failure report", `Overview: ${overview}\nError Details: ${details}`),
30
32
  },
31
33
  ],
32
34
  };
@@ -38,11 +38,11 @@ export function registerPercyTools(server, config) {
38
38
  idempotentHint: true,
39
39
  }, async (args) => {
40
40
  try {
41
- trackMCP("VisualTestIntegrationAgent", server.server.getClientVersion(), config);
41
+ trackMCP("percyVisualTestIntegrationAgent", server.server.getClientVersion(), undefined, config);
42
42
  return simulatePercyChangeHandler(args, config);
43
43
  }
44
44
  catch (error) {
45
- return handleMCPError("VisualTestIntegrationAgent", server, config, error);
45
+ return handleMCPError("percyVisualTestIntegrationAgent", server, config, error);
46
46
  }
47
47
  });
48
48
  tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, {
@@ -53,11 +53,11 @@ export function registerPercyTools(server, config) {
53
53
  idempotentHint: true,
54
54
  }, async (args) => {
55
55
  try {
56
- trackMCP("setupPercyVisualTesting", server.server.getClientVersion(), config);
56
+ trackMCP("expandPercyVisualTesting", server.server.getClientVersion(), undefined, config);
57
57
  return setUpPercyHandler(args, config);
58
58
  }
59
59
  catch (error) {
60
- return handleMCPError("setupPercyVisualTesting", server, config, error);
60
+ return handleMCPError("expandPercyVisualTesting", server, config, error);
61
61
  }
62
62
  });
63
63
  tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams, {
@@ -68,7 +68,7 @@ export function registerPercyTools(server, config) {
68
68
  idempotentHint: true,
69
69
  }, async (args) => {
70
70
  try {
71
- trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), config);
71
+ trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), undefined, config);
72
72
  return await updateTestsWithPercyCommands(args);
73
73
  }
74
74
  catch (error) {
@@ -83,7 +83,7 @@ export function registerPercyTools(server, config) {
83
83
  idempotentHint: true,
84
84
  }, async () => {
85
85
  try {
86
- trackMCP("listTestFiles", server.server.getClientVersion(), config);
86
+ trackMCP("listTestFiles", server.server.getClientVersion(), undefined, config);
87
87
  return addListTestFiles();
88
88
  }
89
89
  catch (error) {
@@ -98,7 +98,7 @@ export function registerPercyTools(server, config) {
98
98
  idempotentHint: true,
99
99
  }, async (args) => {
100
100
  try {
101
- trackMCP("runPercyScan", server.server.getClientVersion(), config);
101
+ trackMCP("runPercyScan", server.server.getClientVersion(), undefined, config);
102
102
  return runPercyScan(args);
103
103
  }
104
104
  catch (error) {
@@ -113,7 +113,7 @@ export function registerPercyTools(server, config) {
113
113
  idempotentHint: true,
114
114
  }, async (args) => {
115
115
  try {
116
- trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
116
+ trackMCP("fetchPercyChanges", server.server.getClientVersion(), undefined, config);
117
117
  return await fetchPercyChanges(args, config);
118
118
  }
119
119
  catch (error) {
@@ -128,7 +128,7 @@ export function registerPercyTools(server, config) {
128
128
  idempotentHint: true,
129
129
  }, async (args) => {
130
130
  try {
131
- trackMCP("managePercyBuildApproval", server.server.getClientVersion(), config);
131
+ trackMCP("managePercyBuildApproval", server.server.getClientVersion(), undefined, config);
132
132
  return await approveOrDeclinePercyBuild(args, config);
133
133
  }
134
134
  catch (error) {
@@ -1,3 +1,4 @@
1
+ import { wrapUntrusted } from "../../lib/untrusted-content.js";
1
2
  // Utility function to format RCA data for better readability
2
3
  export function formatRCAData(rcaData) {
3
4
  if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
@@ -16,21 +17,21 @@ export function formatRCAData(rcaData) {
16
17
  const rca = testCase.rcaData?.rcaData;
17
18
  if (rca) {
18
19
  if (rca.root_cause) {
19
- output += `**Root Cause:** ${rca.root_cause}\n\n`;
20
+ output += `**Root Cause:** ${wrapUntrusted("RCA AI analysis", rca.root_cause)}\n\n`;
20
21
  }
21
22
  if (rca.failure_type) {
22
23
  output += `**Failure Type:** ${rca.failure_type}\n\n`;
23
24
  }
24
25
  if (rca.description) {
25
- output += `**Detailed Analysis:**\n${rca.description}\n\n`;
26
+ output += `**Detailed Analysis:**\n${wrapUntrusted("RCA AI analysis", rca.description)}\n\n`;
26
27
  }
27
28
  if (rca.possible_fix) {
28
29
  hasFixSuggestion = true;
29
- output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${rca.possible_fix}\n\n`;
30
+ output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${wrapUntrusted("RCA AI analysis", rca.possible_fix)}\n\n`;
30
31
  }
31
32
  }
32
33
  else if (testCase.rcaData?.error) {
33
- output += `**Error:** ${testCase.rcaData.error}\n\n`;
34
+ output += `**Error:** ${wrapUntrusted("RCA error output", testCase.rcaData.error)}\n\n`;
34
35
  }
35
36
  else if (testCase.state === "failed") {
36
37
  output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
@@ -53,6 +53,16 @@ export async function getTestIds(buildId, authString, status, includeFailureDeta
53
53
  throw error;
54
54
  }
55
55
  }
56
+ /**
57
+ * The test listing API serialises a missing BrowserStack session id as the
58
+ * literal string "null", so treat that (and blanks) as absent.
59
+ */
60
+ function extractSessionId(rawSessionId) {
61
+ if (typeof rawSessionId !== "string")
62
+ return undefined;
63
+ const sessionId = rawSessionId.trim();
64
+ return sessionId && sessionId !== "null" ? sessionId : undefined;
65
+ }
56
66
  export function extractTestIds(hierarchy, status, includeFailureDetail = false) {
57
67
  let tests = [];
58
68
  for (const node of hierarchy) {
@@ -63,10 +73,12 @@ export function extractTestIds(hierarchy, status, includeFailureDetail = false)
63
73
  if (statusMatches && node.details?.observability_url) {
64
74
  const idMatch = node.details.observability_url.match(/details=(\d+)/);
65
75
  if (idMatch) {
76
+ const sessionId = extractSessionId(node.details.session_id);
66
77
  const entry = {
67
78
  test_id: idMatch[1],
68
79
  test_name: node.display_name || `Test ${idMatch[1]}`,
69
80
  status: nodeStatus,
81
+ ...(sessionId && { session_id: sessionId }),
70
82
  };
71
83
  // Failure signatures only exist for failed tests; include when asked.
72
84
  if (includeFailureDetail && nodeStatus === TestStatus.FAILED) {
@@ -30,6 +30,7 @@ export interface FailedTestInfo {
30
30
  test_name: string;
31
31
  status?: TestStatus;
32
32
  failure?: TestFailureSignature;
33
+ session_id?: string;
33
34
  }
34
35
  export declare enum RCAState {
35
36
  PENDING = "pending",
@@ -7,6 +7,7 @@ import { getRCAData } from "./rca-agent-utils/rca-data.js";
7
7
  import { formatRCAData } from "./rca-agent-utils/format-rca.js";
8
8
  import { handleMCPError } from "../lib/utils.js";
9
9
  import { trackMCP } from "../index.js";
10
+ import { NEEDS_BUILD_ID, NEEDS_TEST_IDS } from "./tool-handoff.js";
10
11
  import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, LIST_TEST_IDS_PARAMS, } from "./rca-agent-utils/constants.js";
11
12
  // Tool function to fetch build ID
12
13
  export async function getBuildIdTool(args, config) {
@@ -130,7 +131,8 @@ export async function listTestIdsTool(args, config) {
130
131
  }
131
132
  export default function addRCATools(server, config) {
132
133
  const tools = {};
133
- tools.fetchRCA = server.tool("fetchRCA", "Fetch AI Root Cause Analysis for the current user's failed BrowserStack Automate/App-Automate tests. Suggests fixes only; never auto-apply, require explicit user approval.", FETCH_RCA_PARAMS, {
134
+ tools.fetchRCA = server.tool("fetchRCA", "Fetch AI Root Cause Analysis for the current user's failed BrowserStack Automate/App-Automate tests. Suggests fixes only; never auto-apply, require explicit user approval." +
135
+ NEEDS_TEST_IDS, FETCH_RCA_PARAMS, {
134
136
  title: "Fetch Root Cause Analysis",
135
137
  readOnlyHint: true,
136
138
  openWorldHint: false,
@@ -175,7 +177,8 @@ export default function addRCATools(server, config) {
175
177
  return handleMCPError("listBuildId", server, config, error);
176
178
  }
177
179
  });
178
- tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status); optional status filter.", LIST_TEST_IDS_PARAMS, {
180
+ tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status and session id); optional status filter." +
181
+ NEEDS_BUILD_ID, LIST_TEST_IDS_PARAMS, {
179
182
  title: "List Test IDs",
180
183
  readOnlyHint: true,
181
184
  openWorldHint: false,
@@ -1,3 +1,4 @@
1
+ import { wrapUntrusted } from "../lib/untrusted-content.js";
1
2
  import { getBrowserStackAuth } from "../lib/get-auth.js";
2
3
  import { getPercyBuildCount } from "./review-agent-utils/build-counts.js";
3
4
  import { getChangedPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js";
@@ -50,7 +51,7 @@ export async function fetchPercyChanges(args, config) {
50
51
  return {
51
52
  content: allDiffs.map((diff) => ({
52
53
  type: "text",
53
- text: `${diff.name} → ${diff.title}: ${diff.description ?? ""}`,
54
+ text: wrapUntrusted("Percy AI visual-diff description", `${diff.name} → ${diff.title}: ${diff.description ?? ""}`),
54
55
  })),
55
56
  };
56
57
  }
@@ -1,4 +1,4 @@
1
- export declare const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup.";
1
+ export declare const IMPORTANT_SETUP_WARNING: string;
2
2
  export declare const SETUP_PERCY_DESCRIPTION = "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}";
3
3
  export declare const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";
4
4
  export declare const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files.";
@@ -1,4 +1,5 @@
1
- export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup.";
1
+ export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup. " +
2
+ "If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them.";
2
3
  export const SETUP_PERCY_DESCRIPTION = "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}";
3
4
  export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";
4
5
  export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files.";
@@ -3,6 +3,7 @@ import { getSelfHealSelectors, fetchSelfHealingReportByBuild, } from "./selfheal
3
3
  import { fetchTestCodeForSessions, formatTestCodeAsContext, describeTestCodeFetchIssues, } from "./selfheal-utils/fetch-test-code.js";
4
4
  import logger from "../logger.js";
5
5
  import { trackMCP } from "../lib/instrumentation.js";
6
+ import { NEEDS_SESSION_ID } from "./tool-handoff.js";
6
7
  // Local helper: returns the server-configured BrowserStack credentials, or
7
8
  // null when either is missing. Lives here because the self-heal tools need
8
9
  // to degrade gracefully — `getBrowserStackAuth` throws, which is wrong for
@@ -430,7 +431,8 @@ export default function addSelfHealTools(server, config) {
430
431
  "the run. Provide exactly one of `sessionId` (single Automate / " +
431
432
  "App-Automate session) or `buildUuid` (full self-healing report for a " +
432
433
  "build). Pass the returned locator pairs to `prepareSelfHealingPlan` " +
433
- "to plan edits.", {
434
+ "to plan edits." +
435
+ NEEDS_SESSION_ID, {
434
436
  sessionId: z
435
437
  .string()
436
438
  .describe("Session ID. Mutually exclusive with buildUuid.")
@@ -499,11 +501,12 @@ export default function addSelfHealTools(server, config) {
499
501
  "[...]}`, the raw report `{healing_logs: [...]}` (with " +
500
502
  "`healed_selectors` aliasing `locators`), and snake_case keys " +
501
503
  "(`session_id`, `original_locator`, `healed_locator`, " +
502
- "`healing_thought`).", {
504
+ "`healing_thought`)." +
505
+ NEEDS_SESSION_ID, {
503
506
  sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
504
507
  }, {
505
508
  title: "Prepare Self-Healing Plan",
506
- readOnlyHint: true,
509
+ readOnlyHint: false,
507
510
  openWorldHint: false,
508
511
  destructiveHint: false,
509
512
  idempotentHint: true,
@@ -1,5 +1,5 @@
1
1
  import { apiClient } from "../../../lib/apiClient.js";
2
- import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, TC_DETAILS_MAX_BATCH, } from "./config.js";
2
+ import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, TC_DETAILS_MAX_BATCH, TCG_POLL_INTERVAL_MS, TCG_POLL_MAX_WAIT_MS, } from "./config.js";
3
3
  import { createTestCasePayload } from "./helpers.js";
4
4
  import { getBrowserStackAuth } from "../../../lib/get-auth.js";
5
5
  import { getTMBaseURL } from "../../../lib/tm-base-url.js";
@@ -118,9 +118,15 @@ export async function pollTestCaseDetails(traceRequestId, config) {
118
118
  let done = false;
119
119
  const tmBaseUrl = await getTMBaseURL(config);
120
120
  const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
121
+ const deadline = Date.now() + TCG_POLL_MAX_WAIT_MS;
121
122
  while (!done) {
123
+ // Bail out if the backend never sends a "termination" message, so a stuck
124
+ // job cannot keep this loop (and its callers) alive forever.
125
+ if (Date.now() > deadline) {
126
+ throw new Error(`TCG test-case detail polling timed out after ${TCG_POLL_MAX_WAIT_MS}ms (trace ${traceRequestId})`);
127
+ }
122
128
  // add a bit of jitter to avoid synchronized polling storms
123
- await new Promise((r) => setTimeout(r, 10000 + Math.random() * 5000));
129
+ await new Promise((r) => setTimeout(r, TCG_POLL_INTERVAL_MS + Math.random() * 5000));
124
130
  const poll = await apiClient.post({
125
131
  url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
126
132
  headers: {
@@ -159,7 +165,20 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
159
165
  const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
160
166
  // Promisify interval-style polling using a wrapper
161
167
  await new Promise((resolve, reject) => {
162
- const intervalId = setInterval(async () => {
168
+ const timers = {};
169
+ const stop = () => {
170
+ if (timers.interval)
171
+ clearInterval(timers.interval);
172
+ if (timers.timeout)
173
+ clearTimeout(timers.timeout);
174
+ };
175
+ // Hard wall-clock deadline: if the backend never sends a "termination"
176
+ // message, reject instead of letting the interval fire forever.
177
+ timers.timeout = setTimeout(() => {
178
+ stop();
179
+ reject(new Error(`TCG scenario polling timed out after ${TCG_POLL_MAX_WAIT_MS}ms (trace ${traceId})`));
180
+ }, TCG_POLL_MAX_WAIT_MS);
181
+ timers.interval = setInterval(async () => {
163
182
  try {
164
183
  const poll = await apiClient.post({
165
184
  url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
@@ -169,7 +188,7 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
169
188
  body: {},
170
189
  });
171
190
  if (poll.status !== 200) {
172
- clearInterval(intervalId);
191
+ stop();
173
192
  reject(new Error(`Polling error: ${poll.statusText || poll.status}`));
174
193
  return;
175
194
  }
@@ -221,16 +240,16 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
221
240
  }
222
241
  }
223
242
  if (msg.type === "termination") {
224
- clearInterval(intervalId);
243
+ stop();
225
244
  resolve();
226
245
  }
227
246
  }
228
247
  }
229
248
  catch (err) {
230
- clearInterval(intervalId);
249
+ stop();
231
250
  reject(err);
232
251
  }
233
- }, 10000); // 10 second interval
252
+ }, TCG_POLL_INTERVAL_MS);
234
253
  });
235
254
  // once all detail fetches are triggered, wait for them to complete
236
255
  const detailsList = await Promise.all(detailPromises);
@@ -1,4 +1,6 @@
1
1
  export declare const TC_DETAILS_MAX_BATCH = 10;
2
+ export declare const TCG_POLL_INTERVAL_MS = 10000;
3
+ export declare const TCG_POLL_MAX_WAIT_MS: number;
2
4
  export declare const TCG_TRIGGER_URL: (baseUrl: string) => string;
3
5
  export declare const TCG_POLL_URL: (baseUrl: string) => string;
4
6
  export declare const FETCH_DETAILS_URL: (baseUrl: string) => string;
@@ -1,4 +1,9 @@
1
1
  export const TC_DETAILS_MAX_BATCH = 10;
2
+ // Wall-clock bounds for the TCG generation polling loops. Without a hard
3
+ // deadline, a job that never emits a "termination" message keeps the loop
4
+ // (and every caller awaiting it) alive forever.
5
+ export const TCG_POLL_INTERVAL_MS = 10_000;
6
+ export const TCG_POLL_MAX_WAIT_MS = 10 * 60 * 1000; // 10 minutes
2
7
  export const TCG_TRIGGER_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/suggest-test-cases`;
3
8
  export const TCG_POLL_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/test-cases-polling`;
4
9
  export const FETCH_DETAILS_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/fetch-test-case-details`;
@@ -1,3 +1,4 @@
1
+ import { wrapUntrusted } from "../../lib/untrusted-content.js";
1
2
  import { fetchFormFields, triggerTestCaseGeneration, pollScenariosTestDetails, bulkCreateTestCases, } from "./TCG-utils/api.js";
2
3
  import { buildDefaultFieldMaps, findBooleanFieldId, } from "./TCG-utils/helpers.js";
3
4
  import { signedUrlMap } from "../../lib/inmemory-store.js";
@@ -37,7 +38,7 @@ export async function createTestCasesFromFile(args, context, config) {
37
38
  content: [
38
39
  {
39
40
  type: "text",
40
- text: resultString,
41
+ text: wrapUntrusted("AI-generated test cases from the uploaded document", resultString),
41
42
  },
42
43
  {
43
44
  type: "text",
@@ -19,16 +19,30 @@ export const UploadFileSchema = z.object({
19
19
  .describe("ID of the project where the file should be uploaded. Do not assume it, always ask user for it."),
20
20
  file_path: z
21
21
  .string()
22
- .describe("Full path to the file that should be uploaded"),
22
+ .describe("Full path to the file that should be uploaded. Must be inside the " +
23
+ "directory configured via the MCP_UPLOAD_BASE_DIR environment variable."),
23
24
  });
24
25
  /**
25
26
  * Uploads a file to BrowserStack Test Management and returns the signed URL.
26
27
  */
27
28
  export async function uploadFile(args, config) {
28
29
  const { project_identifier, file_path } = args;
30
+ if (!appConfig.UPLOAD_BASE_DIR) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: "File upload is disabled. Set the MCP_UPLOAD_BASE_DIR environment " +
36
+ "variable to a directory that contains the files you want to upload, " +
37
+ "then restart the MCP server. Uploads are restricted to that directory.",
38
+ },
39
+ ],
40
+ isError: true,
41
+ };
42
+ }
29
43
  try {
30
44
  // Canonicalize path and enforce upload safety rules (extension, size,
31
- // hidden-directory traversal, optional base-dir containment).
45
+ // hidden-directory traversal, base-dir containment).
32
46
  const safePath = validateUploadPath(file_path, {
33
47
  allowedExtensions: TEST_MANAGEMENT_ATTACHMENT_EXTENSIONS,
34
48
  maxSizeBytes: MAX_ATTACHMENT_UPLOAD_BYTES,