Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,040 changes: 663 additions & 2,377 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@browserstack/mcp-server",
"version": "1.2.12",
"version": "1.2.15",
"description": "BrowserStack's Official MCP Server",
"mcpName": "io.github.browserstack/mcp-server",
"main": "dist/index.js",
Expand Down Expand Up @@ -35,19 +35,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"@types/form-data": "^2.5.2",
"axios": "^1.13.2",
"browserstack-local": "^1.5.8",
"csv-parse": "^6.1.0",
"dotenv": "^17.2.3",
"axios": "^1.15.0",
"browserstack-local": "^1.5.12",
"csv-parse": "^6.2.1",
"dotenv": "^17.4.0",
"form-data": "^4.0.5",
"pino": "^10.1.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"sharp": "^0.34.5",
"uuid": "^13.0.0",
"webdriverio": "^9.21.0",
"zod": "^4.2.1"
"webdriverio": "^9.27.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
"registryType": "npm",
"identifier": "@browserstack/mcp-server",
"version": "1.2.12",
"version": "1.2.15",
"transport": {
"type": "stdio"
},
Expand Down
1 change: 1 addition & 0 deletions src/tools/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export default function addAppAutomationTools(
text: `Error during app automation or screenshot capture: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
13 changes: 12 additions & 1 deletion src/tools/automate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,17 @@ export async function fetchAutomationScreenshotsTool(
};
} catch (error) {
logger.error("Error during fetching screenshots", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error during fetching screenshots: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -99,6 +109,7 @@ export default function addAutomationTools(
text: `Error during fetching automate screenshots: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
14 changes: 13 additions & 1 deletion src/tools/rca-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function getBuildIdTool(
text: `Error fetching build ID: ${errorMessage}`,
},
],
isError: true,
};
}
}
Expand Down Expand Up @@ -83,7 +84,17 @@ export async function fetchRCADataTool(
};
} catch (error) {
logger.error("Error fetching RCA data", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error fetching RCA data: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -120,6 +131,7 @@ export async function listTestIdsTool(
text: `Error listing test IDs: ${errorMessage}`,
},
],
isError: true,
};
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/tools/selfheal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ export async function fetchSelfHealSelectorTool(
};
} catch (error) {
logger.error("Error fetching self-heal selector suggestions", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error fetching self-heal selector suggestions: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -67,6 +77,7 @@ export default function addSelfHealTools(
text: `Error during fetching self-heal suggestions: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
155 changes: 155 additions & 0 deletions src/tools/testmanagement-utils/get-testplan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { apiClient } from "../../lib/apiClient.js";
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { formatAxiosError } from "../../lib/error.js";
import { getBrowserStackAuth } from "../../lib/get-auth.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

/**
* Schema for fetching a single test plan by identifier, including its linked test runs.
*/
export const GetTestPlanSchema = z.object({
project_identifier: z
.string()
.describe(
"Identifier of the project (starts with PR- followed by a number).",
),
test_plan_identifier: z
.string()
.describe(
"Identifier of the test plan (starts with TP- followed by a number).",
),
});

export type GetTestPlanArgs = z.infer<typeof GetTestPlanSchema>;

interface TestPlan {
identifier: string;
name: string;
active_state: string;
description: string | null;
project_id: string;
start_date: string | null;
end_date: string | null;
created_at: string;
test_runs_count?: { active: number; closed: number };
test_runs?: Array<{ identifier: string; name: string }>;
links?: Record<string, string>;
}

interface LinkedTestRun {
identifier: string;
name: string;
run_state: string;
active_state: string;
assignee?: string | null;
description?: string | null;
created_at: string;
project_id: string;
test_cases_count: number;
}

/**
* Fetches a test plan by identifier and its linked test runs, returning a unified view
* suitable for generating documentation (metadata + linked runs + status summary + case count).
*/
export async function getTestPlan(
args: GetTestPlanArgs,
config: BrowserStackConfig,
): Promise<CallToolResult> {
try {
const tmBaseUrl = await getTMBaseURL(config);
const projectId = encodeURIComponent(args.project_identifier);
const planId = encodeURIComponent(args.test_plan_identifier);

const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");
const authHeader =
"Basic " + Buffer.from(`${username}:${password}`).toString("base64");

const planResp = await apiClient.get({
url: `${tmBaseUrl}/api/v2/projects/${projectId}/test-plans/${planId}`,
headers: { Authorization: authHeader },
});

if (!planResp.data?.success) {
return {
content: [
{
type: "text",
text: `Failed to fetch test plan: ${JSON.stringify(planResp.data)}`,
},
],
isError: true,
};
}

const plan: TestPlan = planResp.data.test_plan;

const runsResp = await apiClient.get({
url: `${tmBaseUrl}/api/v2/projects/${projectId}/test-plans/${planId}/test-runs`,
headers: { Authorization: authHeader },
});

const runs: LinkedTestRun[] = runsResp.data?.success
? (runsResp.data.test_runs ?? [])
: [];

const statusSummary: Record<string, number> = {};
let totalCases = 0;
for (const run of runs) {
statusSummary[run.run_state] = (statusSummary[run.run_state] ?? 0) + 1;
totalCases += run.test_cases_count ?? 0;
}

const header = [
`Test Plan ${plan.identifier}: ${plan.name}`,
`Status: ${plan.active_state}`,
plan.description ? `Description: ${plan.description}` : null,
plan.start_date || plan.end_date
? `Dates: ${plan.start_date ?? "—"} → ${plan.end_date ?? "—"}`
: null,
`Linked runs: ${runs.length} (plan counts — active ${plan.test_runs_count?.active ?? 0} / closed ${plan.test_runs_count?.closed ?? 0})`,
`Total test cases across runs: ${totalCases}`,
Object.keys(statusSummary).length > 0
? `Run-state breakdown: ${Object.entries(statusSummary)
.map(([s, n]) => `${s}=${n}`)
.join(", ")}`
: null,
]
.filter(Boolean)
.join("\n");

const runsBlock = runs.length
? "\n\nLinked test runs:\n" +
runs
.map(
(r) =>
`• ${r.identifier}: ${r.name} [${r.run_state}] — ${r.test_cases_count} case(s)${r.assignee ? ` (assignee: ${r.assignee})` : ""}`,
)
.join("\n")
: "\n\nNo test runs linked to this plan.";

return {
content: [
{ type: "text", text: header + runsBlock },
{
type: "text",
text: JSON.stringify(
{
test_plan: plan,
linked_test_runs: runs,
status_summary: statusSummary,
total_test_cases: totalCases,
},
null,
2,
),
},
],
};
} catch (err) {
return formatAxiosError(err, "Failed to fetch test plan");
}
}
Loading
Loading