Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lib/tm-base-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export interface ResolvedBaseUrls {

export function resolveTMBaseUrls(): ResolvedBaseUrls {
const raw = process.env[TM_BASE_URLS_ENV];
if (!raw || !raw.trim()) return { urls: [...TM_BASE_URLS], source: "built-in" };
if (!raw || !raw.trim())
return { urls: [...TM_BASE_URLS], source: "built-in" };

const urls = raw
.split(",")
Expand Down
7 changes: 5 additions & 2 deletions src/tools/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getBrowserStackAuth } from "../lib/get-auth.js";
import { BrowserStackConfig } from "../lib/types.js";
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
import logger from "../logger.js";
import { NEEDS_A11Y_CONFIG_ID, NEEDS_A11Y_SCAN_ID } from "./tool-handoff.js";

interface AuthCredentials {
username: string;
Expand Down Expand Up @@ -575,7 +576,8 @@ export default function addAccessibilityTools(

tools.getAccessibilityAuthConfig = server.tool(
"getAccessibilityAuthConfig",
"Retrieve an existing authentication configuration by ID.",
"Retrieve an existing authentication configuration by ID." +
NEEDS_A11Y_CONFIG_ID,
{
configId: z.number().describe("ID of the auth configuration to retrieve"),
},
Expand All @@ -593,7 +595,8 @@ export default function addAccessibilityTools(

tools.fetchAccessibilityIssues = server.tool(
"fetchAccessibilityIssues",
"Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results.",
"Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results." +
NEEDS_A11Y_SCAN_ID,
{
scanId: z
.string()
Expand Down
3 changes: 2 additions & 1 deletion src/tools/ask-browserstack/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export function isEnabled(): boolean {
*/
export function allowRemoteRelay(): boolean {
return (
(process.env.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY || "").toLowerCase() === "true"
(process.env.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY || "").toLowerCase() ===
"true"
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/tools/get-failure-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { trackMCP } from "../lib/instrumentation.js";
import { BrowserStackConfig } from "../lib/types.js";
import { NEEDS_SESSION_ID } from "./tool-handoff.js";

import {
retrieveNetworkFailures,
Expand Down Expand Up @@ -171,7 +172,8 @@ export default function registerGetFailureLogs(

tools.getFailureLogs = server.tool(
"getFailureLogs",
"Fetch various types of logs from a BrowserStack session. Supports both automate and app-automate sessions.",
"Fetch various types of logs from a BrowserStack session. Supports both automate and app-automate sessions." +
NEEDS_SESSION_ID,
{
sessionType: z
.enum([SessionType.Automate, SessionType.AppAutomate])
Expand Down
7 changes: 5 additions & 2 deletions src/tools/rca-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TestStatus } from "./rca-agent-utils/types.js";
import { handleMCPError } from "../lib/utils.js";
import { trackMCP } from "../index.js";
import { BuildIdArgs } from "./rca-agent-utils/types.js";
import { NEEDS_BUILD_ID, NEEDS_TEST_IDS } from "./tool-handoff.js";
import {
FETCH_RCA_PARAMS,
GET_BUILD_ID_PARAMS,
Expand Down Expand Up @@ -187,7 +188,8 @@ export default function addRCATools(

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 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." +
NEEDS_TEST_IDS,
FETCH_RCA_PARAMS,
{
title: "Fetch Root Cause Analysis",
Expand Down Expand Up @@ -265,7 +267,8 @@ export default function addRCATools(

tools.listTestIds = server.tool(
"listTestIds",
"List test IDs from a BrowserStack Automate build, optionally filtered by status",
"List test IDs from a BrowserStack Automate build, optionally filtered by status" +
NEEDS_BUILD_ID,
LIST_TEST_IDS_PARAMS,
{
title: "List Test IDs",
Expand Down
7 changes: 5 additions & 2 deletions src/tools/selfheal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import logger from "../logger.js";
import { trackMCP } from "../lib/instrumentation.js";
import { BrowserStackConfig } from "../lib/types.js";
import { NEEDS_SESSION_ID } from "./tool-handoff.js";

// Local helper: returns the server-configured BrowserStack credentials, or
// null when either is missing. Lives here because the self-heal tools need
Expand Down Expand Up @@ -581,7 +582,8 @@ export default function addSelfHealTools(
"the run. Provide exactly one of `sessionId` (single Automate / " +
"App-Automate session) or `buildUuid` (full self-healing report for a " +
"build). Pass the returned locator pairs to `prepareSelfHealingPlan` " +
"to plan edits.",
"to plan edits." +
NEEDS_SESSION_ID,
{
sessionId: z
.string()
Expand Down Expand Up @@ -671,7 +673,8 @@ export default function addSelfHealTools(
"[...]}`, the raw report `{healing_logs: [...]}` (with " +
"`healed_selectors` aliasing `locators`), and snake_case keys " +
"(`session_id`, `original_locator`, `healed_locator`, " +
"`healing_thought`).",
"`healing_thought`)." +
NEEDS_SESSION_ID,
{
sessions: sessionsFieldSchema.describe(
"Sessions to plan edits for. See tool description for accepted shapes.",
Expand Down
48 changes: 33 additions & 15 deletions src/tools/testmanagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {

import { BrowserStackConfig } from "../lib/types.js";
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID } from "./tool-handoff.js";

//TODO: Moving the traceMCP and catch block to the parent(server) function

Expand Down Expand Up @@ -728,7 +729,8 @@ export default function addTestManagementTools(

tools.createProjectOrFolder = server.tool(
"createProjectOrFolder",
"Create a project and/or folder in BrowserStack Test Management.",
"Create a project and/or folder in BrowserStack Test Management." +
NEEDS_PROJECT_ID,
CreateProjFoldSchema.shape,
{
title: "Create Project or Folder",
Expand All @@ -742,7 +744,8 @@ export default function addTestManagementTools(

tools.createTestCase = server.tool(
"createTestCase",
"Use this tool to create a test case in BrowserStack Test Management.",
"Use this tool to create a test case in BrowserStack Test Management." +
NEEDS_PROJECT_ID,
CreateTestCaseSchema.shape,
{
title: "Create Test Case",
Expand All @@ -756,7 +759,8 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID,
UpdateTestCaseSchema.shape,
{
title: "Update Test Case",
Expand All @@ -770,7 +774,8 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID,
ListTestCasesSchema.shape,
{
title: "List Test Cases",
Expand All @@ -784,7 +789,8 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID,
ListFoldersSchema.shape,
{
title: "List Folders",
Expand Down Expand Up @@ -812,7 +818,7 @@ export default function addTestManagementTools(

tools.createTestRun = server.tool(
"createTestRun",
"Create a test run in BrowserStack Test Management.",
"Create a test run in BrowserStack Test Management." + NEEDS_PROJECT_ID,
CreateTestRunSchema.shape,
{
title: "Create Test Run",
Expand All @@ -826,7 +832,8 @@ export default function addTestManagementTools(

tools.listTestRuns = server.tool(
"listTestRuns",
"List test runs in a project with optional filters (date ranges, assignee, state, etc.)",
"List test runs in a project with optional filters (date ranges, assignee, state, etc.)" +
NEEDS_PROJECT_ID,
ListTestRunsSchema.shape,
{
title: "List Test Runs",
Expand All @@ -840,7 +847,8 @@ export default function addTestManagementTools(

tools.updateTestRun = server.tool(
"updateTestRun",
"Update a test run's metadata and/or add test cases to it.",
"Update a test run's metadata and/or add test cases to it." +
NEEDS_PROJECT_ID,
UpdateTestRunSchema.shape,
{
title: "Update Test Run",
Expand All @@ -854,7 +862,8 @@ export default function addTestManagementTools(

tools.addTestResult = server.tool(
"addTestResult",
"Add a test result to a specific test run via BrowserStack Test Management API.",
"Add a test result to a specific test run via BrowserStack Test Management API." +
NEEDS_PROJECT_ID,
AddTestResultSchema.shape,
{
title: "Add Test Result",
Expand All @@ -868,7 +877,8 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID,
UploadFileSchema.shape,
{
title: "Upload Product Requirement File",
Expand Down Expand Up @@ -897,7 +907,8 @@ export default function addTestManagementTools(

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.",
"Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent." +
NEEDS_PROJECT_ID,
CreateLCAStepsSchema.shape,
{
title: "Create LCA Steps",
Expand All @@ -911,7 +922,8 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID,
ListTestPlansSchema.shape,
{
title: "List Test Plans",
Expand All @@ -925,7 +937,9 @@ export default function addTestManagementTools(

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.",
"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." +
NEEDS_PROJECT_ID +
NEEDS_TEST_PLAN_ID,
GetTestPlanSchema.shape,
{
title: "Get Test Plan",
Expand All @@ -939,7 +953,9 @@ export default function addTestManagementTools(

tools.listSubTestPlans = server.tool(
"listSubTestPlans",
"List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination.",
"List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
NEEDS_PROJECT_ID +
NEEDS_TEST_PLAN_ID,
ListSubTestPlansSchema.shape,
{
title: "List Sub Test Plans",
Expand All @@ -953,7 +969,9 @@ export default function addTestManagementTools(

tools.getSubTestPlan = server.tool(
"getSubTestPlan",
"Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs.",
"Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
NEEDS_PROJECT_ID +
NEEDS_TEST_PLAN_ID,
GetSubTestPlanSchema.shape,
{
title: "Get Sub Test Plan",
Expand Down
65 changes: 65 additions & 0 deletions src/tools/tool-handoff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Precondition sentences appended to a tool's description.
*
* WHY THESE EXIST. Nothing routes an MCP call: the client's model picks a tool from the
* descriptions alone. A tool that needs an identifier the caller does not have is a dead
* end — the model either asks the user for something they also do not know, or gives up.
* These sentences turn that dead end into a HANDOFF by naming, in the description itself,
* where the missing identifier comes from.
*
* Point at a sibling tool whenever one can produce the id — it is faster and more
* predictable than an agent. Point at `askBrowserstackAI` only when NO tool here can.
*
* The one that matters most: 15 of the 17 Test Management tools require a project
* identifier and NONE of them accepts its absence, yet no tool in this server lists
* projects. "List my projects" is the first step of nearly every Test Management journey
* and it was unserved, which is exactly why that request did not reach any tool unless a
* user named one explicitly.
*
* Keep these as shared constants, not per-tool prose: the wording is a routing signal, and
* twenty hand-written variants drift into twenty different signals.
*/

/** No tool lists projects, so this genuinely has to go to the agent. */
export const NEEDS_PROJECT_ID =
" Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
'have one, call askBrowserstackAI with product "tm" and ask which projects exist, then ' +
"retry this tool with the identifier it returns.";

/** A sibling tool can produce the id — prefer it over the agent. */
export function needsIdFrom(idLabel: string, sourceTool: string): string {
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
}

/** A test plan id (TP-*) comes from listTestPlans. */
export const NEEDS_TEST_PLAN_ID = needsIdFrom(
"a test plan identifier (TP-*)",
"listTestPlans",
);

/** A build id comes from either build-lookup tool. */
export const NEEDS_BUILD_ID = needsIdFrom(
"a BrowserStack build id",
"getBuildId or listBuildId",
);

/** Session ids are not listable by any tool here. */
export const NEEDS_SESSION_ID =
" Requires a session id, which no tool here lists. If you only know the build, call " +
"getBuildId or listBuildId; if you have neither, call askBrowserstackAI with product " +
'"tra" and describe the run you mean.';

/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
export const NEEDS_A11Y_SCAN_ID =
" Requires the ids of a completed scan. They are returned by startAccessibilityScan; " +
'for a scan run earlier, call askBrowserstackAI with product "a11y" to locate it, since ' +
"no tool here lists past scans.";

/** Auth-config ids are not listable by any tool here. */
export const NEEDS_A11Y_CONFIG_ID =
" Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
"existing configurations, so if you do not have the id, call askBrowserstackAI with " +
'product "a11y".';

/** Test ids come from listTestIds, which itself needs a build id. */
export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");