Skip to content
Merged
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
1,102 changes: 61 additions & 1,041 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@browserstack/mcp-server",
"version": "1.2.7",
"version": "1.2.8",
"description": "BrowserStack's Official MCP Server",
"mcpName": "io.github.browserstack/mcp-server",
"main": "dist/index.js",
Expand Down
52 changes: 52 additions & 0 deletions src/lib/tm-base-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { apiClient } from "./apiClient.js";
import logger from "../logger.js";
import { BrowserStackConfig } from "./types.js";
import { getBrowserStackAuth } from "./get-auth.js";

const TM_BASE_URLS = [
"https://test-management.browserstack.com",
"https://test-management-eu.browserstack.com",
"https://test-management-in.browserstack.com",
] as const;

let cachedBaseUrl: string | null = null;

export async function getTMBaseURL(
config: BrowserStackConfig,
): Promise<string> {
if (cachedBaseUrl) {
logger.debug(`Using cached TM base URL: ${cachedBaseUrl}`);
return cachedBaseUrl;
}

logger.info(
"No cached TM base URL found, testing available URLs with authentication",
);

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

for (const baseUrl of TM_BASE_URLS) {
try {
const res = await apiClient.get({
url: `${baseUrl}/api/v2/projects/`,
headers: { Authorization: authHeader },
raise_error: false,
});

if (res.ok) {
cachedBaseUrl = baseUrl;
logger.info(`Selected TM base URL: ${baseUrl}`);
return baseUrl;
}
} catch (err) {
logger.debug(`Failed TM base URL: ${baseUrl} (${err})`);
}
}

throw new Error(
"Unable to connect to BrowserStack Test Management. Please check your credentials and network connection.Please open an issue on GitHub if the problem persists",
);
}
30 changes: 21 additions & 9 deletions src/tools/testmanagement-utils/TCG-utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { createTestCasePayload } from "./helpers.js";
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
import { BrowserStackConfig } from "../../../lib/types.js";
import { getTMBaseURL } from "../../../lib/tm-base-url.js";

/**
* Fetch default and custom form fields for a project.
Expand All @@ -22,8 +23,9 @@ export async function fetchFormFields(
projectId: string,
config: BrowserStackConfig,
): Promise<{ default_fields: any; custom_fields: any }> {
const tmBaseUrl = await getTMBaseURL(config);
const res = await apiClient.get({
url: FORM_FIELDS_URL(projectId),
url: FORM_FIELDS_URL(tmBaseUrl, projectId),
headers: {
"API-TOKEN": getBrowserStackAuth(config),
},
Expand All @@ -42,8 +44,9 @@ export async function triggerTestCaseGeneration(
source: string,
config: BrowserStackConfig,
): Promise<string> {
const tmBaseUrl = await getTMBaseURL(config);
const res = await apiClient.post({
url: TCG_TRIGGER_URL,
url: TCG_TRIGGER_URL(tmBaseUrl),
headers: {
"API-TOKEN": getBrowserStackAuth(config),
"Content-Type": "application/json",
Expand All @@ -55,7 +58,7 @@ export async function triggerTestCaseGeneration(
folderId,
projectId,
source,
webhookUrl: `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
webhookUrl: `${tmBaseUrl}/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
},
});
if (res.status !== 200) {
Expand All @@ -78,8 +81,9 @@ export async function fetchTestCaseDetails(
if (testCaseIds.length === 0) {
throw new Error("No testCaseIds provided to fetchTestCaseDetails");
}
const tmBaseUrl = await getTMBaseURL(config);
const res = await apiClient.post({
url: FETCH_DETAILS_URL,
url: FETCH_DETAILS_URL(tmBaseUrl),
headers: {
"API-TOKEN": getBrowserStackAuth(config),
"request-source": source,
Expand Down Expand Up @@ -107,13 +111,15 @@ export async function pollTestCaseDetails(
): Promise<Record<string, any>> {
const detailMap: Record<string, any> = {};
let done = false;
const tmBaseUrl = await getTMBaseURL(config);
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);

while (!done) {
// add a bit of jitter to avoid synchronized polling storms
await new Promise((r) => setTimeout(r, 10000 + Math.random() * 5000));

const poll = await apiClient.post({
url: `${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
headers: {
"API-TOKEN": getBrowserStackAuth(config),
},
Expand Down Expand Up @@ -157,13 +163,15 @@ export async function pollScenariosTestDetails(
const scenariosMap: Record<string, Scenario> = {};
const detailPromises: Promise<Record<string, any>>[] = [];
let iteratorCount = 0;
const tmBaseUrl = await getTMBaseURL(config);
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);

// Promisify interval-style polling using a wrapper
await new Promise<void>((resolve, reject) => {
const intervalId = setInterval(async () => {
try {
const poll = await apiClient.post({
url: `${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
headers: {
"API-TOKEN": getBrowserStackAuth(config),
},
Expand Down Expand Up @@ -279,6 +287,8 @@ export async function bulkCreateTestCases(
const total = Object.keys(scenariosMap).length;
let doneCount = 0;
let testCaseCount = 0;
const tmBaseUrl = await getTMBaseURL(config);
const BULK_CREATE_URL_VALUE = BULK_CREATE_URL(tmBaseUrl, projectId, folderId);

for (const { id, testcases } of Object.values(scenariosMap)) {
const testCaseLength = testcases.length;
Expand All @@ -300,7 +310,7 @@ export async function bulkCreateTestCases(

try {
const resp = await apiClient.post({
url: BULK_CREATE_URL(projectId, folderId),
url: BULK_CREATE_URL_VALUE,
headers: {
"API-TOKEN": getBrowserStackAuth(config),
"Content-Type": "application/json",
Expand Down Expand Up @@ -341,7 +351,8 @@ export async function projectIdentifierToId(
projectId: string,
config: BrowserStackConfig,
): Promise<string> {
const url = `https://test-management.browserstack.com/api/v1/projects/?q=${projectId}`;
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v1/projects/?q=${projectId}`;

const response = await apiClient.get({
url,
Expand All @@ -368,7 +379,8 @@ export async function testCaseIdentifierToDetails(
testCaseIdentifier: string,
config: BrowserStackConfig,
): Promise<{ testCaseId: string; folderId: string }> {
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;

const response = await apiClient.get({
url,
Expand Down
28 changes: 18 additions & 10 deletions src/tools/testmanagement-utils/TCG-utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
export const TCG_TRIGGER_URL =
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/suggest-test-cases";
export const TCG_POLL_URL =
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/test-cases-polling";
export const FETCH_DETAILS_URL =
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/fetch-test-case-details";
export const FORM_FIELDS_URL = (projectId: string): string =>
`https://test-management.browserstack.com/api/v1/projects/${projectId}/form-fields-v2`;
export const BULK_CREATE_URL = (projectId: string, folderId: string): string =>
`https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
export const TCG_TRIGGER_URL = (baseUrl: string) =>
`${baseUrl}/api/v1/integration/tcg/test-generation/suggest-test-cases`;

export const TCG_POLL_URL = (baseUrl: string) =>
`${baseUrl}/api/v1/integration/tcg/test-generation/test-cases-polling`;

export const FETCH_DETAILS_URL = (baseUrl: string) =>
`${baseUrl}/api/v1/integration/tcg/test-generation/fetch-test-case-details`;

export const FORM_FIELDS_URL = (baseUrl: string, projectId: string) =>
`${baseUrl}/api/v1/projects/${projectId}/form-fields-v2`;

export const BULK_CREATE_URL = (
baseUrl: string,
projectId: string,
folderId: string,
) =>
`${baseUrl}/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
4 changes: 3 additions & 1 deletion src/tools/testmanagement-utils/add-test-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getBrowserStackAuth } from "../../lib/get-auth.js";
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

/**
* Schema for adding a test result to a test run.
Expand Down Expand Up @@ -37,7 +38,8 @@ export async function addTestResult(
): Promise<CallToolResult> {
try {
const args = AddTestResultSchema.parse(rawArgs);
const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-runs/${encodeURIComponent(args.test_run_id)}/results`;

Expand Down
6 changes: 4 additions & 2 deletions src/tools/testmanagement-utils/create-lca-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { pollLCAStatus } from "./poll-lca-status.js";
import { getBrowserStackAuth } from "../../lib/get-auth.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

/**
* Schema for creating LCA steps for a test case
Expand Down Expand Up @@ -81,7 +82,8 @@ export async function createLCASteps(
config,
);

const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;

const payload = {
base_url: args.base_url,
Expand All @@ -90,7 +92,7 @@ export async function createLCASteps(
test_name: args.test_name,
test_case_details: args.test_case_details,
version: "v2",
webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
webhook_path: `${tmBaseUrl}/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
};

await apiClient.post({
Expand Down
9 changes: 6 additions & 3 deletions src/tools/testmanagement-utils/create-project-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { formatAxiosError } from "../../lib/error.js";
import { projectIdentifierToId } from "../testmanagement-utils/TCG-utils/api.js";
import { getBrowserStackAuth } from "../../lib/get-auth.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

// Schema for combined project/folder creation
export const CreateProjFoldSchema = z.object({
Expand Down Expand Up @@ -65,8 +66,9 @@ export async function createProjectOrFolder(
try {
const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");
const tmBaseUrl = await getTMBaseURL(config);
const res = await apiClient.post({
url: "https://test-management.browserstack.com/api/v2/projects",
url: `${tmBaseUrl}/api/v2/projects`,
headers: {
"Content-Type": "application/json",
Authorization:
Expand Down Expand Up @@ -95,8 +97,9 @@ export async function createProjectOrFolder(
if (!projId)
throw new Error("Cannot create folder without project_identifier.");
try {
const tmBaseUrl = await getTMBaseURL(config);
const res = await apiClient.post({
url: `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
projId,
)}/folders`,
headers: {
Expand Down Expand Up @@ -130,7 +133,7 @@ export async function createProjectOrFolder(
- ID: ${folder.id}
- Name: ${folder.name}
- Project Identifier: ${projId}
Access it here: https://test-management.browserstack.com/projects/${projectId}/folder/${folder.id}/`,
Access it here: ${tmBaseUrl}/projects/${projectId}/folder/${folder.id}/`,
},
],
};
Expand Down
6 changes: 4 additions & 2 deletions src/tools/testmanagement-utils/create-testcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { formatAxiosError } from "../../lib/error.js";
import { projectIdentifierToId } from "./TCG-utils/api.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

interface TestCaseStep {
step: string;
Expand Down Expand Up @@ -157,8 +158,9 @@ export async function createTestCase(
const [username, password] = authString.split(":");

try {
const tmBaseUrl = await getTMBaseURL(config);
const response = await apiClient.post({
url: `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
params.project_identifier,
)}/folders/${encodeURIComponent(params.folder_id)}/test-cases`,
headers: {
Expand Down Expand Up @@ -199,7 +201,7 @@ export async function createTestCase(
- Identifier: ${tc.identifier}
- Title: ${tc.title}

You can view it here: https://test-management.browserstack.com/projects/${projectId}/folder/search?q=${tc.identifier}`,
You can view it here: ${tmBaseUrl}/projects/${projectId}/folder/search?q=${tc.identifier}`,
},
{
type: "text",
Expand Down
4 changes: 3 additions & 1 deletion src/tools/testmanagement-utils/create-testrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 creating a test run.
Expand Down Expand Up @@ -66,7 +67,8 @@ export async function createTestRun(
};
const args = CreateTestRunSchema.parse(inputArgs);

const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-runs`;

Expand Down
4 changes: 3 additions & 1 deletion src/tools/testmanagement-utils/list-testcases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 listing test cases with optional filters.
Expand Down Expand Up @@ -49,7 +50,8 @@ export async function listTestCases(
if (args.priority) params.append("priority", args.priority);
if (args.p !== undefined) params.append("p", args.p.toString());

const url = `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-cases?${params.toString()}`;

Expand Down
4 changes: 3 additions & 1 deletion src/tools/testmanagement-utils/list-testruns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 listing test runs with optional filters.
Expand Down Expand Up @@ -35,8 +36,9 @@ export async function listTestRuns(
params.set("run_state", args.run_state);
}

const tmBaseUrl = await getTMBaseURL(config);
const url =
`https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
`${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
args.project_identifier,
)}/test-runs?` + params.toString();

Expand Down
4 changes: 3 additions & 1 deletion src/tools/testmanagement-utils/poll-lca-status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { apiClient } from "../../lib/apiClient.js";
import { getBrowserStackAuth } from "../../lib/get-auth.js";
import { BrowserStackConfig } from "../../lib/types.js";
import { getTMBaseURL } from "../../lib/tm-base-url.js";

/**
* Interface for the test case response structure
Expand Down Expand Up @@ -39,7 +40,8 @@ export async function pollLCAStatus(
pollIntervalMs: number = 10 * 1000, // 10 seconds interval
config: BrowserStackConfig,
): Promise<{ resource_path: string; status: string } | null> {
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/test-cases/${testCaseId}`;
const tmBaseUrl = await getTMBaseURL(config);
const url = `${tmBaseUrl}/api/v1/projects/${projectId}/folder/${folderId}/test-cases/${testCaseId}`;

const startTime = Date.now();

Expand Down
Loading
Loading