@browserstack/mcp-server 1.2.36 → 1.3.1-beta.1

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 (40) hide show
  1. package/README.md +2 -0
  2. package/dist/config.d.ts +4 -1
  3. package/dist/config.js +23 -2
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/lib/tm-base-url.js +7 -1
  7. package/dist/tools/accessibility.js +30 -0
  8. package/dist/tools/appautomate.js +20 -2
  9. package/dist/tools/applive.js +6 -0
  10. package/dist/tools/automate.js +6 -0
  11. package/dist/tools/bstack-sdk.js +7 -1
  12. package/dist/tools/build-insights.js +16 -3
  13. package/dist/tools/get-failure-logs.js +6 -0
  14. package/dist/tools/live.js +7 -1
  15. package/dist/tools/observability.js +6 -0
  16. package/dist/tools/percy-sdk.js +49 -7
  17. package/dist/tools/rca-agent-utils/constants.d.ts +2 -0
  18. package/dist/tools/rca-agent-utils/constants.js +8 -0
  19. package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +3 -2
  20. package/dist/tools/rca-agent-utils/get-failed-test-id.js +94 -25
  21. package/dist/tools/rca-agent-utils/types.d.ts +10 -0
  22. package/dist/tools/rca-agent.d.ts +1 -0
  23. package/dist/tools/rca-agent.js +30 -6
  24. package/dist/tools/selfheal.js +12 -0
  25. package/dist/tools/testmanagement.js +119 -17
  26. package/dist/tools/tfa-rca-collaboration.d.ts +15 -0
  27. package/dist/tools/tfa-rca-collaboration.js +162 -0
  28. package/dist/tools/tfa-rca-utils/build-failure-themes.d.ts +61 -0
  29. package/dist/tools/tfa-rca-utils/build-failure-themes.js +133 -0
  30. package/dist/tools/tfa-rca-utils/constants.d.ts +67 -0
  31. package/dist/tools/tfa-rca-utils/constants.js +103 -0
  32. package/dist/tools/tfa-rca-utils/submit-turn.d.ts +29 -0
  33. package/dist/tools/tfa-rca-utils/submit-turn.js +188 -0
  34. package/dist/tools/tfa-rca-utils/trigger-report.d.ts +22 -0
  35. package/dist/tools/tfa-rca-utils/trigger-report.js +61 -0
  36. package/dist/tools/tfa-rca-utils/turn-result.d.ts +14 -0
  37. package/dist/tools/tfa-rca-utils/turn-result.js +131 -0
  38. package/dist/tools/tfa-rca-utils/types.d.ts +61 -0
  39. package/dist/tools/tfa-rca-utils/types.js +9 -0
  40. package/package.json +1 -1
@@ -17,9 +17,19 @@ export interface TestRun {
17
17
  next_page: string | null;
18
18
  };
19
19
  }
20
+ export interface TestFailureSignature {
21
+ category?: string;
22
+ error_summary?: string;
23
+ file_path?: string;
24
+ is_flaky?: boolean;
25
+ is_always_failing?: boolean;
26
+ is_new_failure?: boolean;
27
+ }
20
28
  export interface FailedTestInfo {
21
29
  test_id: number;
22
30
  test_name: string;
31
+ status?: TestStatus;
32
+ failure?: TestFailureSignature;
23
33
  }
24
34
  export declare enum RCAState {
25
35
  PENDING = "pending",
@@ -11,5 +11,6 @@ export declare function fetchRCADataTool(args: {
11
11
  export declare function listTestIdsTool(args: {
12
12
  buildId: string;
13
13
  status?: TestStatus;
14
+ includeFailureDetail?: boolean;
14
15
  }, config: BrowserStackConfig): Promise<CallToolResult>;
15
16
  export default function addRCATools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
@@ -101,10 +101,10 @@ export async function fetchRCADataTool(args, config) {
101
101
  }
102
102
  export async function listTestIdsTool(args, config) {
103
103
  try {
104
- const { buildId, status } = args;
104
+ const { buildId, status, includeFailureDetail } = args;
105
105
  const authString = getBrowserStackAuth(config);
106
106
  // Get test IDs
107
- const testIds = await getTestIds(buildId, authString, status);
107
+ const testIds = await getTestIds(buildId, authString, status, includeFailureDetail);
108
108
  return {
109
109
  content: [
110
110
  {
@@ -130,7 +130,13 @@ export async function listTestIdsTool(args, config) {
130
130
  }
131
131
  export default function addRCATools(server, config) {
132
132
  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, async (args) => {
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
+ title: "Fetch Root Cause Analysis",
135
+ readOnlyHint: true,
136
+ openWorldHint: false,
137
+ destructiveHint: false,
138
+ idempotentHint: true,
139
+ }, async (args) => {
134
140
  try {
135
141
  trackMCP("fetchRCA", server.server.getClientVersion(), undefined, config);
136
142
  return await fetchRCADataTool(args, config);
@@ -139,7 +145,13 @@ export default function addRCATools(server, config) {
139
145
  return handleMCPError("fetchRCA", server, config, error);
140
146
  }
141
147
  });
142
- tools.getBuildId = server.tool("getBuildId", "Get the BrowserStack build ID for a given project and build name, scoped to the current user's builds.", GET_BUILD_ID_PARAMS, async (args) => {
148
+ tools.getBuildId = server.tool("getBuildId", "Get the BrowserStack build ID for a given project and build name, scoped to the current user's builds.", GET_BUILD_ID_PARAMS, {
149
+ title: "Get Build ID",
150
+ readOnlyHint: true,
151
+ openWorldHint: false,
152
+ destructiveHint: false,
153
+ idempotentHint: true,
154
+ }, async (args) => {
143
155
  try {
144
156
  trackMCP("getBuildId", server.server.getClientVersion(), undefined, config);
145
157
  return await getBuildIdTool(args, config);
@@ -148,7 +160,13 @@ export default function addRCATools(server, config) {
148
160
  return handleMCPError("getBuildId", server, config, error);
149
161
  }
150
162
  });
151
- tools.listBuildId = server.tool("listBuildId", "Get the latest build ID for a project and build name, across all users (no user filter).", GET_BUILD_ID_PARAMS, async (args) => {
163
+ tools.listBuildId = server.tool("listBuildId", "Get the latest build ID for a project and build name, across all users (no user filter).", GET_BUILD_ID_PARAMS, {
164
+ title: "List Build IDs",
165
+ readOnlyHint: true,
166
+ openWorldHint: false,
167
+ destructiveHint: false,
168
+ idempotentHint: true,
169
+ }, async (args) => {
152
170
  try {
153
171
  trackMCP("listBuildId", server.server.getClientVersion(), undefined, config);
154
172
  return await listBuildIdTool(args, config);
@@ -157,7 +175,13 @@ export default function addRCATools(server, config) {
157
175
  return handleMCPError("listBuildId", server, config, error);
158
176
  }
159
177
  });
160
- tools.listTestIds = server.tool("listTestIds", "List test IDs from a BrowserStack Automate build, optionally filtered by status", LIST_TEST_IDS_PARAMS, async (args) => {
178
+ tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status); optional status filter.", LIST_TEST_IDS_PARAMS, {
179
+ title: "List Test IDs",
180
+ readOnlyHint: true,
181
+ openWorldHint: false,
182
+ destructiveHint: false,
183
+ idempotentHint: true,
184
+ }, async (args) => {
161
185
  try {
162
186
  trackMCP("listTestIds", server.server.getClientVersion(), undefined, config);
163
187
  return await listTestIdsTool(args, config);
@@ -443,6 +443,12 @@ export default function addSelfHealTools(server, config) {
443
443
  .string()
444
444
  .describe("Build UUID. Fetches the build's self-healing report.")
445
445
  .optional(),
446
+ }, {
447
+ title: "Fetch Self-Healed Selectors",
448
+ readOnlyHint: true,
449
+ openWorldHint: false,
450
+ destructiveHint: false,
451
+ idempotentHint: true,
446
452
  }, async (args) => {
447
453
  try {
448
454
  trackMCP("fetchSelfHealedSelectors", server.server.getClientVersion(), undefined, config);
@@ -495,6 +501,12 @@ export default function addSelfHealTools(server, config) {
495
501
  "(`session_id`, `original_locator`, `healed_locator`, " +
496
502
  "`healing_thought`).", {
497
503
  sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
504
+ }, {
505
+ title: "Prepare Self-Healing Plan",
506
+ readOnlyHint: true,
507
+ openWorldHint: false,
508
+ destructiveHint: false,
509
+ idempotentHint: true,
498
510
  }, async (args) => {
499
511
  try {
500
512
  trackMCP("prepareSelfHealingPlan", server.server.getClientVersion(), undefined, config);
@@ -432,22 +432,124 @@ export async function getSubTestPlanTool(args, config, server) {
432
432
  */
433
433
  export default function addTestManagementTools(server, config) {
434
434
  const tools = {};
435
- tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management.", CreateProjFoldSchema.shape, (args) => createProjectOrFolderTool(args, config, server));
436
- tools.createTestCase = server.tool("createTestCase", "Use this tool to create a test case in BrowserStack Test Management.", CreateTestCaseSchema.shape, (args) => createTestCaseTool(args, config, server));
437
- tools.updateTestCase = server.tool("updateTestCase", "Update an existing test case in BrowserStack Test Management. Any subset of the following fields may be changed: name, description, preconditions, test_case_steps, owner, priority, case_type, automation_status, status, tags, issues, custom_fields. Only the supplied fields are modified.", UpdateTestCaseSchema.shape, (args) => updateTestCaseTool(args, config, server));
438
- tools.listTestCases = server.tool("listTestCases", "List test cases in a project, optionally scoped to a specific folder. Omit folder_id to list all test cases in the project; provide folder_id (discoverable via listFolders) to list only that folder's cases. Supports filters: case_type, priority, pagination.", ListTestCasesSchema.shape, (args) => listTestCasesTool(args, config, server));
439
- tools.listFolders = server.tool("listFolders", "List folders in a BrowserStack Test Management project, returning each folder's id and name (plus case counts and sub-folder counts). Pass parent_id to list sub-folders under a specific folder instead of top-level folders.", ListFoldersSchema.shape, (args) => listFoldersTool(args, config, server));
440
- tools.listTestCaseTemplates = server.tool("listTestCaseTemplates", "List test-case templates with their numeric template_id. Use the id with createTestCase to apply a custom template (the 'template' slug only selects system templates).", ListTemplatesSchema.shape, (args) => listTemplatesTool(args, config, server));
441
- tools.createTestRun = server.tool("createTestRun", "Create a test run in BrowserStack Test Management.", CreateTestRunSchema.shape, (args) => createTestRunTool(args, config, server));
442
- tools.listTestRuns = server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)", ListTestRunsSchema.shape, (args) => listTestRunsTool(args, config, server));
443
- tools.updateTestRun = server.tool("updateTestRun", "Update a test run's metadata and/or add test cases to it.", UpdateTestRunSchema.shape, (args) => updateTestRunTool(args, config, server));
444
- tools.addTestResult = server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API.", AddTestResultSchema.shape, (args) => addTestResultTool(args, config, server));
445
- tools.uploadProductRequirementFile = server.tool("uploadProductRequirementFile", "Upload files (e.g., PDRs, PDFs) to BrowserStack Test Management and retrieve a file mapping ID. This is utilized for generating test cases from files and is part of the Test Case Generator AI Agent in BrowserStack.", UploadFileSchema.shape, (args) => uploadProductRequirementFileTool(args, config, server));
446
- tools.createTestCasesFromFile = server.tool("createTestCasesFromFile", "Generate test cases from a file in BrowserStack Test Management using the Test Case Generator AI Agent.", CreateTestCasesFromFileSchema.shape, (args, context) => createTestCasesFromFileTool(args, context, config, server));
447
- tools.createLCASteps = server.tool("createLCASteps", "Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent.", CreateLCAStepsSchema.shape, (args, context) => createLCAStepsTool(args, context, config, server));
448
- 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.", ListTestPlansSchema.shape, (args) => listTestPlansTool(args, config, server));
449
- 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.", GetTestPlanSchema.shape, (args) => getTestPlanTool(args, config, server));
450
- tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination.", ListSubTestPlansSchema.shape, (args) => listSubTestPlansTool(args, config, server));
451
- tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs.", GetSubTestPlanSchema.shape, (args) => getSubTestPlanTool(args, config, server));
435
+ tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management.", CreateProjFoldSchema.shape, {
436
+ title: "Create Project or Folder",
437
+ readOnlyHint: false,
438
+ openWorldHint: false,
439
+ destructiveHint: false,
440
+ idempotentHint: false,
441
+ }, (args) => createProjectOrFolderTool(args, config, server));
442
+ tools.createTestCase = server.tool("createTestCase", "Use this tool to create a test case in BrowserStack Test Management.", CreateTestCaseSchema.shape, {
443
+ title: "Create Test Case",
444
+ readOnlyHint: false,
445
+ openWorldHint: false,
446
+ destructiveHint: false,
447
+ idempotentHint: false,
448
+ }, (args) => createTestCaseTool(args, config, server));
449
+ tools.updateTestCase = server.tool("updateTestCase", "Update an existing test case in BrowserStack Test Management. Any subset of the following fields may be changed: name, description, preconditions, test_case_steps, owner, priority, case_type, automation_status, status, tags, issues, custom_fields. Only the supplied fields are modified.", UpdateTestCaseSchema.shape, {
450
+ title: "Update Test Case",
451
+ readOnlyHint: false,
452
+ openWorldHint: false,
453
+ destructiveHint: true,
454
+ idempotentHint: true,
455
+ }, (args) => updateTestCaseTool(args, config, server));
456
+ tools.listTestCases = server.tool("listTestCases", "List test cases in a project, optionally scoped to a specific folder. Omit folder_id to list all test cases in the project; provide folder_id (discoverable via listFolders) to list only that folder's cases. Supports filters: case_type, priority, pagination.", ListTestCasesSchema.shape, {
457
+ title: "List Test Cases",
458
+ readOnlyHint: true,
459
+ openWorldHint: false,
460
+ destructiveHint: false,
461
+ idempotentHint: true,
462
+ }, (args) => listTestCasesTool(args, config, server));
463
+ tools.listFolders = server.tool("listFolders", "List folders in a BrowserStack Test Management project, returning each folder's id and name (plus case counts and sub-folder counts). Pass parent_id to list sub-folders under a specific folder instead of top-level folders.", ListFoldersSchema.shape, {
464
+ title: "List Folders",
465
+ readOnlyHint: true,
466
+ openWorldHint: false,
467
+ destructiveHint: false,
468
+ idempotentHint: true,
469
+ }, (args) => listFoldersTool(args, config, server));
470
+ tools.listTestCaseTemplates = server.tool("listTestCaseTemplates", "List test-case templates with their numeric template_id. Use the id with createTestCase to apply a custom template (the 'template' slug only selects system templates).", ListTemplatesSchema.shape, {
471
+ title: "List Test Case Templates",
472
+ readOnlyHint: true,
473
+ openWorldHint: false,
474
+ destructiveHint: false,
475
+ idempotentHint: true,
476
+ }, (args) => listTemplatesTool(args, config, server));
477
+ tools.createTestRun = server.tool("createTestRun", "Create a test run in BrowserStack Test Management.", CreateTestRunSchema.shape, {
478
+ title: "Create Test Run",
479
+ readOnlyHint: false,
480
+ openWorldHint: false,
481
+ destructiveHint: false,
482
+ idempotentHint: false,
483
+ }, (args) => createTestRunTool(args, config, server));
484
+ tools.listTestRuns = server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)", ListTestRunsSchema.shape, {
485
+ title: "List Test Runs",
486
+ readOnlyHint: true,
487
+ openWorldHint: false,
488
+ destructiveHint: false,
489
+ idempotentHint: true,
490
+ }, (args) => listTestRunsTool(args, config, server));
491
+ tools.updateTestRun = server.tool("updateTestRun", "Update a test run's metadata and/or add test cases to it.", UpdateTestRunSchema.shape, {
492
+ title: "Update Test Run",
493
+ readOnlyHint: false,
494
+ openWorldHint: false,
495
+ destructiveHint: true,
496
+ idempotentHint: true,
497
+ }, (args) => updateTestRunTool(args, config, server));
498
+ tools.addTestResult = server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API.", AddTestResultSchema.shape, {
499
+ title: "Add Test Result",
500
+ readOnlyHint: false,
501
+ openWorldHint: false,
502
+ destructiveHint: false,
503
+ idempotentHint: false,
504
+ }, (args) => addTestResultTool(args, config, server));
505
+ tools.uploadProductRequirementFile = server.tool("uploadProductRequirementFile", "Upload files (e.g., PDRs, PDFs) to BrowserStack Test Management and retrieve a file mapping ID. This is utilized for generating test cases from files and is part of the Test Case Generator AI Agent in BrowserStack.", UploadFileSchema.shape, {
506
+ title: "Upload Product Requirement File",
507
+ readOnlyHint: false,
508
+ openWorldHint: false,
509
+ destructiveHint: false,
510
+ idempotentHint: false,
511
+ }, (args) => uploadProductRequirementFileTool(args, config, server));
512
+ tools.createTestCasesFromFile = server.tool("createTestCasesFromFile", "Generate test cases from a file in BrowserStack Test Management using the Test Case Generator AI Agent.", CreateTestCasesFromFileSchema.shape, {
513
+ title: "Create Test Cases from File",
514
+ readOnlyHint: false,
515
+ openWorldHint: false,
516
+ destructiveHint: false,
517
+ idempotentHint: false,
518
+ }, (args, context) => createTestCasesFromFileTool(args, context, config, server));
519
+ tools.createLCASteps = server.tool("createLCASteps", "Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent.", CreateLCAStepsSchema.shape, {
520
+ title: "Create LCA Steps",
521
+ readOnlyHint: false,
522
+ openWorldHint: false,
523
+ destructiveHint: false,
524
+ idempotentHint: false,
525
+ }, (args, context) => createLCAStepsTool(args, context, config, server));
526
+ 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.", ListTestPlansSchema.shape, {
527
+ title: "List Test Plans",
528
+ readOnlyHint: true,
529
+ openWorldHint: false,
530
+ destructiveHint: false,
531
+ idempotentHint: true,
532
+ }, (args) => listTestPlansTool(args, config, server));
533
+ 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.", GetTestPlanSchema.shape, {
534
+ title: "Get Test Plan",
535
+ readOnlyHint: true,
536
+ openWorldHint: false,
537
+ destructiveHint: false,
538
+ idempotentHint: true,
539
+ }, (args) => getTestPlanTool(args, config, server));
540
+ tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination.", ListSubTestPlansSchema.shape, {
541
+ title: "List Sub Test Plans",
542
+ readOnlyHint: true,
543
+ openWorldHint: false,
544
+ destructiveHint: false,
545
+ idempotentHint: true,
546
+ }, (args) => listSubTestPlansTool(args, config, server));
547
+ tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs.", GetSubTestPlanSchema.shape, {
548
+ title: "Get Sub Test Plan",
549
+ readOnlyHint: true,
550
+ openWorldHint: false,
551
+ destructiveHint: false,
552
+ idempotentHint: true,
553
+ }, (args) => getSubTestPlanTool(args, config, server));
452
554
  return tools;
453
555
  }
@@ -0,0 +1,15 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { BrowserStackConfig } from "../lib/types.js";
4
+ import { TfaRcaTurnArgs } from "./tfa-rca-utils/submit-turn.js";
5
+ import { GetTfaTurnResultArgs } from "./tfa-rca-utils/turn-result.js";
6
+ import { TriggerRcaReportArgs } from "./tfa-rca-utils/trigger-report.js";
7
+ import { ListTestsInFailureThemeArgs } from "./tfa-rca-utils/build-failure-themes.js";
8
+ export declare function tfaRcaTurnTool(args: TfaRcaTurnArgs, config: BrowserStackConfig, context?: any): Promise<CallToolResult>;
9
+ export declare function getTfaTurnResultTool(args: GetTfaTurnResultArgs, config: BrowserStackConfig): Promise<CallToolResult>;
10
+ export declare function triggerRcaReportTool(args: TriggerRcaReportArgs, config: BrowserStackConfig): Promise<CallToolResult>;
11
+ export declare function getBuildFailureThemesTool(args: {
12
+ buildUuid: string;
13
+ }, config: BrowserStackConfig): Promise<CallToolResult>;
14
+ export declare function listTestsInFailureThemeTool(args: ListTestsInFailureThemeArgs, config: BrowserStackConfig): Promise<CallToolResult>;
15
+ export default function addTfaRcaCollaborationTools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
@@ -0,0 +1,162 @@
1
+ import { trackMCP } from "../lib/instrumentation.js";
2
+ import { handleMCPError } from "../lib/utils.js";
3
+ import { GET_BUILD_FAILURE_THEMES_PARAMS, GET_TFA_TURN_RESULT_PARAMS, LIST_TESTS_IN_FAILURE_THEME_PARAMS, TFA_RCA_TURN_PARAMS, TRIGGER_RCA_REPORT_PARAMS, } from "./tfa-rca-utils/constants.js";
4
+ import { submitTfaRcaTurn, } from "./tfa-rca-utils/submit-turn.js";
5
+ import { getTfaTurnResult, TfaRcaTurnError, } from "./tfa-rca-utils/turn-result.js";
6
+ import { triggerRcaReport, TriggerRcaReportError, } from "./tfa-rca-utils/trigger-report.js";
7
+ import { BuildFailureThemesError, fetchBuildFailureThemes, fetchTestsInFailureTheme, } from "./tfa-rca-utils/build-failure-themes.js";
8
+ const TOOL_NAME = "tfaRcaTurn";
9
+ const GET_RESULT_TOOL_NAME = "getTfaTurnResult";
10
+ const TRIGGER_TOOL_NAME = "triggerRcaReport";
11
+ const GET_BUILD_FAILURE_THEMES_TOOL_NAME = "getBuildFailureThemes";
12
+ const LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME = "listTestsInFailureTheme";
13
+ /** Wrap a domain error into the standard `{ isError: true }` envelope. */
14
+ function domainErrorResult(toolName, error) {
15
+ const readable = toolName.replace(/([A-Z])/g, " $1").toLowerCase();
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: `Failed to ${readable}: ${error.message}`,
21
+ },
22
+ ],
23
+ isError: true,
24
+ };
25
+ }
26
+ export async function tfaRcaTurnTool(args, config, context) {
27
+ // The util returns the trimmed, status-discriminated contract; JSON.stringify
28
+ // drops the undefined slots, so the wrapper stays a plain serializer.
29
+ const result = await submitTfaRcaTurn(args, config, context);
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: JSON.stringify(result, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ export async function getTfaTurnResultTool(args, config) {
40
+ // Same trimmed contract as `tfaRcaTurn`, read once without submitting.
41
+ const result = await getTfaTurnResult(args, config);
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: JSON.stringify(result, null, 2),
47
+ },
48
+ ],
49
+ };
50
+ }
51
+ export async function triggerRcaReportTool(args, config) {
52
+ const glimpse = await triggerRcaReport(args, config);
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: JSON.stringify(glimpse, null, 2),
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ export async function getBuildFailureThemesTool(args, config) {
63
+ const result = await fetchBuildFailureThemes(args.buildUuid, config);
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: JSON.stringify(result, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ }
73
+ export async function listTestsInFailureThemeTool(args, config) {
74
+ const result = await fetchTestsInFailureTheme(args, config);
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: JSON.stringify(result, null, 2),
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ export default function addTfaRcaCollaborationTools(server, config) {
85
+ const tools = {};
86
+ tools.tfaRcaTurn = server.tool(TOOL_NAME, "Submit one collaborative RCA turn for a test run to the TFA agent, then poll in-call for a result. Omit threadId on the first turn for a test run — this starts a new investigation thread; every following turn on that same test MUST pass back the threadId from the previous response, since a test run should have only one active thread at a time. Returns status RESOLVED (terminal, includes root_cause/related_prs), NEEDS_INFO (fulfill the asks and submit the next turn with the same threadId), or PENDING (still working — poll it with getTfaTurnResult using the returned turnId; do NOT call tfaRcaTurn again for the same turn, that submits a duplicate).", TFA_RCA_TURN_PARAMS, async (args, context) => {
87
+ try {
88
+ const result = await tfaRcaTurnTool(args, config, context);
89
+ trackMCP(TOOL_NAME, server.server.getClientVersion(), undefined, config);
90
+ return result;
91
+ }
92
+ catch (error) {
93
+ // Domain failures carry a client-safe, group-scope-safe message.
94
+ if (error instanceof TfaRcaTurnError) {
95
+ trackMCP(TOOL_NAME, server.server.getClientVersion(), error, config);
96
+ return domainErrorResult(TOOL_NAME, error);
97
+ }
98
+ return handleMCPError(TOOL_NAME, server, config, error);
99
+ }
100
+ });
101
+ tools.getTfaTurnResult = server.tool(GET_RESULT_TOOL_NAME, "Read a previously submitted RCA turn's status once, given the testRunId and the turnId a PENDING tfaRcaTurn response returned — this never resubmits or duplicates the turn. Returns PENDING again if the TFA agent is still working, or the same RESOLVED/NEEDS_INFO contract as tfaRcaTurn once it finishes. On NEEDS_INFO, gather the requested evidence and continue via tfaRcaTurn with the same threadId; RESOLVED is terminal for this test — do not call this tool or tfaRcaTurn again for it.", GET_TFA_TURN_RESULT_PARAMS, async (args) => {
102
+ try {
103
+ const result = await getTfaTurnResultTool(args, config);
104
+ trackMCP(GET_RESULT_TOOL_NAME, server.server.getClientVersion(), undefined, config);
105
+ return result;
106
+ }
107
+ catch (error) {
108
+ // Domain failures carry a client-safe, group-scope-safe message.
109
+ if (error instanceof TfaRcaTurnError) {
110
+ trackMCP(GET_RESULT_TOOL_NAME, server.server.getClientVersion(), error, config);
111
+ return domainErrorResult(GET_RESULT_TOOL_NAME, error);
112
+ }
113
+ return handleMCPError(GET_RESULT_TOOL_NAME, server, config, error);
114
+ }
115
+ });
116
+ tools.triggerRcaReport = server.tool(TRIGGER_TOOL_NAME, "Trigger (or read, if one already exists) a build's Release Readiness report, returning a trimmed verdict glimpse and a Test Observability UI link — never the raw report body. Without force, a completed report is read as-is at no extra analysis cost, so it's safe to call this repeatedly just to check status. Pass force:true only to force a fresh re-analysis of an already-completed report (e.g. after new evidence changed the picture) — do NOT set force on every call, since that discards the cached report and always pays the full re-analysis cost.", TRIGGER_RCA_REPORT_PARAMS, async (args) => {
117
+ try {
118
+ const result = await triggerRcaReportTool(args, config);
119
+ trackMCP(TRIGGER_TOOL_NAME, server.server.getClientVersion(), undefined, config);
120
+ return result;
121
+ }
122
+ catch (error) {
123
+ // Domain failures carry a client-safe, group-scope-safe message.
124
+ if (error instanceof TriggerRcaReportError) {
125
+ trackMCP(TRIGGER_TOOL_NAME, server.server.getClientVersion(), error, config);
126
+ return domainErrorResult(TRIGGER_TOOL_NAME, error);
127
+ }
128
+ return handleMCPError(TRIGGER_TOOL_NAME, server, config, error);
129
+ }
130
+ });
131
+ tools.getBuildFailureThemes = server.tool(GET_BUILD_FAILURE_THEMES_TOOL_NAME, "Get a build's server-computed failure-theme clusters (buildThemes + buildWorkflows), the preferred grouping source for representative/sibling fan-out — call this once per build, not per test. Triggers server-side computation if nothing has ever run for this build, and polls in-call up to its own budget for the result to finish; never blocks indefinitely. ready:false means either the computation is still running past the poll budget or the trigger itself failed (status:'trigger-unavailable') — either way, fall back to client-side clustering rather than waiting longer or retrying this call.", GET_BUILD_FAILURE_THEMES_PARAMS, async (args) => {
132
+ try {
133
+ const result = await getBuildFailureThemesTool(args, config);
134
+ trackMCP(GET_BUILD_FAILURE_THEMES_TOOL_NAME, server.server.getClientVersion(), undefined, config);
135
+ return result;
136
+ }
137
+ catch (error) {
138
+ // Domain failures carry a client-safe, group-scope-safe message.
139
+ if (error instanceof BuildFailureThemesError) {
140
+ trackMCP(GET_BUILD_FAILURE_THEMES_TOOL_NAME, server.server.getClientVersion(), error, config);
141
+ return domainErrorResult(GET_BUILD_FAILURE_THEMES_TOOL_NAME, error);
142
+ }
143
+ return handleMCPError(GET_BUILD_FAILURE_THEMES_TOOL_NAME, server, config, error);
144
+ }
145
+ });
146
+ tools.listTestsInFailureTheme = server.tool(LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME, "List the test runs belonging to one failure theme or workflow from a prior getBuildFailureThemes call, filtered by themeId or workflowId. Results are paginated — pass the previous response's nextCursor back as cursor and keep calling until no nextCursor is returned; do not assume a single page covers all members. This is the representative/sibling grouping source: every test in a theme's list becomes either the cluster's representative or a pre-seeded sibling confirm.", LIST_TESTS_IN_FAILURE_THEME_PARAMS, async (args) => {
147
+ try {
148
+ const result = await listTestsInFailureThemeTool(args, config);
149
+ trackMCP(LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME, server.server.getClientVersion(), undefined, config);
150
+ return result;
151
+ }
152
+ catch (error) {
153
+ // Domain failures carry a client-safe, group-scope-safe message.
154
+ if (error instanceof BuildFailureThemesError) {
155
+ trackMCP(LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME, server.server.getClientVersion(), error, config);
156
+ return domainErrorResult(LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME, error);
157
+ }
158
+ return handleMCPError(LIST_TESTS_IN_FAILURE_THEME_TOOL_NAME, server, config, error);
159
+ }
160
+ });
161
+ return tools;
162
+ }
@@ -0,0 +1,61 @@
1
+ import { BrowserStackConfig } from "../../lib/types.js";
2
+ export declare class BuildFailureThemesError extends Error {
3
+ }
4
+ export interface BuildFailureTheme {
5
+ themeId: string;
6
+ buildFailureThemeId: number;
7
+ themeData: {
8
+ name: string;
9
+ description: string;
10
+ };
11
+ affectedWorkflows: unknown[];
12
+ affectedWorkflowCount?: number;
13
+ testRunCount?: number;
14
+ testPercentage?: number;
15
+ }
16
+ export interface BuildFailureWorkflow {
17
+ workflowIdentifier: string;
18
+ buildFailureWorkflowId: number;
19
+ workflowData: {
20
+ name: string;
21
+ description: string;
22
+ };
23
+ testRunCount?: number;
24
+ testPercentage?: number;
25
+ themesIdentified?: unknown[];
26
+ themeIdentifiedCount?: number;
27
+ }
28
+ export interface BuildFailureThemesResult {
29
+ ready: boolean;
30
+ /** Last observed `buildThemeWorkflow.status`, or "PENDING" if budget spent with no status yet. */
31
+ status: string;
32
+ buildId?: string;
33
+ buildThemes?: BuildFailureTheme[];
34
+ buildWorkflows?: BuildFailureWorkflow[];
35
+ stats?: {
36
+ totalThemes: number;
37
+ newThemes: number;
38
+ existingThemes: number;
39
+ };
40
+ }
41
+ export interface TestInFailureTheme {
42
+ testRunId: string | number | undefined;
43
+ title?: string;
44
+ status?: string;
45
+ raw: unknown;
46
+ }
47
+ export interface ListTestsInFailureThemeArgs {
48
+ buildUuid: string;
49
+ themeId?: number;
50
+ workflowId?: number;
51
+ limit?: number;
52
+ cursor?: string;
53
+ }
54
+ export interface ListTestsInFailureThemeResult {
55
+ tests: TestInFailureTheme[];
56
+ /** Cursor for the next page, if any. */
57
+ nextCursor?: string;
58
+ }
59
+ export declare function fetchBuildFailureThemes(buildUuid: string, config: BrowserStackConfig): Promise<BuildFailureThemesResult>;
60
+ /** Paginated test-run membership for one failure theme or workflow. */
61
+ export declare function fetchTestsInFailureTheme(args: ListTestsInFailureThemeArgs, config: BrowserStackConfig): Promise<ListTestsInFailureThemeResult>;
@@ -0,0 +1,133 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { AI_FAILURES_FLAT_PATH, AI_FAILURES_PATH, BUILD_THEMES_FAILURE_STATUSES, BUILD_THEMES_POLL_INTERVAL_MS, BUILD_THEMES_POLL_MAX_WAIT_MS, BUILD_THEMES_SUCCESS_STATUS, getO11yBaseUrl, } from "./constants.js";
3
+ import { buildAuthHeader } from "./turn-result.js";
4
+ export class BuildFailureThemesError extends Error {
5
+ }
6
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+ function authHeaders(config) {
8
+ return {
9
+ "Content-Type": "application/json",
10
+ Authorization: buildAuthHeader(config),
11
+ };
12
+ }
13
+ // Normalize a response body to a plain object — a malformed 200 is treated as "no data".
14
+ function asObject(data) {
15
+ return typeof data === "object" && data !== null && !Array.isArray(data)
16
+ ? data
17
+ : {};
18
+ }
19
+ function failuresUrl(buildUuid) {
20
+ return (getO11yBaseUrl() +
21
+ AI_FAILURES_PATH.replace("{buildUuid}", encodeURIComponent(buildUuid)));
22
+ }
23
+ async function fetchFailuresOnce(buildUuid, config) {
24
+ return apiClient.get({
25
+ url: failuresUrl(buildUuid),
26
+ headers: authHeaders(config),
27
+ raise_error: false,
28
+ });
29
+ }
30
+ // Same URL as the GET read, different verb — kicks off build-level theme computation.
31
+ async function triggerFailuresOnce(buildUuid, config) {
32
+ return apiClient.post({
33
+ url: failuresUrl(buildUuid),
34
+ headers: authHeaders(config),
35
+ body: {},
36
+ raise_error: false,
37
+ });
38
+ }
39
+ // Fetch a build's server-computed failure-theme clusters, triggering computation
40
+ // if nothing has run yet. Triggers at most once per call, then polls until
41
+ // SUCCESS or BUILD_THEMES_POLL_MAX_WAIT_MS is spent — never blocks past that.
42
+ export async function fetchBuildFailureThemes(buildUuid, config) {
43
+ const startTime = Date.now();
44
+ let lastStatus;
45
+ let triggered = false;
46
+ /** Attempts the one-time trigger POST; returns whether it succeeded. */
47
+ const triggerOnce = async () => {
48
+ triggered = true;
49
+ const triggerResponse = await triggerFailuresOnce(buildUuid, config);
50
+ return triggerResponse.ok;
51
+ };
52
+ while (true) {
53
+ const response = await fetchFailuresOnce(buildUuid, config);
54
+ if (response.ok) {
55
+ const data = asObject(response.data);
56
+ const status = data.buildThemeWorkflow?.status;
57
+ if (status === BUILD_THEMES_SUCCESS_STATUS) {
58
+ return {
59
+ ready: true,
60
+ status,
61
+ buildId: data.buildId,
62
+ buildThemes: data.buildThemes ?? [],
63
+ buildWorkflows: data.buildWorkflows ?? [],
64
+ stats: data.stats,
65
+ };
66
+ }
67
+ if (status && BUILD_THEMES_FAILURE_STATUSES.includes(status)) {
68
+ if (triggered) {
69
+ // Already retried once and it failed again — a real failure, not
70
+ // async lag. Nothing left to do but report it.
71
+ return { ready: false, status };
72
+ }
73
+ if (!(await triggerOnce())) {
74
+ return { ready: false, status: "trigger-unavailable" };
75
+ }
76
+ }
77
+ else {
78
+ // status undefined (never computed) — trigger once. An in-progress
79
+ // value (PENDING/PROCESSING) just keeps polling either way.
80
+ if (!status && !triggered && !(await triggerOnce())) {
81
+ return { ready: false, status: "trigger-unavailable" };
82
+ }
83
+ lastStatus = status ?? lastStatus;
84
+ }
85
+ }
86
+ else if (response.status === 404) {
87
+ if (!triggered && !(await triggerOnce())) {
88
+ return { ready: false, status: "trigger-unavailable" };
89
+ }
90
+ }
91
+ else {
92
+ throw new BuildFailureThemesError(`failed to fetch build failure themes (status ${response.status})`);
93
+ }
94
+ if (Date.now() - startTime >= BUILD_THEMES_POLL_MAX_WAIT_MS) {
95
+ return { ready: false, status: lastStatus ?? "PENDING" };
96
+ }
97
+ await delay(BUILD_THEMES_POLL_INTERVAL_MS);
98
+ }
99
+ }
100
+ /** Paginated test-run membership for one failure theme or workflow. */
101
+ export async function fetchTestsInFailureTheme(args, config) {
102
+ const params = {
103
+ limit: args.limit ?? 50,
104
+ };
105
+ if (args.themeId !== undefined)
106
+ params.buildFailureThemeId = args.themeId;
107
+ if (args.workflowId !== undefined)
108
+ params.buildFailureWorkflowId = args.workflowId;
109
+ if (args.cursor)
110
+ params.searchAfter = args.cursor;
111
+ const url = getO11yBaseUrl() +
112
+ AI_FAILURES_FLAT_PATH.replace("{buildUuid}", encodeURIComponent(args.buildUuid));
113
+ const response = await apiClient.get({
114
+ url,
115
+ headers: authHeaders(config),
116
+ params,
117
+ raise_error: false,
118
+ });
119
+ if (!response.ok) {
120
+ throw new BuildFailureThemesError(`failed to list tests in failure theme (status ${response.status})`);
121
+ }
122
+ const data = asObject(response.data);
123
+ const testRuns = data.testRuns ?? [];
124
+ return {
125
+ tests: testRuns.map((t) => ({
126
+ testRunId: t?.details?.id ?? t?.id,
127
+ title: t?.title,
128
+ status: t?.details?.status,
129
+ raw: t,
130
+ })),
131
+ nextCursor: data.nextCursor ?? data.searchAfter ?? data.next_search_after,
132
+ };
133
+ }