Skip to content

Commit 19ad3ef

Browse files
Feat: Add Test Management Tools for Project, Folder, and Test Case Creation (#8)
* Feat : test management tools and create test case functionality * Feat: Implement project and folder creation utility and integrate with test management tools * Refactor: Remove unnecessary nullish checks from test case tool schema * Feat: Add project and folder creation tests with error handling * Feat: Add Zod schema for test case creation validation * Feat: Add error handling for test case creation response * Fix: Improve descriptions in CreateTestCaseSchema for clarity * Fix: Update descriptions in project and test case schemas for consistency * Feat: Add error handling for project and folder creation responses * Fix: Simplify error handling in project and test case creation functions * Fix: Correct error handling for project and folder creation responses * Fix: Refine error handling in project and folder creation functions * Fix: Enhance error handling for project and folder creation * Fix: Refactor error handling in project and folder creation to use a centralized function * Fix: Centralize Axios error formatting and improve error handling in project and test case creation
1 parent 852b38c commit 19ad3ef

File tree

6 files changed

+582
-0
lines changed

6 files changed

+582
-0
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import addObservabilityTools from "./tools/observability";
1111
import addBrowserLiveTools from "./tools/live";
1212
import addAccessibilityTools from "./tools/accessibility";
1313
import addAutomateTools from "./tools/automate";
14+
import addTestManagementTools from "./tools/testmanagement";
1415

1516
function registerTools(server: McpServer) {
1617
addSDKTools(server);
@@ -19,6 +20,7 @@ function registerTools(server: McpServer) {
1920
addObservabilityTools(server);
2021
addAccessibilityTools(server);
2122
addAutomateTools(server);
23+
addTestManagementTools(server);
2224
}
2325

2426
// Create an MCP server

src/lib/error.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AxiosError } from "axios";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
4+
/**
5+
* Formats an AxiosError into a CallToolResult with an appropriate message.
6+
* @param err - The error object to format
7+
* @param defaultText - The fallback error message
8+
*/
9+
export function formatAxiosError(
10+
err: unknown,
11+
defaultText: string,
12+
): CallToolResult {
13+
let text = defaultText;
14+
15+
if (err instanceof AxiosError && err.response?.data) {
16+
const message =
17+
err.response.data.message ||
18+
err.response.data.error ||
19+
err.message ||
20+
defaultText;
21+
text = message;
22+
} else if (err instanceof Error) {
23+
text = err.message;
24+
}
25+
26+
return {
27+
content: [{ type: "text", text }],
28+
isError: true,
29+
};
30+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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"; // or correct path
6+
7+
// Schema for combined project/folder creation
8+
export const CreateProjFoldSchema = z.object({
9+
project_name: z
10+
.string()
11+
.optional()
12+
.describe("Name of the project to create."),
13+
project_description: z
14+
.string()
15+
.optional()
16+
.describe("Description for the new project."),
17+
project_identifier: z
18+
.string()
19+
.optional()
20+
.describe("Existing project identifier to use for folder creation."),
21+
folder_name: z.string().optional().describe("Name of the folder to create."),
22+
folder_description: z
23+
.string()
24+
.optional()
25+
.describe("Description for the new folder."),
26+
parent_id: z
27+
.number()
28+
.optional()
29+
.describe("Parent folder ID; if omitted, folder is created at root."),
30+
});
31+
32+
type CreateProjFoldArgs = z.infer<typeof CreateProjFoldSchema>;
33+
34+
/**
35+
* Creates a project and/or folder in BrowserStack Test Management.
36+
*/
37+
export async function createProjectOrFolder(
38+
args: CreateProjFoldArgs,
39+
): Promise<CallToolResult> {
40+
const {
41+
project_name,
42+
project_description,
43+
project_identifier,
44+
folder_name,
45+
folder_description,
46+
parent_id,
47+
} = CreateProjFoldSchema.parse(args);
48+
49+
if (!project_name && !project_identifier && !folder_name) {
50+
throw new Error(
51+
"Provide project_name (to create project), or project_identifier and folder_name (to create folder).",
52+
);
53+
}
54+
55+
let projId = project_identifier;
56+
57+
// Step 1: Create project if project_name provided
58+
if (project_name) {
59+
try {
60+
const res = await axios.post(
61+
"https://test-management.browserstack.com/api/v2/projects",
62+
{ project: { name: project_name, description: project_description } },
63+
{
64+
auth: {
65+
username: config.browserstackUsername,
66+
password: config.browserstackAccessKey,
67+
},
68+
headers: { "Content-Type": "application/json" },
69+
},
70+
);
71+
72+
if (!res.data.success) {
73+
throw new Error(
74+
`Failed to create project: ${JSON.stringify(res.data)}`,
75+
);
76+
}
77+
// Project created successfully
78+
79+
projId = res.data.project.identifier;
80+
} catch (err) {
81+
return formatAxiosError(err, "Failed to create project..");
82+
}
83+
}
84+
// Step 2: Create folder if folder_name provided
85+
if (folder_name) {
86+
if (!projId)
87+
throw new Error("Cannot create folder without project_identifier.");
88+
try {
89+
const res = await axios.post(
90+
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
91+
projId,
92+
)}/folders`,
93+
{
94+
folder: {
95+
name: folder_name,
96+
description: folder_description,
97+
parent_id,
98+
},
99+
},
100+
{
101+
auth: {
102+
username: config.browserstackUsername,
103+
password: config.browserstackAccessKey,
104+
},
105+
headers: { "Content-Type": "application/json" },
106+
},
107+
);
108+
109+
if (!res.data.success) {
110+
throw new Error(`Failed to create folder: ${JSON.stringify(res.data)}`);
111+
}
112+
// Folder created successfully
113+
114+
const folder = res.data.folder;
115+
return {
116+
content: [
117+
{
118+
type: "text",
119+
text: `Folder created: ID=${folder.id}, name=${folder.name} in project with identifier ${projId}`,
120+
},
121+
],
122+
};
123+
} catch (err) {
124+
return formatAxiosError(err, "Failed to create folder.");
125+
}
126+
}
127+
128+
// Only project was created
129+
return {
130+
content: [
131+
{ type: "text", text: `Project created with identifier=${projId}` },
132+
],
133+
};
134+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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"; // or correct
6+
7+
interface TestCaseStep {
8+
step: string;
9+
result: string;
10+
}
11+
12+
interface IssueTracker {
13+
name: string;
14+
host: string;
15+
}
16+
17+
export interface TestCaseCreateRequest {
18+
project_identifier: string;
19+
folder_id: string;
20+
name: string;
21+
description?: string;
22+
owner?: string;
23+
preconditions?: string;
24+
test_case_steps: TestCaseStep[];
25+
issues?: string[];
26+
issue_tracker?: IssueTracker;
27+
tags?: string[];
28+
custom_fields?: Record<string, string>;
29+
}
30+
31+
export interface TestCaseResponse {
32+
data: {
33+
success: boolean;
34+
test_case: {
35+
case_type: string;
36+
priority: string;
37+
status: string;
38+
folder_id: number;
39+
issues: Array<{
40+
jira_id: string;
41+
issue_type: string;
42+
}>;
43+
tags: string[];
44+
template: string;
45+
description: string;
46+
preconditions: string;
47+
title: string;
48+
identifier: string;
49+
automation_status: string;
50+
owner: string;
51+
steps: TestCaseStep[];
52+
custom_fields: Array<{
53+
name: string;
54+
value: string;
55+
}>;
56+
};
57+
};
58+
}
59+
60+
export const CreateTestCaseSchema = z.object({
61+
project_identifier: z
62+
.string()
63+
.describe(
64+
"The ID of the BrowserStack project where the test case should be created. If no project identifier is provided, ask the user if they would like to create a new project using the createProjectOrFolder tool.",
65+
),
66+
folder_id: z
67+
.string()
68+
.describe(
69+
"The ID of the folder within the project where the test case should be created. If not provided, ask the user if they would like to create a new folder using the createProjectOrFolder tool.",
70+
),
71+
name: z.string().describe("Name of the test case."),
72+
description: z
73+
.string()
74+
.optional()
75+
.describe("Brief description of the test case."),
76+
owner: z
77+
.string()
78+
.email()
79+
.describe("Email of the test case owner.")
80+
.optional(),
81+
preconditions: z
82+
.string()
83+
.optional()
84+
.describe("Any preconditions (HTML allowed)."),
85+
test_case_steps: z
86+
.array(
87+
z.object({
88+
step: z.string().describe("Action to perform in this step."),
89+
result: z.string().describe("Expected result of this step."),
90+
}),
91+
)
92+
.describe("List of steps and expected results."),
93+
issues: z
94+
.array(z.string())
95+
.optional()
96+
.describe(
97+
"List of the linked Jira, Asana or Azure issues ID's. This should be strictly in array format not the string of json.",
98+
),
99+
issue_tracker: z
100+
.object({
101+
name: z
102+
.string()
103+
.describe(
104+
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.",
105+
),
106+
host: z.string().url().describe("Base URL of the issue tracker."),
107+
})
108+
.optional(),
109+
tags: z
110+
.array(z.string())
111+
.optional()
112+
.describe(
113+
"Tags to attach to the test case. This should be strictly in array format not the string of json",
114+
),
115+
custom_fields: z
116+
.record(z.string(), z.string())
117+
.optional()
118+
.describe("Map of custom field names to values."),
119+
});
120+
121+
export function sanitizeArgs(args: any) {
122+
const cleaned = { ...args };
123+
124+
if (cleaned.description === null) delete cleaned.description;
125+
if (cleaned.owner === null) delete cleaned.owner;
126+
if (cleaned.preconditions === null) delete cleaned.preconditions;
127+
128+
if (cleaned.issue_tracker) {
129+
if (
130+
cleaned.issue_tracker.name === undefined ||
131+
cleaned.issue_tracker.host === undefined
132+
) {
133+
delete cleaned.issue_tracker;
134+
}
135+
}
136+
137+
return cleaned;
138+
}
139+
140+
export async function createTestCase(
141+
params: TestCaseCreateRequest,
142+
): Promise<CallToolResult> {
143+
const body = { test_case: params };
144+
145+
try {
146+
const response = await axios.post<TestCaseResponse>(
147+
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
148+
params.project_identifier,
149+
)}/folders/${encodeURIComponent(params.folder_id)}/test-cases`,
150+
body,
151+
{
152+
auth: {
153+
username: config.browserstackUsername,
154+
password: config.browserstackAccessKey,
155+
},
156+
headers: { "Content-Type": "application/json" },
157+
},
158+
);
159+
160+
const { data } = response.data;
161+
if (!data.success) {
162+
return {
163+
content: [
164+
{
165+
type: "text",
166+
text: `Failed to create test case: ${JSON.stringify(
167+
response.data,
168+
)}`,
169+
isError: true,
170+
},
171+
],
172+
isError: true,
173+
};
174+
}
175+
176+
const tc = data.test_case;
177+
return {
178+
content: [
179+
{
180+
type: "text",
181+
text: `Successfully created test case ${tc.identifier}: ${tc.title}`,
182+
},
183+
{ type: "text", text: JSON.stringify(tc, null, 2) },
184+
],
185+
};
186+
} catch (err) {
187+
// Delegate to our centralized Axios error formatter
188+
return formatAxiosError(err, "Failed to create test case");
189+
}
190+
}

0 commit comments

Comments
 (0)