@browserstack/mcp-server 1.2.15-beta.1 → 1.2.16-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.
@@ -1,7 +1,6 @@
1
1
  import { trackMCP } from "../index.js";
2
2
  import { fetchPercyChanges } from "./review-agent.js";
3
3
  import { addListTestFiles } from "./list-test-files.js";
4
- import { runPercyScan } from "./run-percy-scan.js";
5
4
  import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js";
6
5
  import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js";
7
6
  import { approveOrDeclinePercyBuild } from "./review-agent-utils/percy-approve-reject.js";
@@ -9,7 +8,10 @@ import { setUpPercyHandler, simulatePercyChangeHandler, } from "./sdk-utils/hand
9
8
  import { z } from "zod";
10
9
  import { SETUP_PERCY_DESCRIPTION, LIST_TEST_FILES_DESCRIPTION, PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, SIMULATE_PERCY_CHANGE_DESCRIPTION, } from "./sdk-utils/common/constants.js";
11
10
  import { UpdateTestFileWithInstructionsParams } from "./percy-snapshot-utils/constants.js";
12
- import { RunPercyScanParamsShape, FetchPercyChangesParamsShape, ManagePercyBuildApprovalParamsShape, } from "./sdk-utils/common/schema.js";
11
+ import {
12
+ // PMAA-100: kept commented so the registration block below is easy to restore once the proper fix lands.
13
+ // RunPercyScanParamsShape,
14
+ FetchPercyChangesParamsShape, ManagePercyBuildApprovalParamsShape, } from "./sdk-utils/common/schema.js";
13
15
  import { handleMCPError } from "../lib/utils.js";
14
16
  export function registerPercyTools(server, config) {
15
17
  const tools = {};
@@ -66,15 +68,22 @@ export function registerPercyTools(server, config) {
66
68
  return handleMCPError("listTestFiles", server, config, error);
67
69
  }
68
70
  });
69
- tools.runPercyScan = server.tool("runPercyScan", "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool", RunPercyScanParamsShape, async (args) => {
70
- try {
71
- trackMCP("runPercyScan", server.server.getClientVersion(), config);
72
- return runPercyScan(args, config);
73
- }
74
- catch (error) {
75
- return handleMCPError("runPercyScan", server, config, error);
76
- }
77
- });
71
+ // PMAA-100: runPercyScan temporarily disabled fetched Percy token was being
72
+ // returned in plaintext within tool output (see HackerOne #3576387). Re-enable
73
+ // once the token is replaced with a placeholder in run-percy-scan.ts.
74
+ // tools.runPercyScan = server.tool(
75
+ // "runPercyScan",
76
+ // "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool",
77
+ // RunPercyScanParamsShape,
78
+ // async (args) => {
79
+ // try {
80
+ // trackMCP("runPercyScan", server.server.getClientVersion()!, config);
81
+ // return runPercyScan(args, config);
82
+ // } catch (error) {
83
+ // return handleMCPError("runPercyScan", server, config, error);
84
+ // }
85
+ // },
86
+ // );
78
87
  tools.fetchPercyChanges = server.tool("fetchPercyChanges", "Retrieves and summarizes all visual changes detected by Percy AI between the latest and previous builds, helping quickly review what has changed in your project.", FetchPercyChangesParamsShape, async (args) => {
79
88
  try {
80
89
  trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { BrowserStackConfig } from "../../lib/types.js";
4
+ /**
5
+ * Schema for fetching a single test plan by identifier, including its linked test runs.
6
+ */
7
+ export declare const GetTestPlanSchema: z.ZodObject<{
8
+ project_identifier: z.ZodString;
9
+ test_plan_identifier: z.ZodString;
10
+ }, z.core.$strip>;
11
+ export type GetTestPlanArgs = z.infer<typeof GetTestPlanSchema>;
12
+ /**
13
+ * Fetches a test plan by identifier and its linked test runs, returning a unified view
14
+ * suitable for generating documentation (metadata + linked runs + status summary + case count).
15
+ */
16
+ export declare function getTestPlan(args: GetTestPlanArgs, config: BrowserStackConfig): Promise<CallToolResult>;
@@ -0,0 +1,99 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { z } from "zod";
3
+ import { formatAxiosError } from "../../lib/error.js";
4
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
5
+ import { getTMBaseURL } from "../../lib/tm-base-url.js";
6
+ /**
7
+ * Schema for fetching a single test plan by identifier, including its linked test runs.
8
+ */
9
+ export const GetTestPlanSchema = z.object({
10
+ project_identifier: z
11
+ .string()
12
+ .describe("Identifier of the project (starts with PR- followed by a number)."),
13
+ test_plan_identifier: z
14
+ .string()
15
+ .describe("Identifier of the test plan (starts with TP- followed by a number)."),
16
+ });
17
+ /**
18
+ * Fetches a test plan by identifier and its linked test runs, returning a unified view
19
+ * suitable for generating documentation (metadata + linked runs + status summary + case count).
20
+ */
21
+ export async function getTestPlan(args, config) {
22
+ try {
23
+ const tmBaseUrl = await getTMBaseURL(config);
24
+ const projectId = encodeURIComponent(args.project_identifier);
25
+ const planId = encodeURIComponent(args.test_plan_identifier);
26
+ const authString = getBrowserStackAuth(config);
27
+ const [username, password] = authString.split(":");
28
+ const authHeader = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
29
+ const planResp = await apiClient.get({
30
+ url: `${tmBaseUrl}/api/v2/projects/${projectId}/test-plans/${planId}`,
31
+ headers: { Authorization: authHeader },
32
+ });
33
+ if (!planResp.data?.success) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: `Failed to fetch test plan: ${JSON.stringify(planResp.data)}`,
39
+ },
40
+ ],
41
+ isError: true,
42
+ };
43
+ }
44
+ const plan = planResp.data.test_plan;
45
+ const runsResp = await apiClient.get({
46
+ url: `${tmBaseUrl}/api/v2/projects/${projectId}/test-plans/${planId}/test-runs`,
47
+ headers: { Authorization: authHeader },
48
+ });
49
+ const runs = runsResp.data?.success
50
+ ? (runsResp.data.test_runs ?? [])
51
+ : [];
52
+ const statusSummary = {};
53
+ let totalCases = 0;
54
+ for (const run of runs) {
55
+ statusSummary[run.run_state] = (statusSummary[run.run_state] ?? 0) + 1;
56
+ totalCases += run.test_cases_count ?? 0;
57
+ }
58
+ const header = [
59
+ `Test Plan ${plan.identifier}: ${plan.name}`,
60
+ `Status: ${plan.active_state}`,
61
+ plan.description ? `Description: ${plan.description}` : null,
62
+ plan.start_date || plan.end_date
63
+ ? `Dates: ${plan.start_date ?? "—"} → ${plan.end_date ?? "—"}`
64
+ : null,
65
+ `Linked runs: ${runs.length} (plan counts — active ${plan.test_runs_count?.active ?? 0} / closed ${plan.test_runs_count?.closed ?? 0})`,
66
+ `Total test cases across runs: ${totalCases}`,
67
+ Object.keys(statusSummary).length > 0
68
+ ? `Run-state breakdown: ${Object.entries(statusSummary)
69
+ .map(([s, n]) => `${s}=${n}`)
70
+ .join(", ")}`
71
+ : null,
72
+ ]
73
+ .filter(Boolean)
74
+ .join("\n");
75
+ const runsBlock = runs.length
76
+ ? "\n\nLinked test runs:\n" +
77
+ runs
78
+ .map((r) => `• ${r.identifier}: ${r.name} [${r.run_state}] — ${r.test_cases_count} case(s)${r.assignee ? ` (assignee: ${r.assignee})` : ""}`)
79
+ .join("\n")
80
+ : "\n\nNo test runs linked to this plan.";
81
+ return {
82
+ content: [
83
+ { type: "text", text: header + runsBlock },
84
+ {
85
+ type: "text",
86
+ text: JSON.stringify({
87
+ test_plan: plan,
88
+ linked_test_runs: runs,
89
+ status_summary: statusSummary,
90
+ total_test_cases: totalCases,
91
+ }, null, 2),
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ catch (err) {
97
+ return formatAxiosError(err, "Failed to fetch test plan");
98
+ }
99
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { BrowserStackConfig } from "../../lib/types.js";
4
+ /**
5
+ * Schema for listing folders in a BrowserStack Test Management project.
6
+ */
7
+ export declare const ListFoldersSchema: z.ZodObject<{
8
+ project_identifier: z.ZodString;
9
+ parent_id: z.ZodOptional<z.ZodNumber>;
10
+ p: z.ZodOptional<z.ZodNumber>;
11
+ }, z.core.$strip>;
12
+ export type ListFoldersArgs = z.infer<typeof ListFoldersSchema>;
13
+ /**
14
+ * Lists folders (or sub-folders) for a project in BrowserStack Test Management.
15
+ */
16
+ export declare function listFolders(args: ListFoldersArgs, config: BrowserStackConfig): Promise<CallToolResult>;
@@ -0,0 +1,77 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { z } from "zod";
3
+ import { formatAxiosError } from "../../lib/error.js";
4
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
5
+ import { getTMBaseURL } from "../../lib/tm-base-url.js";
6
+ /**
7
+ * Schema for listing folders in a BrowserStack Test Management project.
8
+ */
9
+ export const ListFoldersSchema = z.object({
10
+ project_identifier: z
11
+ .string()
12
+ .describe("Identifier of the project to fetch folders from (starts with PR- followed by a number)."),
13
+ parent_id: z
14
+ .number()
15
+ .optional()
16
+ .describe("If provided, list sub-folders under this parent folder id. If omitted, lists top-level folders."),
17
+ p: z.number().optional().describe("Page number."),
18
+ });
19
+ /**
20
+ * Lists folders (or sub-folders) for a project in BrowserStack Test Management.
21
+ */
22
+ export async function listFolders(args, config) {
23
+ try {
24
+ const params = new URLSearchParams();
25
+ if (args.p !== undefined)
26
+ params.append("p", args.p.toString());
27
+ const tmBaseUrl = await getTMBaseURL(config);
28
+ const projectId = encodeURIComponent(args.project_identifier);
29
+ // GET /api/v2/projects/{projectIdentifier}/folders
30
+ // or /api/v2/projects/{projectIdentifier}/folders/{parent_id}/sub-folders
31
+ const path = args.parent_id !== undefined
32
+ ? `folders/${args.parent_id}/sub-folders`
33
+ : `folders`;
34
+ const url = `${tmBaseUrl}/api/v2/projects/${projectId}/${path}?${params.toString()}`;
35
+ const authString = getBrowserStackAuth(config);
36
+ const [username, password] = authString.split(":");
37
+ const resp = await apiClient.get({
38
+ url,
39
+ headers: {
40
+ Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
41
+ },
42
+ });
43
+ const folders = resp.data?.folders ?? [];
44
+ const info = resp.data?.info ?? {};
45
+ const count = info.count ?? folders.length;
46
+ if (folders.length === 0) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: args.parent_id !== undefined
52
+ ? `No sub-folders found under folder ${args.parent_id} in project ${args.project_identifier}.`
53
+ : `No folders found in project ${args.project_identifier}.`,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ const summary = folders
59
+ .map((f) => `• [id=${f.id}] ${f.name} — ${f.cases_count} case(s), ${f.sub_folders_count} sub-folder(s)${f.parent_id ? ` (parent=${f.parent_id})` : ""}`)
60
+ .join("\n");
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: `Found ${count} folder(s) in project ${args.project_identifier}:\n\n${summary}`,
66
+ },
67
+ {
68
+ type: "text",
69
+ text: JSON.stringify(folders, null, 2),
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ catch (err) {
75
+ return formatAxiosError(err, "Failed to list folders");
76
+ }
77
+ }
@@ -13,7 +13,7 @@ export const ListTestCasesSchema = z.object({
13
13
  folder_id: z
14
14
  .string()
15
15
  .optional()
16
- .describe("If provided, only return cases in this folder."),
16
+ .describe("Optional. If provided, only return test cases in this folder. If omitted, returns all test cases in the project. Folder ids can be discovered via listFolders."),
17
17
  case_type: z
18
18
  .string()
19
19
  .optional()
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { BrowserStackConfig } from "../../lib/types.js";
4
+ /**
5
+ * Schema for listing test plans in a BrowserStack Test Management project.
6
+ */
7
+ export declare const ListTestPlansSchema: z.ZodObject<{
8
+ project_identifier: z.ZodString;
9
+ p: z.ZodOptional<z.ZodNumber>;
10
+ }, z.core.$strip>;
11
+ export type ListTestPlansArgs = z.infer<typeof ListTestPlansSchema>;
12
+ /**
13
+ * Lists test plans for a project in BrowserStack Test Management.
14
+ */
15
+ export declare function listTestPlans(args: ListTestPlansArgs, config: BrowserStackConfig): Promise<CallToolResult>;
@@ -0,0 +1,75 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { z } from "zod";
3
+ import { formatAxiosError } from "../../lib/error.js";
4
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
5
+ import { getTMBaseURL } from "../../lib/tm-base-url.js";
6
+ /**
7
+ * Schema for listing test plans in a BrowserStack Test Management project.
8
+ */
9
+ export const ListTestPlansSchema = z.object({
10
+ project_identifier: z
11
+ .string()
12
+ .describe("Identifier of the project to fetch test plans from (starts with PR- followed by a number)."),
13
+ p: z.number().optional().describe("Page number."),
14
+ });
15
+ /**
16
+ * Lists test plans for a project in BrowserStack Test Management.
17
+ */
18
+ export async function listTestPlans(args, config) {
19
+ try {
20
+ const params = new URLSearchParams();
21
+ if (args.p !== undefined)
22
+ params.append("p", args.p.toString());
23
+ const tmBaseUrl = await getTMBaseURL(config);
24
+ const projectId = encodeURIComponent(args.project_identifier);
25
+ const url = `${tmBaseUrl}/api/v2/projects/${projectId}/test-plans?${params.toString()}`;
26
+ const authString = getBrowserStackAuth(config);
27
+ const [username, password] = authString.split(":");
28
+ const resp = await apiClient.get({
29
+ url,
30
+ headers: {
31
+ Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
32
+ },
33
+ });
34
+ const data = resp.data;
35
+ if (!data.success) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `Failed to list test plans: ${JSON.stringify(data)}`,
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
46
+ const plans = data.test_plans ?? [];
47
+ const info = data.info ?? {};
48
+ const count = info.count ?? plans.length;
49
+ if (plans.length === 0) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: `No test plans found in project ${args.project_identifier}.`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ const summary = plans
60
+ .map((p) => `• ${p.identifier}: ${p.name} [${p.active_state}] — ${p.test_runs_count?.active ?? 0} active / ${p.test_runs_count?.closed ?? 0} closed run(s)`)
61
+ .join("\n");
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: `Found ${count} test plan(s) in project ${args.project_identifier}:\n\n${summary}`,
67
+ },
68
+ { type: "text", text: JSON.stringify(plans, null, 2) },
69
+ ],
70
+ };
71
+ }
72
+ catch (err) {
73
+ return formatAxiosError(err, "Failed to list test plans");
74
+ }
75
+ }
@@ -11,6 +11,14 @@ export interface TestCaseUpdateRequest {
11
11
  step: string;
12
12
  result: string;
13
13
  }>;
14
+ owner?: string;
15
+ priority?: string;
16
+ case_type?: string;
17
+ automation_status?: string;
18
+ status?: string;
19
+ tags?: string[];
20
+ issues?: string[];
21
+ custom_fields?: Record<string, string | number | boolean>;
14
22
  }
15
23
  export declare const UpdateTestCaseSchema: z.ZodObject<{
16
24
  project_identifier: z.ZodString;
@@ -22,6 +30,14 @@ export declare const UpdateTestCaseSchema: z.ZodObject<{
22
30
  step: z.ZodString;
23
31
  result: z.ZodString;
24
32
  }, z.core.$strip>>>;
33
+ owner: z.ZodOptional<z.ZodString>;
34
+ priority: z.ZodOptional<z.ZodString>;
35
+ case_type: z.ZodOptional<z.ZodString>;
36
+ automation_status: z.ZodOptional<z.ZodString>;
37
+ status: z.ZodOptional<z.ZodString>;
38
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
+ issues: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ custom_fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
25
41
  }, z.core.$strip>;
26
42
  /**
27
43
  * Updates an existing test case in BrowserStack Test Management.
@@ -1,7 +1,7 @@
1
1
  import { apiClient } from "../../lib/apiClient.js";
2
2
  import { z } from "zod";
3
3
  import { formatAxiosError } from "../../lib/error.js";
4
- import { projectIdentifierToId } from "./TCG-utils/api.js";
4
+ import { fetchFormFields, projectIdentifierToId } from "./TCG-utils/api.js";
5
5
  import { getTMBaseURL } from "../../lib/tm-base-url.js";
6
6
  import { getBrowserStackAuth } from "../../lib/get-auth.js";
7
7
  import logger from "../../logger.js";
@@ -28,26 +28,148 @@ export const UpdateTestCaseSchema = z.object({
28
28
  }))
29
29
  .optional()
30
30
  .describe("Updated list of test case steps with expected results."),
31
+ owner: z
32
+ .string()
33
+ .email()
34
+ .optional()
35
+ .describe("Email of the test case owner."),
36
+ priority: z
37
+ .string()
38
+ .optional()
39
+ .describe("Updated priority. Accepts either display name (e.g. 'Medium', 'Critical', 'High', 'Low') or internal name (e.g. 'medium'). Valid values are per-project and discoverable via the form-fields endpoint."),
40
+ case_type: z
41
+ .string()
42
+ .optional()
43
+ .describe("Updated test case type. Accepts either display name (e.g. 'Functional', 'Regression', 'Smoke & Sanity') or internal name (e.g. 'functional', 'smoke_sanity'). Valid values are per-project."),
44
+ automation_status: z
45
+ .string()
46
+ .optional()
47
+ .describe("Updated automation status. Use internal name such as 'not_automated', 'automated', 'automation_not_required', 'cannot_be_automated', or 'obsolete'."),
48
+ status: z
49
+ .string()
50
+ .optional()
51
+ .describe("Updated review status of the test case (e.g. 'active', 'draft', 'in_review', 'outdated', 'rejected')."),
52
+ tags: z
53
+ .array(z.string())
54
+ .optional()
55
+ .describe("Replacement list of tags for the test case."),
56
+ issues: z
57
+ .array(z.string())
58
+ .optional()
59
+ .describe("Replacement list of linked Jira/Asana/Azure issue IDs for the test case."),
60
+ custom_fields: z
61
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
62
+ .optional()
63
+ .describe("Map of custom field name/id to value. Valid field names and value types are per-project; discover them via the project's form fields."),
31
64
  });
65
+ /**
66
+ * Build a normalizer for a default field's accepted value.
67
+ * The TM PATCH endpoint accepts different casings for different default
68
+ * fields (Title-Case display name for priority/case_type, snake_case
69
+ * internal_name for automation_status). We accept either from the caller
70
+ * and emit the form the API actually wants.
71
+ *
72
+ * Returns undefined when no matching option is found — callers should
73
+ * pass the raw value through so the backend can surface its own error.
74
+ */
75
+ function normalizeDefaultFieldValue(fieldValues, input, emit) {
76
+ const normalized = input.toLowerCase().trim();
77
+ const match = fieldValues.find((v) => (v.internal_name ?? "").toLowerCase() === normalized ||
78
+ (v.name ?? "").toLowerCase() === normalized);
79
+ if (!match)
80
+ return undefined;
81
+ if (emit === "name")
82
+ return match.name;
83
+ return match.internal_name ?? match.name;
84
+ }
85
+ /**
86
+ * Normalise default-field inputs (priority/case_type/automation_status) to
87
+ * what the TM PATCH endpoint accepts. Fetches the project's form-fields
88
+ * on demand; on failure, returns inputs unchanged and lets the backend
89
+ * surface validation errors.
90
+ */
91
+ async function normalizeDefaultFields(params, config) {
92
+ const needsLookup = params.priority !== undefined ||
93
+ params.case_type !== undefined ||
94
+ params.automation_status !== undefined;
95
+ if (!needsLookup)
96
+ return {};
97
+ try {
98
+ const numericProjectId = await projectIdentifierToId(params.project_identifier, config);
99
+ const { default_fields } = await fetchFormFields(numericProjectId, config);
100
+ const out = {};
101
+ if (params.priority !== undefined) {
102
+ out.priority =
103
+ normalizeDefaultFieldValue(default_fields?.priority?.values ?? [], params.priority, "name") ?? params.priority;
104
+ }
105
+ if (params.case_type !== undefined) {
106
+ out.case_type =
107
+ normalizeDefaultFieldValue(default_fields?.case_type?.values ?? [], params.case_type, "name") ?? params.case_type;
108
+ }
109
+ if (params.automation_status !== undefined) {
110
+ // automation_status.values have null internal_name and the internal
111
+ // name is actually held in `value` (see API inspection). Accept
112
+ // either the display name or the internal snake_case form.
113
+ const values = default_fields?.automation_status?.values ?? [];
114
+ const input = params.automation_status.toLowerCase().trim();
115
+ const match = values.find((v) => (v.value ?? "").toLowerCase() === input ||
116
+ (v.name ?? "").toLowerCase() === input);
117
+ out.automation_status = match?.value ?? params.automation_status;
118
+ }
119
+ return out;
120
+ }
121
+ catch (err) {
122
+ logger.warn("Failed to normalize default field values; passing through as given: %s", err instanceof Error ? err.message : String(err));
123
+ return {
124
+ priority: params.priority,
125
+ case_type: params.case_type,
126
+ automation_status: params.automation_status,
127
+ };
128
+ }
129
+ }
32
130
  /**
33
131
  * Updates an existing test case in BrowserStack Test Management.
34
132
  */
35
133
  export async function updateTestCase(params, config) {
36
134
  const authString = getBrowserStackAuth(config);
37
135
  const [username, password] = authString.split(":");
38
- // Build the request body with only the fields to update
39
136
  const testCaseBody = {};
40
- if (params.name !== undefined) {
137
+ if (params.name !== undefined)
41
138
  testCaseBody.name = params.name;
42
- }
43
- if (params.description !== undefined) {
139
+ if (params.description !== undefined)
44
140
  testCaseBody.description = params.description;
45
- }
46
- if (params.preconditions !== undefined) {
141
+ if (params.preconditions !== undefined)
47
142
  testCaseBody.preconditions = params.preconditions;
48
- }
49
- if (params.test_case_steps !== undefined) {
143
+ if (params.test_case_steps !== undefined)
50
144
  testCaseBody.steps = params.test_case_steps;
145
+ if (params.owner !== undefined)
146
+ testCaseBody.owner = params.owner;
147
+ if (params.status !== undefined)
148
+ testCaseBody.status = params.status;
149
+ if (params.tags !== undefined)
150
+ testCaseBody.tags = params.tags;
151
+ if (params.issues !== undefined)
152
+ testCaseBody.issues = params.issues;
153
+ if (params.custom_fields !== undefined)
154
+ testCaseBody.custom_fields = params.custom_fields;
155
+ // Default fields need value normalization (see notes above the helper).
156
+ const normalized = await normalizeDefaultFields(params, config);
157
+ if (normalized.priority !== undefined)
158
+ testCaseBody.priority = normalized.priority;
159
+ if (normalized.case_type !== undefined)
160
+ testCaseBody.case_type = normalized.case_type;
161
+ if (normalized.automation_status !== undefined)
162
+ testCaseBody.automation_status = normalized.automation_status;
163
+ if (Object.keys(testCaseBody).length === 0) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: "No updatable fields provided. Pass at least one of: name, description, preconditions, test_case_steps, owner, priority, case_type, automation_status, status, tags, issues, custom_fields.",
169
+ },
170
+ ],
171
+ isError: true,
172
+ };
51
173
  }
52
174
  const body = { test_case: testCaseBody };
53
175
  try {
@@ -80,7 +202,7 @@ export async function updateTestCase(params, config) {
80
202
  {
81
203
  type: "text",
82
204
  text: `Test case successfully updated:
83
-
205
+
84
206
  **Test Case Details:**
85
207
  - **ID**: ${tc.identifier}
86
208
  - **Name**: ${tc.title}
@@ -88,6 +210,7 @@ export async function updateTestCase(params, config) {
88
210
  - **Case Type**: ${tc.case_type}
89
211
  - **Priority**: ${tc.priority}
90
212
  - **Status**: ${tc.status}
213
+ - **Automation Status**: ${tc.automation_status ?? "N/A"}
91
214
 
92
215
  **View on BrowserStack Dashboard:**
93
216
  https://test-management.browserstack.com/projects/${projectId}/folders/${tc.folder_id}/test-cases/${tc.identifier}
@@ -5,6 +5,7 @@ import { CreateProjFoldSchema } from "./testmanagement-utils/create-project-fold
5
5
  import { TestCaseCreateRequest } from "./testmanagement-utils/create-testcase.js";
6
6
  import { TestCaseUpdateRequest } from "./testmanagement-utils/update-testcase.js";
7
7
  import { ListTestCasesSchema } from "./testmanagement-utils/list-testcases.js";
8
+ import { ListFoldersSchema } from "./testmanagement-utils/list-folders.js";
8
9
  import { CreateTestRunSchema } from "./testmanagement-utils/create-testrun.js";
9
10
  import { ListTestRunsSchema } from "./testmanagement-utils/list-testruns.js";
10
11
  import { UpdateTestRunSchema } from "./testmanagement-utils/update-testrun.js";
@@ -12,6 +13,8 @@ import { AddTestResultSchema } from "./testmanagement-utils/add-test-result.js";
12
13
  import { UploadFileSchema } from "./testmanagement-utils/upload-file.js";
13
14
  import { CreateTestCasesFromFileSchema } from "./testmanagement-utils/TCG-utils/types.js";
14
15
  import { CreateLCAStepsSchema } from "./testmanagement-utils/create-lca-steps.js";
16
+ import { ListTestPlansSchema } from "./testmanagement-utils/list-testplans.js";
17
+ import { GetTestPlanSchema } from "./testmanagement-utils/get-testplan.js";
15
18
  import { BrowserStackConfig } from "../lib/types.js";
16
19
  /**
17
20
  * Wrapper to call createProjectOrFolder util.
@@ -29,6 +32,10 @@ export declare function updateTestCaseTool(args: TestCaseUpdateRequest, config:
29
32
  * Lists test cases in a project with optional filters (status, priority, custom fields, etc.)
30
33
  */
31
34
  export declare function listTestCasesTool(args: z.infer<typeof ListTestCasesSchema>, config: BrowserStackConfig, server: McpServer): Promise<CallToolResult>;
35
+ /**
36
+ * Lists folders in a project (or sub-folders under a parent folder).
37
+ */
38
+ export declare function listFoldersTool(args: z.infer<typeof ListFoldersSchema>, config: BrowserStackConfig, server: McpServer): Promise<CallToolResult>;
32
39
  /**
33
40
  * Creates a test run in BrowserStack Test Management.
34
41
  */
@@ -59,6 +66,14 @@ export declare function createTestCasesFromFileTool(args: z.infer<typeof CreateT
59
66
  * Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management.
60
67
  */
61
68
  export declare function createLCAStepsTool(args: z.infer<typeof CreateLCAStepsSchema>, context: any, config: BrowserStackConfig, server: McpServer): Promise<CallToolResult>;
69
+ /**
70
+ * Lists test plans in a project.
71
+ */
72
+ export declare function listTestPlansTool(args: z.infer<typeof ListTestPlansSchema>, config: BrowserStackConfig, server: McpServer): Promise<CallToolResult>;
73
+ /**
74
+ * Fetches a test plan by identifier, with its linked runs and a derived status summary.
75
+ */
76
+ export declare function getTestPlanTool(args: z.infer<typeof GetTestPlanSchema>, config: BrowserStackConfig, server: McpServer): Promise<CallToolResult>;
62
77
  /**
63
78
  * Registers both project/folder and test-case tools.
64
79
  */
@@ -4,6 +4,7 @@ import { createProjectOrFolder, CreateProjFoldSchema, } from "./testmanagement-u
4
4
  import { createTestCase as createTestCaseAPI, sanitizeArgs, CreateTestCaseSchema, } from "./testmanagement-utils/create-testcase.js";
5
5
  import { updateTestCase as updateTestCaseAPI, UpdateTestCaseSchema, } from "./testmanagement-utils/update-testcase.js";
6
6
  import { listTestCases, ListTestCasesSchema, } from "./testmanagement-utils/list-testcases.js";
7
+ import { listFolders, ListFoldersSchema, } from "./testmanagement-utils/list-folders.js";
7
8
  import { CreateTestRunSchema, createTestRun, } from "./testmanagement-utils/create-testrun.js";
8
9
  import { ListTestRunsSchema, listTestRuns, } from "./testmanagement-utils/list-testruns.js";
9
10
  import { UpdateTestRunSchema, updateTestRun, } from "./testmanagement-utils/update-testrun.js";
@@ -12,6 +13,8 @@ import { UploadFileSchema, uploadFile, } from "./testmanagement-utils/upload-fil
12
13
  import { createTestCasesFromFile } from "./testmanagement-utils/testcase-from-file.js";
13
14
  import { CreateTestCasesFromFileSchema } from "./testmanagement-utils/TCG-utils/types.js";
14
15
  import { createLCASteps, CreateLCAStepsSchema, } from "./testmanagement-utils/create-lca-steps.js";
16
+ import { listTestPlans, ListTestPlansSchema, } from "./testmanagement-utils/list-testplans.js";
17
+ import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-testplan.js";
15
18
  //TODO: Moving the traceMCP and catch block to the parent(server) function
16
19
  /**
17
20
  * Wrapper to call createProjectOrFolder util.
@@ -102,6 +105,27 @@ export async function listTestCasesTool(args, config, server) {
102
105
  };
103
106
  }
104
107
  }
108
+ /**
109
+ * Lists folders in a project (or sub-folders under a parent folder).
110
+ */
111
+ export async function listFoldersTool(args, config, server) {
112
+ try {
113
+ trackMCP("listFolders", server.server.getClientVersion(), undefined, config);
114
+ return await listFolders(args, config);
115
+ }
116
+ catch (err) {
117
+ trackMCP("listFolders", server.server.getClientVersion(), err, config);
118
+ return {
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: `Failed to list folders: ${err instanceof Error ? err.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
123
+ },
124
+ ],
125
+ isError: true,
126
+ };
127
+ }
128
+ }
105
129
  /**
106
130
  * Creates a test run in BrowserStack Test Management.
107
131
  */
@@ -251,6 +275,50 @@ export async function createLCAStepsTool(args, context, config, server) {
251
275
  };
252
276
  }
253
277
  }
278
+ /**
279
+ * Lists test plans in a project.
280
+ */
281
+ export async function listTestPlansTool(args, config, server) {
282
+ try {
283
+ trackMCP("listTestPlans", server.server.getClientVersion(), undefined, config);
284
+ return await listTestPlans(args, config);
285
+ }
286
+ catch (err) {
287
+ logger.error("Failed to list test plans: %s", err);
288
+ trackMCP("listTestPlans", server.server.getClientVersion(), err, config);
289
+ return {
290
+ content: [
291
+ {
292
+ type: "text",
293
+ text: `Failed to list test plans: ${err instanceof Error ? err.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
294
+ },
295
+ ],
296
+ isError: true,
297
+ };
298
+ }
299
+ }
300
+ /**
301
+ * Fetches a test plan by identifier, with its linked runs and a derived status summary.
302
+ */
303
+ export async function getTestPlanTool(args, config, server) {
304
+ try {
305
+ trackMCP("getTestPlan", server.server.getClientVersion(), undefined, config);
306
+ return await getTestPlan(args, config);
307
+ }
308
+ catch (err) {
309
+ logger.error("Failed to fetch test plan: %s", err);
310
+ trackMCP("getTestPlan", server.server.getClientVersion(), err, config);
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: `Failed to fetch test plan: ${err instanceof Error ? err.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
316
+ },
317
+ ],
318
+ isError: true,
319
+ };
320
+ }
321
+ }
254
322
  /**
255
323
  * Registers both project/folder and test-case tools.
256
324
  */
@@ -258,8 +326,9 @@ export default function addTestManagementTools(server, config) {
258
326
  const tools = {};
259
327
  tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management.", CreateProjFoldSchema.shape, (args) => createProjectOrFolderTool(args, config, server));
260
328
  tools.createTestCase = server.tool("createTestCase", "Use this tool to create a test case in BrowserStack Test Management.", CreateTestCaseSchema.shape, (args) => createTestCaseTool(args, config, server));
261
- tools.updateTestCase = server.tool("updateTestCase", "Use this tool to update an existing test case in BrowserStack Test Management. Allows editing test case details like name, description, steps, owner, priority, and more.", UpdateTestCaseSchema.shape, (args) => updateTestCaseTool(args, config, server));
262
- tools.listTestCases = server.tool("listTestCases", "List test cases in a project with optional filters (status, priority, custom fields, etc.)", ListTestCasesSchema.shape, (args) => listTestCasesTool(args, config, server));
329
+ 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));
330
+ 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));
331
+ 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));
263
332
  tools.createTestRun = server.tool("createTestRun", "Create a test run in BrowserStack Test Management.", CreateTestRunSchema.shape, (args) => createTestRunTool(args, config, server));
264
333
  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));
265
334
  tools.updateTestRun = server.tool("updateTestRun", "Update a test run in BrowserStack Test Management.", UpdateTestRunSchema.shape, (args) => updateTestRunTool(args, config, server));
@@ -267,5 +336,7 @@ export default function addTestManagementTools(server, config) {
267
336
  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));
268
337
  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));
269
338
  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));
339
+ 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));
340
+ 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));
270
341
  return tools;
271
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.2.15-beta.1",
3
+ "version": "1.2.16-beta.1",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",