@browserstack/mcp-server 1.4.0-beta.2 → 1.4.0

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 (49) hide show
  1. package/README.md +43 -29
  2. package/dist/config.d.ts +4 -1
  3. package/dist/config.js +23 -2
  4. package/dist/lib/constants.d.ts +3 -0
  5. package/dist/lib/constants.js +3 -0
  6. package/dist/lib/untrusted-content.d.ts +15 -0
  7. package/dist/lib/untrusted-content.js +24 -0
  8. package/dist/lib/version-resolver.d.ts +0 -5
  9. package/dist/lib/version-resolver.js +8 -2
  10. package/dist/logger.js +1 -10
  11. package/dist/tools/accessibility.js +4 -3
  12. package/dist/tools/accessiblity-utils/accessibility-rag.js +3 -1
  13. package/dist/tools/ask-browserstack/central-oauth.d.ts +7 -1
  14. package/dist/tools/ask-browserstack/central-oauth.js +25 -19
  15. package/dist/tools/ask-browserstack/config.d.ts +7 -1
  16. package/dist/tools/ask-browserstack/config.js +11 -5
  17. package/dist/tools/ask-browserstack/register.js +14 -1
  18. package/dist/tools/ask-browserstack/stream.js +9 -10
  19. package/dist/tools/ask-browserstack/types.d.ts +1 -1
  20. package/dist/tools/ask-browserstack/types.js +5 -1
  21. package/dist/tools/automate-utils/list-session-ids.d.ts +28 -0
  22. package/dist/tools/automate-utils/list-session-ids.js +87 -0
  23. package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +30 -0
  24. package/dist/tools/automate-utils/resolve-hashed-build-id.js +124 -0
  25. package/dist/tools/automate.d.ts +7 -0
  26. package/dist/tools/automate.js +104 -1
  27. package/dist/tools/build-insights.js +40 -1
  28. package/dist/tools/failurelogs-utils/app-automate.js +4 -3
  29. package/dist/tools/failurelogs-utils/automate.js +5 -4
  30. package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +2 -0
  31. package/dist/tools/failurelogs-utils/resolve-app-build-id.js +5 -0
  32. package/dist/tools/failurelogs-utils/video.d.ts +3 -0
  33. package/dist/tools/failurelogs-utils/video.js +25 -0
  34. package/dist/tools/get-failure-logs.js +28 -11
  35. package/dist/tools/observability.js +3 -1
  36. package/dist/tools/rca-agent-utils/format-rca.js +5 -4
  37. package/dist/tools/rca-agent-utils/get-failed-test-id.js +12 -0
  38. package/dist/tools/rca-agent-utils/types.d.ts +1 -0
  39. package/dist/tools/rca-agent.js +1 -1
  40. package/dist/tools/review-agent.js +2 -1
  41. package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
  42. package/dist/tools/sdk-utils/common/constants.js +2 -1
  43. package/dist/tools/selfheal.js +1 -1
  44. package/dist/tools/testmanagement-utils/testcase-from-file.js +2 -1
  45. package/dist/tools/testmanagement-utils/upload-file.js +16 -2
  46. package/dist/tools/testmanagement.js +10 -6
  47. package/dist/tools/tool-handoff.d.ts +30 -1
  48. package/dist/tools/tool-handoff.js +40 -9
  49. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { getBrowserStackAuth } from "../../lib/get-auth.js";
2
+ import { wrapUntrusted } from "../../lib/untrusted-content.js";
2
3
  import { filterLinesByKeywords, validateLogResponse, } from "./utils.js";
3
4
  import { apiClient } from "../../lib/apiClient.js";
4
5
  // NETWORK LOGS
@@ -22,7 +23,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
22
23
  entry.response.status >= 400 ||
23
24
  entry.response._error !== undefined);
24
25
  return failureEntries.length > 0
25
- ? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(failureEntries.map((entry) => ({
26
+ ? `Network Failures (${failureEntries.length} found):\n${wrapUntrusted("network logs", JSON.stringify(failureEntries.map((entry) => ({
26
27
  startedDateTime: entry.startedDateTime,
27
28
  request: {
28
29
  method: entry.request?.method,
@@ -36,7 +37,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
36
37
  },
37
38
  serverIPAddress: entry.serverIPAddress,
38
39
  time: entry.time,
39
- })), null, 2)}`
40
+ })), null, 2))}`
40
41
  : "No network failures found";
41
42
  }
42
43
  // SESSION LOGS
@@ -60,7 +61,7 @@ export async function retrieveSessionFailures(sessionId, config) {
60
61
  : JSON.stringify(response.data);
61
62
  const logs = filterSessionFailures(logText);
62
63
  return logs.length > 0
63
- ? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
64
+ ? `Session Failures (${logs.length} found):\n${wrapUntrusted("session logs", JSON.stringify(logs, null, 2))}`
64
65
  : "No session failures found";
65
66
  }
66
67
  // CONSOLE LOGS
@@ -84,7 +85,7 @@ export async function retrieveConsoleFailures(sessionId, config) {
84
85
  : JSON.stringify(response.data);
85
86
  const logs = filterConsoleFailures(logText);
86
87
  return logs.length > 0
87
- ? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
88
+ ? `Console Failures (${logs.length} found):\n${wrapUntrusted("console logs", JSON.stringify(logs, null, 2))}`
88
89
  : "No console failures found";
89
90
  }
90
91
  // FILTER: session logs
@@ -0,0 +1,2 @@
1
+ import { BrowserStackConfig } from "../../lib/types.js";
2
+ export declare function resolveAppAutomateBuildId(sessionId: string, config: BrowserStackConfig): Promise<string | undefined>;
@@ -0,0 +1,5 @@
1
+ import { SessionType } from "../../lib/constants.js";
2
+ import { resolveBuildIdFromSession } from "../automate-utils/resolve-hashed-build-id.js";
3
+ export async function resolveAppAutomateBuildId(sessionId, config) {
4
+ return resolveBuildIdFromSession(sessionId, SessionType.AppAutomate, config);
5
+ }
@@ -0,0 +1,3 @@
1
+ import { BrowserStackConfig } from "../../lib/types.js";
2
+ import { SessionType } from "../../lib/constants.js";
3
+ export declare function retrieveSessionVideo(sessionId: string, sessionType: SessionType, config: BrowserStackConfig): Promise<string>;
@@ -0,0 +1,25 @@
1
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
2
+ import { apiClient } from "../../lib/apiClient.js";
3
+ import { SessionType } from "../../lib/constants.js";
4
+ import { validateLogResponse } from "./utils.js";
5
+ export async function retrieveSessionVideo(sessionId, sessionType, config) {
6
+ const product = sessionType === SessionType.AppAutomate ? "app-automate" : "automate";
7
+ const url = `https://api.browserstack.com/${product}/sessions/${encodeURIComponent(sessionId)}.json`;
8
+ const authString = getBrowserStackAuth(config);
9
+ const auth = Buffer.from(authString).toString("base64");
10
+ const response = await apiClient.get({
11
+ url,
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ Authorization: `Basic ${auth}`,
15
+ },
16
+ raise_error: false,
17
+ });
18
+ const validationError = validateLogResponse(response, "session video");
19
+ if (validationError)
20
+ return validationError.message;
21
+ const videoUrl = response.data?.automation_session?.video_url;
22
+ return typeof videoUrl === "string" && videoUrl.trim()
23
+ ? `Session video: ${videoUrl.trim()}`
24
+ : "No session video available for this session";
25
+ }
@@ -3,7 +3,9 @@ import { trackMCP } from "../lib/instrumentation.js";
3
3
  import { NEEDS_SESSION_ID } from "./tool-handoff.js";
4
4
  import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
5
5
  import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
6
- 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";
7
9
  // Main log fetcher function
8
10
  export async function getFailureLogs(args, config) {
9
11
  const results = [];
@@ -12,17 +14,16 @@ export async function getFailureLogs(args, config) {
12
14
  if (!args.sessionId) {
13
15
  throw new Error("Session ID is required");
14
16
  }
15
- if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
16
- throw new Error("Build ID is required for app-automate sessions");
17
- }
18
17
  // Validate log types and collect errors
19
18
  validLogTypes = args.logTypes.filter((logType) => {
20
19
  const isAutomate = Object.values(AutomateLogType).includes(logType);
21
20
  const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
22
21
  if (!isAutomate && !isAppAutomate) {
23
22
  errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
24
- ...Object.values(AutomateLogType),
25
- ...Object.values(AppAutomateLogType),
23
+ ...new Set([
24
+ ...Object.values(AutomateLogType),
25
+ ...Object.values(AppAutomateLogType),
26
+ ]),
26
27
  ].join(", ")}`);
27
28
  return false;
28
29
  }
@@ -47,11 +48,26 @@ export async function getFailureLogs(args, config) {
47
48
  isError: true,
48
49
  };
49
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
+ }
50
61
  let response;
51
62
  // eslint-disable-next-line no-useless-catch
52
63
  try {
53
64
  for (const logType of validLogTypes) {
54
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
+ }
55
71
  case AutomateLogType.NetworkLogs: {
56
72
  response = await retrieveNetworkFailures(args.sessionId, config);
57
73
  results.push({ type: "text", text: response });
@@ -68,17 +84,17 @@ export async function getFailureLogs(args, config) {
68
84
  break;
69
85
  }
70
86
  case AppAutomateLogType.DeviceLogs: {
71
- response = await retrieveDeviceLogs(args.sessionId, args.buildId, config);
87
+ response = await retrieveDeviceLogs(args.sessionId, buildId, config);
72
88
  results.push({ type: "text", text: response });
73
89
  break;
74
90
  }
75
91
  case AppAutomateLogType.AppiumLogs: {
76
- response = await retrieveAppiumLogs(args.sessionId, args.buildId, config);
92
+ response = await retrieveAppiumLogs(args.sessionId, buildId, config);
77
93
  results.push({ type: "text", text: response });
78
94
  break;
79
95
  }
80
96
  case AppAutomateLogType.CrashLogs: {
81
- response = await retrieveCrashLogs(args.sessionId, args.buildId, config);
97
+ response = await retrieveCrashLogs(args.sessionId, buildId, config);
82
98
  results.push({ type: "text", text: response });
83
99
  break;
84
100
  }
@@ -99,7 +115,7 @@ export async function getFailureLogs(args, config) {
99
115
  // Register tool with the MCP server
100
116
  export default function registerGetFailureLogs(server, config) {
101
117
  const tools = {};
102
- 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." +
103
119
  NEEDS_SESSION_ID, {
104
120
  sessionType: z
105
121
  .enum([SessionType.Automate, SessionType.AppAutomate])
@@ -110,7 +126,7 @@ export default function registerGetFailureLogs(server, config) {
110
126
  buildId: z
111
127
  .string()
112
128
  .optional()
113
- .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."),
114
130
  logTypes: z
115
131
  .array(z.enum([
116
132
  AutomateLogType.NetworkLogs,
@@ -119,6 +135,7 @@ export default function registerGetFailureLogs(server, config) {
119
135
  AppAutomateLogType.DeviceLogs,
120
136
  AppAutomateLogType.AppiumLogs,
121
137
  AppAutomateLogType.CrashLogs,
138
+ SessionVideoLogType,
122
139
  ]))
123
140
  .describe("The types of logs to fetch."),
124
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
  };
@@ -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",
@@ -177,7 +177,7 @@ export default function addRCATools(server, config) {
177
177
  return handleMCPError("listBuildId", server, config, error);
178
178
  }
179
179
  });
180
- tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status); optional status filter." +
180
+ tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status and session id); optional status filter." +
181
181
  NEEDS_BUILD_ID, LIST_TEST_IDS_PARAMS, {
182
182
  title: "List Test IDs",
183
183
  readOnlyHint: true,
@@ -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.";
@@ -506,7 +506,7 @@ export default function addSelfHealTools(server, config) {
506
506
  sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
507
507
  }, {
508
508
  title: "Prepare Self-Healing Plan",
509
- readOnlyHint: true,
509
+ readOnlyHint: false,
510
510
  openWorldHint: false,
511
511
  destructiveHint: false,
512
512
  idempotentHint: true,
@@ -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,
@@ -19,7 +19,7 @@ import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-test
19
19
  import { listSubTestPlans, ListSubTestPlansSchema, } from "./testmanagement-utils/list-sub-testplans.js";
20
20
  import { getSubTestPlan, GetSubTestPlanSchema, } from "./testmanagement-utils/get-sub-testplan.js";
21
21
  import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
22
- import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID } from "./tool-handoff.js";
22
+ import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID, PLAN_WRITES_VIA_AGENT, PROJECT_ID_ONLY_FOR_FOLDER, } from "./tool-handoff.js";
23
23
  //TODO: Moving the traceMCP and catch block to the parent(server) function
24
24
  /**
25
25
  * Wrapper to call createProjectOrFolder util.
@@ -434,7 +434,7 @@ export async function getSubTestPlanTool(args, config, server) {
434
434
  export default function addTestManagementTools(server, config) {
435
435
  const tools = {};
436
436
  tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management." +
437
- NEEDS_PROJECT_ID, CreateProjFoldSchema.shape, {
437
+ PROJECT_ID_ONLY_FOR_FOLDER, CreateProjFoldSchema.shape, {
438
438
  title: "Create Project or Folder",
439
439
  readOnlyHint: false,
440
440
  openWorldHint: false,
@@ -535,7 +535,8 @@ export default function addTestManagementTools(server, config) {
535
535
  idempotentHint: false,
536
536
  }, (args, context) => createLCAStepsTool(args, context, config, server));
537
537
  tools.listTestPlans = server.tool("listTestPlans", "List test plans in a BrowserStack Test Management project. Returns each plan's identifier (TP-*), name, status, description, dates, and active/closed test-run counts. Supports pagination." +
538
- NEEDS_PROJECT_ID, ListTestPlansSchema.shape, {
538
+ NEEDS_PROJECT_ID +
539
+ PLAN_WRITES_VIA_AGENT, ListTestPlansSchema.shape, {
539
540
  title: "List Test Plans",
540
541
  readOnlyHint: true,
541
542
  openWorldHint: false,
@@ -544,7 +545,8 @@ export default function addTestManagementTools(server, config) {
544
545
  }, (args) => listTestPlansTool(args, config, server));
545
546
  tools.getTestPlan = server.tool("getTestPlan", "Fetch a test plan by identifier (TP-*) from BrowserStack Test Management. Returns plan metadata, the full list of linked test runs, total test-case count across runs, and a status summary — suitable for generating test documentation or QA status reports." +
546
547
  NEEDS_PROJECT_ID +
547
- NEEDS_TEST_PLAN_ID, GetTestPlanSchema.shape, {
548
+ NEEDS_TEST_PLAN_ID +
549
+ PLAN_WRITES_VIA_AGENT, GetTestPlanSchema.shape, {
548
550
  title: "Get Test Plan",
549
551
  readOnlyHint: true,
550
552
  openWorldHint: false,
@@ -553,7 +555,8 @@ export default function addTestManagementTools(server, config) {
553
555
  }, (args) => getTestPlanTool(args, config, server));
554
556
  tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
555
557
  NEEDS_PROJECT_ID +
556
- NEEDS_TEST_PLAN_ID, ListSubTestPlansSchema.shape, {
558
+ NEEDS_TEST_PLAN_ID +
559
+ PLAN_WRITES_VIA_AGENT, ListSubTestPlansSchema.shape, {
557
560
  title: "List Sub Test Plans",
558
561
  readOnlyHint: true,
559
562
  openWorldHint: false,
@@ -562,7 +565,8 @@ export default function addTestManagementTools(server, config) {
562
565
  }, (args) => listSubTestPlansTool(args, config, server));
563
566
  tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
564
567
  NEEDS_PROJECT_ID +
565
- NEEDS_TEST_PLAN_ID, GetSubTestPlanSchema.shape, {
568
+ NEEDS_TEST_PLAN_ID +
569
+ PLAN_WRITES_VIA_AGENT, GetSubTestPlanSchema.shape, {
566
570
  title: "Get Sub Test Plan",
567
571
  readOnlyHint: true,
568
572
  openWorldHint: false,
@@ -23,11 +23,40 @@
23
23
  export declare const NEEDS_PROJECT_ID: string;
24
24
  /** A sibling tool can produce the id — prefer it over the agent. */
25
25
  export declare function needsIdFrom(idLabel: string, sourceTool: string): string;
26
+ /**
27
+ * createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
28
+ * there, and the create-a-PROJECT half needs no id at all. With the generic constant the
29
+ * tool read "Requires a project identifier ... call askBrowserStackAI", which routed
30
+ * "create me a project" through the agent before letting the tool run.
31
+ */
32
+ export declare const PROJECT_ID_ONLY_FOR_FOLDER: string;
26
33
  /** A test plan id (TP-*) comes from listTestPlans. */
27
34
  export declare const NEEDS_TEST_PLAN_ID: string;
35
+ /**
36
+ * The ONLY capability handoff here: every other constant points at a tool that produces a
37
+ * missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
38
+ * `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
39
+ * (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
40
+ * /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
41
+ * read tools, finds no create, and reports the capability as absent — which is exactly what
42
+ * a QA eval concluded.
43
+ *
44
+ * Deliberately narrow: it names the specific operations that are missing rather than
45
+ * inviting the model to route plan work to the agent generally, because the tool
46
+ * descriptions otherwise say to prefer a specific tool whenever one fits.
47
+ *
48
+ * Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
49
+ * only completes on a client that can show a prompt. On one that cannot, the intended write
50
+ * comes back in `needs_approval` instead of happening.
51
+ */
52
+ export declare const PLAN_WRITES_VIA_AGENT: string;
28
53
  /** A build id comes from either build-lookup tool. */
29
54
  export declare const NEEDS_BUILD_ID: string;
30
- /** Session ids are not listable by any tool here. */
55
+ /**
56
+ * Session ids ARE listable now — PR #395 added `listSessions`, which merged into main while
57
+ * this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
58
+ * pointing at the agent when a real tool exists is exactly what the header above forbids.
59
+ */
31
60
  export declare const NEEDS_SESSION_ID: string;
32
61
  /** A completed scan's ids come from startAccessibilityScan, or from the agent. */
33
62
  export declare const NEEDS_A11Y_SCAN_ID: string;
@@ -27,21 +27,52 @@ export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool
27
27
  export function needsIdFrom(idLabel, sourceTool) {
28
28
  return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
29
29
  }
30
+ /**
31
+ * createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
32
+ * there, and the create-a-PROJECT half needs no id at all. With the generic constant the
33
+ * tool read "Requires a project identifier ... call askBrowserStackAI", which routed
34
+ * "create me a project" through the agent before letting the tool run.
35
+ */
36
+ export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
37
+ "needs that project's identifier (PR-*); no tool here lists projects, so ask " +
38
+ 'askBrowserStackAI with product "tm" for it.';
30
39
  /** A test plan id (TP-*) comes from listTestPlans. */
31
40
  export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
41
+ /**
42
+ * The ONLY capability handoff here: every other constant points at a tool that produces a
43
+ * missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
44
+ * `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
45
+ * (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
46
+ * /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
47
+ * read tools, finds no create, and reports the capability as absent — which is exactly what
48
+ * a QA eval concluded.
49
+ *
50
+ * Deliberately narrow: it names the specific operations that are missing rather than
51
+ * inviting the model to route plan work to the agent generally, because the tool
52
+ * descriptions otherwise say to prefer a specific tool whenever one fits.
53
+ *
54
+ * Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
55
+ * only completes on a client that can show a prompt. On one that cannot, the intended write
56
+ * comes back in `needs_approval` instead of happening.
57
+ */
58
+ export const PLAN_WRITES_VIA_AGENT = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
59
+ 'available as tools here: call askBrowserStackAI with product "tm" and describe what you ' +
60
+ "want. It asks you to confirm before changing anything.";
32
61
  /** A build id comes from either build-lookup tool. */
33
62
  export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
34
- /** Session ids are not listable by any tool here. */
35
- export const NEEDS_SESSION_ID = " Requires a session id, which no tool here lists. If you only know the build, call " +
36
- "getBuildId or listBuildId; if you have neither, call askBrowserStackAI with product " +
37
- '"tra" and describe the run you mean.';
63
+ /**
64
+ * Session ids ARE listable now PR #395 added `listSessions`, which merged into main while
65
+ * this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
66
+ * pointing at the agent when a real tool exists is exactly what the header above forbids.
67
+ */
68
+ export const NEEDS_SESSION_ID = " Requires a session id. listSessions lists them for a build; if you do not have the " +
69
+ "build either, getBuildId or listBuildId resolves one from a project and build name.";
38
70
  /** A completed scan's ids come from startAccessibilityScan, or from the agent. */
39
- export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan. They are returned by startAccessibilityScan; " +
40
- 'for a scan run earlier, call askBrowserStackAI with product "a11y" to locate it, since ' +
41
- "no tool here lists past scans.";
71
+ export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " +
72
+ "No tool here lists past scans, so if you do not have the ids, start a new scan rather " +
73
+ "than guessing.";
42
74
  /** Auth-config ids are not listable by any tool here. */
43
75
  export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
44
- "existing configurations, so if you do not have the id, call askBrowserStackAI with " +
45
- 'product "a11y".';
76
+ "existing configurations, so if you do not have the id, create one rather than guessing.";
46
77
  /** Test ids come from listTestIds, which itself needs a build id. */
47
78
  export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.4.0-beta.2",
3
+ "version": "1.4.0",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",