Skip to content

Commit f309c86

Browse files
Feat: Implement project and folder creation utility and integrate with test management tools
1 parent db791e9 commit f309c86

File tree

4 files changed

+210
-24
lines changed

4 files changed

+210
-24
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import axios, { AxiosError } from "axios";
2+
import config from "../../config";
3+
import { z } from "zod";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
6+
// Schema for combined project/folder creation
7+
export const CreateProjFoldSchema = z.object({
8+
project_name: z
9+
.string()
10+
.optional()
11+
.describe("Name of the project to create."),
12+
project_description: z
13+
.string()
14+
.optional()
15+
.describe("Description for the new project."),
16+
project_identifier: z
17+
.string()
18+
.optional()
19+
.describe("Existing project identifier to use for folder creation."),
20+
folder_name: z.string().optional().describe("Name of the folder to create."),
21+
folder_description: z
22+
.string()
23+
.optional()
24+
.describe("Description for the new folder."),
25+
parent_id: z
26+
.number()
27+
.optional()
28+
.describe("Parent folder ID; if omitted, folder is created at root."),
29+
});
30+
31+
type CreateProjFoldArgs = z.infer<typeof CreateProjFoldSchema>;
32+
33+
/**
34+
* Creates a project and/or folder in BrowserStack Test Management.
35+
*/
36+
export async function createProjectOrFolder(
37+
args: CreateProjFoldArgs,
38+
): Promise<CallToolResult> {
39+
const {
40+
project_name,
41+
project_description,
42+
project_identifier,
43+
folder_name,
44+
folder_description,
45+
parent_id,
46+
} = CreateProjFoldSchema.parse(args);
47+
48+
if (!project_name && !project_identifier && !folder_name) {
49+
throw new Error(
50+
"Provide project_name (to create project), or project_identifier and folder_name (to create folder).",
51+
);
52+
}
53+
54+
let projId = project_identifier;
55+
56+
// Step 1: Create project if project_name provided
57+
if (project_name) {
58+
try {
59+
const res = await axios.post(
60+
"https://test-management.browserstack.com/api/v2/projects",
61+
{ project: { name: project_name, description: project_description } },
62+
{
63+
auth: {
64+
username: config.browserstackUsername,
65+
password: config.browserstackAccessKey,
66+
},
67+
headers: { "Content-Type": "application/json" },
68+
},
69+
);
70+
projId = res.data.project.identifier;
71+
} catch (err) {
72+
const msg =
73+
err instanceof AxiosError && err.response?.data?.message
74+
? err.response.data.message
75+
: err instanceof Error
76+
? err.message
77+
: "Unknown error";
78+
return {
79+
content: [{ type: "text", text: `Failed to create project: ${msg}` }],
80+
isError: true,
81+
};
82+
}
83+
}
84+
85+
// Step 2: Create folder if folder_name provided
86+
if (folder_name) {
87+
if (!projId)
88+
throw new Error("Cannot create folder without project_identifier.");
89+
try {
90+
const res = await axios.post(
91+
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
92+
projId,
93+
)}/folders`,
94+
{
95+
folder: {
96+
name: folder_name,
97+
description: folder_description,
98+
parent_id,
99+
},
100+
},
101+
{
102+
auth: {
103+
username: config.browserstackUsername,
104+
password: config.browserstackAccessKey,
105+
},
106+
headers: { "Content-Type": "application/json" },
107+
},
108+
);
109+
const folder = res.data.folder;
110+
return {
111+
content: [
112+
{
113+
type: "text",
114+
text: `Folder created: ID=${folder.id}, name="${folder.name}" in project ${projId}`,
115+
},
116+
],
117+
};
118+
} catch (err) {
119+
const msg =
120+
err instanceof AxiosError && err.response?.data?.message
121+
? err.response.data.message
122+
: err instanceof Error
123+
? err.message
124+
: "Unknown error";
125+
return {
126+
content: [{ type: "text", text: `Failed to create folder: ${msg}` }],
127+
isError: true,
128+
};
129+
}
130+
}
131+
132+
// Only project was created
133+
return {
134+
content: [
135+
{ type: "text", text: `Project created with identifier=${projId}` },
136+
],
137+
};
138+
}

src/tools/testmanagement-utils/create-testcase.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import axios, { AxiosError } from "axios";
22
import config from "../../config";
3-
import logger from "./../../logger";
43

54
interface TestCaseStep {
65
step: string;
@@ -11,24 +10,6 @@ interface IssueTracker {
1110
name: string;
1211
host: string;
1312
}
14-
export function sanitizeArgs(args: any) {
15-
const cleaned = { ...args };
16-
17-
if (cleaned.description === null) delete cleaned.description;
18-
if (cleaned.owner === null) delete cleaned.owner;
19-
if (cleaned.preconditions === null) delete cleaned.preconditions;
20-
21-
if (cleaned.issue_tracker) {
22-
if (
23-
cleaned.issue_tracker.name === undefined ||
24-
cleaned.issue_tracker.host === undefined
25-
) {
26-
delete cleaned.issue_tracker;
27-
}
28-
}
29-
30-
return cleaned;
31-
}
3213

3314
export interface TestCaseCreateRequest {
3415
project_identifier: string;
@@ -73,6 +54,25 @@ export interface TestCaseResponse {
7354
};
7455
}
7556

57+
export function sanitizeArgs(args: any) {
58+
const cleaned = { ...args };
59+
60+
if (cleaned.description === null) delete cleaned.description;
61+
if (cleaned.owner === null) delete cleaned.owner;
62+
if (cleaned.preconditions === null) delete cleaned.preconditions;
63+
64+
if (cleaned.issue_tracker) {
65+
if (
66+
cleaned.issue_tracker.name === undefined ||
67+
cleaned.issue_tracker.host === undefined
68+
) {
69+
delete cleaned.issue_tracker;
70+
}
71+
}
72+
73+
return cleaned;
74+
}
75+
7676
export async function createTestCase(
7777
params: TestCaseCreateRequest,
7878
): Promise<TestCaseResponse> {

src/tools/testmanagement.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { z } from "zod";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import {
5+
createProjectOrFolder,
6+
CreateProjFoldSchema,
7+
} from "./testmanagement-utils/create-project-folder";
48
import {
59
createTestCase as createTestCaseAPI,
610
TestCaseCreateRequest,
711
sanitizeArgs,
812
} from "./testmanagement-utils/create-testcase";
913

14+
/**
15+
* Wrapper to call createProjectOrFolder util.
16+
*/
17+
export async function createProjectOrFolderTool(
18+
args: z.infer<typeof CreateProjFoldSchema>,
19+
): Promise<CallToolResult> {
20+
try {
21+
return await createProjectOrFolder(args);
22+
} catch (error) {
23+
const msg = error instanceof Error ? error.message : "Unknown error";
24+
return {
25+
content: [
26+
{
27+
type: "text",
28+
text: `Failed to create project/folder: ${msg}`,
29+
isError: true,
30+
},
31+
],
32+
isError: true,
33+
};
34+
}
35+
}
36+
1037
/**
1138
* Creates a test case in BrowserStack Test Management.
1239
*/
@@ -47,20 +74,30 @@ export async function createTestCaseTool(
4774
}
4875
}
4976

77+
/**
78+
* Registers both project/folder and test-case tools.
79+
*/
5080
export default function addTestManagementTools(server: McpServer) {
81+
server.tool(
82+
"createProjectOrFolder",
83+
"Create a project and/or folder in BrowserStack Test Management.",
84+
CreateProjFoldSchema.shape,
85+
createProjectOrFolderTool,
86+
);
87+
5188
server.tool(
5289
"createTestCase",
5390
"Use this tool to create a test case in BrowserStack Test Management.",
5491
{
5592
project_identifier: z
5693
.string()
5794
.describe(
58-
"The ID of the BrowserStack project in which to create the test case.",
95+
"The ID of the BrowserStack project in which to create the test case. Ask User if he want to create a new project if no project ID is provided using createProjectOrFolder tool.",
5996
),
6097
folder_id: z
6198
.string()
6299
.describe(
63-
"The ID of the folder under the project to create the test case in.",
100+
"The ID of the folder under the project to create the test case in. If omitted, Ask user if he wants to create a new folder createProjectOrFolder tool.",
64101
),
65102
name: z.string().describe("Name of the test case."),
66103
description: z
@@ -91,14 +128,16 @@ export default function addTestManagementTools(server: McpServer) {
91128
.array(z.string())
92129
.optional()
93130
.nullish()
94-
.describe("List of the linked Jira, Asana or Azure issues ID's."),
131+
.describe(
132+
"List of the linked Jira, Asana or Azure issues ID's. This should be strictly in array format not the string of json.",
133+
),
95134
issue_tracker: z
96135
.object({
97136
name: z
98137
.string()
99138
.nullish()
100139
.describe(
101-
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.​",
140+
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana ​",
102141
),
103142
host: z
104143
.string()
@@ -111,7 +150,9 @@ export default function addTestManagementTools(server: McpServer) {
111150
.array(z.string())
112151
.optional()
113152
.nullish()
114-
.describe("Tags to attach to the test case."),
153+
.describe(
154+
"Tags to attach to the test case. This should be strictly in array format not the string of json",
155+
),
115156
custom_fields: z
116157
.record(z.string(), z.string())
117158
.optional()

tests/tools/testmanagement.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ jest.mock('../../src/tools/testmanagement-utils/create-testcase', () => ({
66
createTestCase: jest.fn(),
77
sanitizeArgs: jest.fn((args) => args),
88
}));
9+
jest.mock('../../src/config', () => ({
10+
__esModule: true,
11+
default: {
12+
browserstackUsername: 'fake-user',
13+
browserstackAccessKey: 'fake-key',
14+
},
15+
}));
916

1017
describe('createTestCaseTool', () => {
1118
beforeEach(() => {

0 commit comments

Comments
 (0)