@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.
- package/README.md +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +23 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/tm-base-url.js +7 -1
- package/dist/tools/accessibility.js +30 -0
- package/dist/tools/appautomate.js +20 -2
- package/dist/tools/applive.js +6 -0
- package/dist/tools/automate.js +6 -0
- package/dist/tools/bstack-sdk.js +7 -1
- package/dist/tools/build-insights.js +16 -3
- package/dist/tools/get-failure-logs.js +6 -0
- package/dist/tools/live.js +7 -1
- package/dist/tools/observability.js +6 -0
- package/dist/tools/percy-sdk.js +49 -7
- package/dist/tools/rca-agent-utils/constants.d.ts +2 -0
- package/dist/tools/rca-agent-utils/constants.js +8 -0
- package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +3 -2
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +94 -25
- package/dist/tools/rca-agent-utils/types.d.ts +10 -0
- package/dist/tools/rca-agent.d.ts +1 -0
- package/dist/tools/rca-agent.js +30 -6
- package/dist/tools/selfheal.js +12 -0
- package/dist/tools/testmanagement.js +119 -17
- package/dist/tools/tfa-rca-collaboration.d.ts +15 -0
- package/dist/tools/tfa-rca-collaboration.js +162 -0
- package/dist/tools/tfa-rca-utils/build-failure-themes.d.ts +61 -0
- package/dist/tools/tfa-rca-utils/build-failure-themes.js +133 -0
- package/dist/tools/tfa-rca-utils/constants.d.ts +67 -0
- package/dist/tools/tfa-rca-utils/constants.js +103 -0
- package/dist/tools/tfa-rca-utils/submit-turn.d.ts +29 -0
- package/dist/tools/tfa-rca-utils/submit-turn.js +188 -0
- package/dist/tools/tfa-rca-utils/trigger-report.d.ts +22 -0
- package/dist/tools/tfa-rca-utils/trigger-report.js +61 -0
- package/dist/tools/tfa-rca-utils/turn-result.d.ts +14 -0
- package/dist/tools/tfa-rca-utils/turn-result.js +131 -0
- package/dist/tools/tfa-rca-utils/types.d.ts +61 -0
- package/dist/tools/tfa-rca-utils/types.js +9 -0
- 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>;
|
package/dist/tools/rca-agent.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
|
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);
|
package/dist/tools/selfheal.js
CHANGED
|
@@ -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,
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
tools.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
tools.
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
}
|