Skip to content

Commit 204e996

Browse files
Feat: Add test management utilities for creating test runs and listing test cases
1 parent fc13c91 commit 204e996

File tree

4 files changed

+416
-3
lines changed

4 files changed

+416
-3
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import axios from "axios";
2+
import config from "../../config";
3+
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error";
6+
7+
/**
8+
* Schema for creating a test run.
9+
*/
10+
export const CreateTestRunSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe("Identifier of the project in which to create the test run."),
14+
test_run: z.object({
15+
name: z.string().describe("Name of the test run"),
16+
description: z
17+
.string()
18+
.optional()
19+
.describe("Brief information about the test run"),
20+
run_state: z
21+
.enum([
22+
"new_run",
23+
"in_progress",
24+
"under_review",
25+
"rejected",
26+
"done",
27+
"closed",
28+
])
29+
.optional()
30+
.describe(
31+
"State of the test run. One of new_run, in_progress, under_review, rejected, done, closed",
32+
),
33+
assignee: z
34+
.string()
35+
.email()
36+
.optional()
37+
.describe("Email of the test run assignee"),
38+
test_case_assignee: z
39+
.string()
40+
.email()
41+
.optional()
42+
.describe("Email of the test case assignee"),
43+
tags: z.array(z.string()).optional().describe("Labels for the test run"),
44+
issues: z.array(z.string()).optional().describe("Linked issue IDs"),
45+
issue_tracker: z
46+
.object({ name: z.string(), host: z.string().url() })
47+
.optional()
48+
.describe("Issue tracker configuration"),
49+
configurations: z
50+
.array(z.number())
51+
.optional()
52+
.describe("List of configuration IDs"),
53+
test_plan_id: z.string().optional().describe("Identifier of the test plan"),
54+
test_cases: z
55+
.array(z.string())
56+
.optional()
57+
.describe("List of test case IDs"),
58+
folder_ids: z
59+
.array(z.number())
60+
.optional()
61+
.describe("Folder IDs to include"),
62+
include_all: z
63+
.boolean()
64+
.optional()
65+
.describe("If true, include all test cases in the project"),
66+
is_automation: z
67+
.boolean()
68+
.optional()
69+
.describe("Mark as automated if true, otherwise manual"),
70+
filter_test_cases: z
71+
.object({
72+
status: z.array(z.string()).optional(),
73+
priority: z.array(z.string()).optional(),
74+
case_type: z.array(z.string()).optional(),
75+
owner: z.array(z.string()).optional(),
76+
tags: z.array(z.string()).optional(),
77+
custom_fields: z
78+
.record(z.array(z.union([z.string(), z.number()])))
79+
.optional(),
80+
})
81+
.optional()
82+
.describe("Filters to apply before adding test cases"),
83+
}),
84+
});
85+
86+
export type CreateTestRunArgs = z.infer<typeof CreateTestRunSchema>;
87+
88+
/**
89+
* Creates a test run via BrowserStack Test Management API.
90+
*/
91+
export async function createTestRun(
92+
rawArgs: CreateTestRunArgs,
93+
): Promise<CallToolResult> {
94+
try {
95+
// Validate and narrow
96+
const args = CreateTestRunSchema.parse(rawArgs);
97+
98+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
99+
args.project_identifier,
100+
)}/test-runs`;
101+
102+
const response = await axios.post(
103+
url,
104+
{ test_run: args.test_run },
105+
{
106+
auth: {
107+
username: config.browserstackUsername,
108+
password: config.browserstackAccessKey,
109+
},
110+
headers: { "Content-Type": "application/json" },
111+
},
112+
);
113+
114+
const data = response.data;
115+
if (!data.success) {
116+
throw new Error(
117+
`API returned unsuccessful response: ${JSON.stringify(data)}`,
118+
);
119+
}
120+
121+
// Assume data.test_run contains created run info
122+
const created = data.test_run || data;
123+
const runId = created.identifier || created.id || created.name;
124+
125+
const summary = `Successfully created test run ${runId}`;
126+
return {
127+
content: [
128+
{ type: "text", text: summary },
129+
{ type: "text", text: JSON.stringify(created, null, 2) },
130+
],
131+
};
132+
} catch (err) {
133+
return formatAxiosError(err, "Failed to create test run");
134+
}
135+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import axios from "axios";
2+
import config from "../../config";
3+
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
import { formatAxiosError } from "../../lib/error";
6+
7+
/**
8+
* Schema for listing test cases with optional filters.
9+
*/
10+
export const ListTestCasesSchema = z.object({
11+
project_identifier: z
12+
.string()
13+
.describe("Identifier of the project to fetch test cases from."),
14+
folder_id: z
15+
.string()
16+
.optional()
17+
.describe("If provided, only return cases in this folder."),
18+
case_type: z
19+
.string()
20+
.optional()
21+
.describe(
22+
"Comma-separated list of case types (e.g. functional,regression).",
23+
),
24+
priority: z
25+
.string()
26+
.optional()
27+
.describe("Comma-separated list of priorities (e.g. critical,medium,low)."),
28+
status: z
29+
.string()
30+
.optional()
31+
.describe("Comma-separated list of statuses (e.g. draft,active)."),
32+
tags: z.string().optional().describe("Comma-separated list of tags."),
33+
owner: z.string().optional().describe("Owner email to filter by."),
34+
p: z.number().optional().describe("Page number."),
35+
custom_fields: z
36+
.record(z.array(z.string()))
37+
.optional()
38+
.describe(
39+
"Map of custom field names to arrays of values, e.g. { estimate: ['10','20'], 'automation type': ['automated'] }",
40+
),
41+
});
42+
43+
export type ListTestCasesArgs = z.infer<typeof ListTestCasesSchema>;
44+
45+
/**
46+
* Calls BrowserStack Test Management to list test cases with filters.
47+
*/
48+
export async function listTestCases(
49+
args: ListTestCasesArgs,
50+
): Promise<CallToolResult> {
51+
try {
52+
// Build query string
53+
const params = new URLSearchParams();
54+
if (args.folder_id) params.append("folder_id", args.folder_id);
55+
if (args.case_type) params.append("case_type", args.case_type);
56+
if (args.priority) params.append("priority", args.priority);
57+
if (args.status) params.append("status", args.status);
58+
if (args.tags) params.append("tags", args.tags);
59+
if (args.owner) params.append("owner", args.owner);
60+
if (args.p !== undefined) params.append("p", args.p.toString());
61+
if (args.custom_fields) {
62+
for (const [field, values] of Object.entries(args.custom_fields)) {
63+
params.append(`custom_fields[${field}]`, values.join(","));
64+
}
65+
}
66+
67+
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
68+
args.project_identifier,
69+
)}/test-cases?${params.toString()}`;
70+
71+
const resp = await axios.get(url, {
72+
auth: {
73+
username: config.browserstackUsername,
74+
password: config.browserstackAccessKey,
75+
},
76+
});
77+
78+
const { test_cases, info } = resp.data;
79+
const count = info?.count ?? test_cases.length;
80+
81+
// Summary for more focused output
82+
const summary = test_cases
83+
.map(
84+
(tc: any) =>
85+
`• ${tc.identifier}: ${tc.title} [${tc.case_type} | ${tc.status} | ${tc.priority}]`,
86+
)
87+
.join("\n");
88+
89+
return {
90+
content: [
91+
{
92+
type: "text",
93+
text: `Found ${count} test case(s):\n\n${summary}`,
94+
},
95+
{
96+
type: "text",
97+
text: JSON.stringify(test_cases, null, 2),
98+
},
99+
],
100+
};
101+
} catch (err) {
102+
return formatAxiosError(err, "Failed to list test cases");
103+
}
104+
}

src/tools/testmanagement.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ import {
1212
CreateTestCaseSchema,
1313
} from "./testmanagement-utils/create-testcase";
1414

15+
import {
16+
listTestCases,
17+
ListTestCasesSchema,
18+
} from "./testmanagement-utils/list-testcases";
19+
20+
import {
21+
CreateTestRunSchema,
22+
createTestRun,
23+
} from "./testmanagement-utils/create-testrun";
24+
1525
/**
1626
* Wrapper to call createProjectOrFolder util.
1727
*/
@@ -62,6 +72,56 @@ export async function createTestCaseTool(
6272
}
6373
}
6474

75+
/**
76+
* Lists test cases in a project with optional filters (status, priority, custom fields, etc.)
77+
*/
78+
79+
export async function listTestCasesTool(
80+
args: z.infer<typeof ListTestCasesSchema>,
81+
): Promise<CallToolResult> {
82+
try {
83+
return await listTestCases(args);
84+
} catch (err) {
85+
return {
86+
content: [
87+
{
88+
type: "text",
89+
text: `Failed to list test cases: ${
90+
err instanceof Error ? err.message : "Unknown error"
91+
}. Please open an issue on GitHub if the problem persists`,
92+
isError: true,
93+
},
94+
],
95+
isError: true,
96+
};
97+
}
98+
}
99+
100+
/**
101+
* CREATE TEST RUN
102+
* Creates a test run in BrowserStack Test Management.
103+
*/
104+
export async function createTestRunTool(
105+
args: z.infer<typeof CreateTestRunSchema>,
106+
): Promise<CallToolResult> {
107+
try {
108+
return await createTestRun(args);
109+
} catch (err) {
110+
return {
111+
content: [
112+
{
113+
type: "text",
114+
text: `Failed to create test run: ${
115+
err instanceof Error ? err.message : "Unknown error"
116+
}. Please open an issue on GitHub if the problem persists`,
117+
isError: true,
118+
},
119+
],
120+
isError: true,
121+
};
122+
}
123+
}
124+
65125
/**
66126
* Registers both project/folder and test-case tools.
67127
*/
@@ -79,4 +139,18 @@ export default function addTestManagementTools(server: McpServer) {
79139
CreateTestCaseSchema.shape,
80140
createTestCaseTool,
81141
);
142+
143+
server.tool(
144+
"listTestCases",
145+
"List test cases in a project with optional filters (status, priority, custom fields, etc.)",
146+
ListTestCasesSchema.shape,
147+
listTestCasesTool,
148+
);
149+
150+
server.tool(
151+
"createTestRun",
152+
"Create a test run in BrowserStack Test Management.",
153+
CreateTestRunSchema.shape,
154+
createTestRunTool,
155+
);
82156
}

0 commit comments

Comments
 (0)