Skip to content

Commit db791e9

Browse files
Feat : test management tools and create test case functionality
1 parent 73c7030 commit db791e9

File tree

4 files changed

+332
-0
lines changed

4 files changed

+332
-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
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import axios, { AxiosError } from "axios";
2+
import config from "../../config";
3+
import logger from "./../../logger";
4+
5+
interface TestCaseStep {
6+
step: string;
7+
result: string;
8+
}
9+
10+
interface IssueTracker {
11+
name: string;
12+
host: string;
13+
}
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+
}
32+
33+
export interface TestCaseCreateRequest {
34+
project_identifier: string;
35+
folder_id: string;
36+
name: string;
37+
description?: string;
38+
owner?: string;
39+
preconditions?: string;
40+
test_case_steps: TestCaseStep[];
41+
issues?: string[];
42+
issue_tracker?: IssueTracker;
43+
tags?: string[];
44+
custom_fields?: Record<string, string>;
45+
}
46+
47+
export interface TestCaseResponse {
48+
data: {
49+
success: boolean;
50+
test_case: {
51+
case_type: string;
52+
priority: string;
53+
status: string;
54+
folder_id: number;
55+
issues: Array<{
56+
jira_id: string;
57+
issue_type: string;
58+
}>;
59+
tags: string[];
60+
template: string;
61+
description: string;
62+
preconditions: string;
63+
title: string;
64+
identifier: string;
65+
automation_status: string;
66+
owner: string;
67+
steps: TestCaseStep[];
68+
custom_fields: Array<{
69+
name: string;
70+
value: string;
71+
}>;
72+
};
73+
};
74+
}
75+
76+
export async function createTestCase(
77+
params: TestCaseCreateRequest,
78+
): Promise<TestCaseResponse> {
79+
const {
80+
project_identifier,
81+
folder_id,
82+
name,
83+
description,
84+
owner,
85+
preconditions,
86+
test_case_steps,
87+
issues,
88+
issue_tracker,
89+
tags,
90+
custom_fields,
91+
} = params;
92+
93+
const body = {
94+
test_case: {
95+
name,
96+
description,
97+
owner,
98+
preconditions,
99+
test_case_steps,
100+
issues,
101+
issue_tracker,
102+
tags,
103+
custom_fields,
104+
},
105+
};
106+
107+
try {
108+
const response = await axios.post<TestCaseResponse>(
109+
`https://test-management.browserstack.com/api/v2/projects/${project_identifier}/folders/${folder_id}/test-cases`,
110+
body,
111+
{
112+
auth: {
113+
username: config.browserstackUsername,
114+
password: config.browserstackAccessKey,
115+
},
116+
headers: {
117+
"Content-Type": "application/json",
118+
},
119+
},
120+
);
121+
return response.data;
122+
} catch (error) {
123+
if (error instanceof AxiosError) {
124+
if (error.response?.data?.message) {
125+
throw new Error(
126+
`Failed to create test case: ${error.response.data.message}`,
127+
);
128+
} else {
129+
throw new Error(`Failed to create test case: ${error.message}`);
130+
}
131+
}
132+
throw error;
133+
}
134+
}

src/tools/testmanagement.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import {
5+
createTestCase as createTestCaseAPI,
6+
TestCaseCreateRequest,
7+
sanitizeArgs,
8+
} from "./testmanagement-utils/create-testcase";
9+
10+
/**
11+
* Creates a test case in BrowserStack Test Management.
12+
*/
13+
export async function createTestCaseTool(
14+
args: TestCaseCreateRequest,
15+
): Promise<CallToolResult> {
16+
// Sanitize input arguments
17+
const cleanedArgs = sanitizeArgs(args);
18+
try {
19+
const response = await createTestCaseAPI(cleanedArgs);
20+
const testCase = response.data.test_case;
21+
22+
return {
23+
content: [
24+
{
25+
type: "text",
26+
text: `Successfully created test case ${testCase.identifier}: "${testCase.title}"`,
27+
},
28+
{
29+
type: "text",
30+
text: JSON.stringify(testCase, null, 2),
31+
},
32+
],
33+
};
34+
} catch (error) {
35+
return {
36+
content: [
37+
{
38+
type: "text",
39+
text: `Failed to create test case: ${
40+
error instanceof Error ? error.message : "Unknown error"
41+
}. Please open an issue on GitHub if the problem persists`,
42+
isError: true,
43+
},
44+
],
45+
isError: true,
46+
};
47+
}
48+
}
49+
50+
export default function addTestManagementTools(server: McpServer) {
51+
server.tool(
52+
"createTestCase",
53+
"Use this tool to create a test case in BrowserStack Test Management.",
54+
{
55+
project_identifier: z
56+
.string()
57+
.describe(
58+
"The ID of the BrowserStack project in which to create the test case.",
59+
),
60+
folder_id: z
61+
.string()
62+
.describe(
63+
"The ID of the folder under the project to create the test case in.",
64+
),
65+
name: z.string().describe("Name of the test case."),
66+
description: z
67+
.string()
68+
.optional()
69+
.nullish()
70+
.describe("Brief description of the test case."),
71+
owner: z
72+
.string()
73+
.email()
74+
.describe("Email of the test case owner.")
75+
.optional()
76+
.nullish(),
77+
preconditions: z
78+
.string()
79+
.optional()
80+
.nullish()
81+
.describe("Any preconditions (HTML allowed)."),
82+
test_case_steps: z
83+
.array(
84+
z.object({
85+
step: z.string().describe("Action to perform in this step."),
86+
result: z.string().describe("Expected result of this step."),
87+
}),
88+
)
89+
.describe("List of steps and expected results."),
90+
issues: z
91+
.array(z.string())
92+
.optional()
93+
.nullish()
94+
.describe("List of the linked Jira, Asana or Azure issues ID's."),
95+
issue_tracker: z
96+
.object({
97+
name: z
98+
.string()
99+
.nullish()
100+
.describe(
101+
"Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.​",
102+
),
103+
host: z
104+
.string()
105+
.url()
106+
.describe("Base URL of the issue tracker.")
107+
.nullish(),
108+
})
109+
.optional(),
110+
tags: z
111+
.array(z.string())
112+
.optional()
113+
.nullish()
114+
.describe("Tags to attach to the test case."),
115+
custom_fields: z
116+
.record(z.string(), z.string())
117+
.optional()
118+
.nullish()
119+
.describe("Map of custom field names to values."),
120+
},
121+
async (args) => {
122+
return createTestCaseTool(args as TestCaseCreateRequest);
123+
},
124+
);
125+
}

tests/tools/testmanagement.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createTestCaseTool } from '../../src/tools/testmanagement';
2+
import { createTestCase, sanitizeArgs, TestCaseCreateRequest } from '../../src/tools/testmanagement-utils/create-testcase';
3+
4+
// Mock the dependencies
5+
jest.mock('../../src/tools/testmanagement-utils/create-testcase', () => ({
6+
createTestCase: jest.fn(),
7+
sanitizeArgs: jest.fn((args) => args),
8+
}));
9+
10+
describe('createTestCaseTool', () => {
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
});
14+
15+
const validArgs: TestCaseCreateRequest = {
16+
project_identifier: 'proj-123',
17+
folder_id: 'fold-456',
18+
name: 'Sample Test Case',
19+
description: 'Test case description',
20+
21+
preconditions: 'Some precondition',
22+
test_case_steps: [
23+
{ step: 'Step 1', result: 'Result 1' },
24+
{ step: 'Step 2', result: 'Result 2' },
25+
],
26+
issues: ['JIRA-1'],
27+
issue_tracker: { name: 'jira', host: 'https://jira.example.com' },
28+
tags: ['smoke'],
29+
custom_fields: { priority: 'high' },
30+
};
31+
32+
const mockResponse = {
33+
data: {
34+
success: true,
35+
test_case: {
36+
identifier: 'TC-001',
37+
title: 'Sample Test Case',
38+
// additional fields omitted for brevity
39+
},
40+
},
41+
};
42+
43+
it('should successfully create a test case', async () => {
44+
(createTestCase as jest.Mock).mockResolvedValue(mockResponse);
45+
46+
const result = await createTestCaseTool(validArgs);
47+
48+
expect(sanitizeArgs).toHaveBeenCalledWith(validArgs);
49+
expect(createTestCase).toHaveBeenCalledWith(validArgs);
50+
expect(result.content[0].text).toContain('Successfully created test case TC-001');
51+
expect(result.content[1].text).toContain('"title": "Sample Test Case"');
52+
});
53+
54+
it('should handle API errors gracefully', async () => {
55+
(createTestCase as jest.Mock).mockRejectedValue(new Error('API Error'));
56+
57+
const result = await createTestCaseTool(validArgs);
58+
59+
expect(result.isError).toBe(true);
60+
expect(result.content[0].text).toContain('Failed to create test case: API Error');
61+
});
62+
63+
it('should handle unknown error types', async () => {
64+
(createTestCase as jest.Mock).mockRejectedValue('unexpected');
65+
66+
const result = await createTestCaseTool(validArgs);
67+
68+
expect(result.isError).toBe(true);
69+
expect(result.content[0].text).toContain('Unknown error');
70+
});
71+
});

0 commit comments

Comments
 (0)