Skip to content

Commit 8bd98ce

Browse files
Merge pull request #181 from tech-sushant/tm-regions
feat: refactor TM API URLs to use dynamic base URL retrieval
2 parents 8912c86 + c360994 commit 8bd98ce

16 files changed

+191
-1076
lines changed

package-lock.json

Lines changed: 61 additions & 1041 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@browserstack/mcp-server",
3-
"version": "1.2.7",
3+
"version": "1.2.8",
44
"description": "BrowserStack's Official MCP Server",
55
"mcpName": "io.github.browserstack/mcp-server",
66
"main": "dist/index.js",

src/lib/tm-base-url.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { apiClient } from "./apiClient.js";
2+
import logger from "../logger.js";
3+
import { BrowserStackConfig } from "./types.js";
4+
import { getBrowserStackAuth } from "./get-auth.js";
5+
6+
const TM_BASE_URLS = [
7+
"https://test-management.browserstack.com",
8+
"https://test-management-eu.browserstack.com",
9+
"https://test-management-in.browserstack.com",
10+
] as const;
11+
12+
let cachedBaseUrl: string | null = null;
13+
14+
export async function getTMBaseURL(
15+
config: BrowserStackConfig,
16+
): Promise<string> {
17+
if (cachedBaseUrl) {
18+
logger.debug(`Using cached TM base URL: ${cachedBaseUrl}`);
19+
return cachedBaseUrl;
20+
}
21+
22+
logger.info(
23+
"No cached TM base URL found, testing available URLs with authentication",
24+
);
25+
26+
const authString = getBrowserStackAuth(config);
27+
const [username, password] = authString.split(":");
28+
const authHeader =
29+
"Basic " + Buffer.from(`${username}:${password}`).toString("base64");
30+
31+
for (const baseUrl of TM_BASE_URLS) {
32+
try {
33+
const res = await apiClient.get({
34+
url: `${baseUrl}/api/v2/projects/`,
35+
headers: { Authorization: authHeader },
36+
raise_error: false,
37+
});
38+
39+
if (res.ok) {
40+
cachedBaseUrl = baseUrl;
41+
logger.info(`Selected TM base URL: ${baseUrl}`);
42+
return baseUrl;
43+
}
44+
} catch (err) {
45+
logger.debug(`Failed TM base URL: ${baseUrl} (${err})`);
46+
}
47+
}
48+
49+
throw new Error(
50+
"Unable to connect to BrowserStack Test Management. Please check your credentials and network connection.Please open an issue on GitHub if the problem persists",
51+
);
52+
}

src/tools/testmanagement-utils/TCG-utils/api.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { createTestCasePayload } from "./helpers.js";
1515
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
1616
import { BrowserStackConfig } from "../../../lib/types.js";
17+
import { getTMBaseURL } from "../../../lib/tm-base-url.js";
1718

1819
/**
1920
* Fetch default and custom form fields for a project.
@@ -22,8 +23,9 @@ export async function fetchFormFields(
2223
projectId: string,
2324
config: BrowserStackConfig,
2425
): Promise<{ default_fields: any; custom_fields: any }> {
26+
const tmBaseUrl = await getTMBaseURL(config);
2527
const res = await apiClient.get({
26-
url: FORM_FIELDS_URL(projectId),
28+
url: FORM_FIELDS_URL(tmBaseUrl, projectId),
2729
headers: {
2830
"API-TOKEN": getBrowserStackAuth(config),
2931
},
@@ -42,8 +44,9 @@ export async function triggerTestCaseGeneration(
4244
source: string,
4345
config: BrowserStackConfig,
4446
): Promise<string> {
47+
const tmBaseUrl = await getTMBaseURL(config);
4548
const res = await apiClient.post({
46-
url: TCG_TRIGGER_URL,
49+
url: TCG_TRIGGER_URL(tmBaseUrl),
4750
headers: {
4851
"API-TOKEN": getBrowserStackAuth(config),
4952
"Content-Type": "application/json",
@@ -55,7 +58,7 @@ export async function triggerTestCaseGeneration(
5558
folderId,
5659
projectId,
5760
source,
58-
webhookUrl: `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
61+
webhookUrl: `${tmBaseUrl}/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
5962
},
6063
});
6164
if (res.status !== 200) {
@@ -78,8 +81,9 @@ export async function fetchTestCaseDetails(
7881
if (testCaseIds.length === 0) {
7982
throw new Error("No testCaseIds provided to fetchTestCaseDetails");
8083
}
84+
const tmBaseUrl = await getTMBaseURL(config);
8185
const res = await apiClient.post({
82-
url: FETCH_DETAILS_URL,
86+
url: FETCH_DETAILS_URL(tmBaseUrl),
8387
headers: {
8488
"API-TOKEN": getBrowserStackAuth(config),
8589
"request-source": source,
@@ -107,13 +111,15 @@ export async function pollTestCaseDetails(
107111
): Promise<Record<string, any>> {
108112
const detailMap: Record<string, any> = {};
109113
let done = false;
114+
const tmBaseUrl = await getTMBaseURL(config);
115+
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
110116

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

115121
const poll = await apiClient.post({
116-
url: `${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
122+
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
117123
headers: {
118124
"API-TOKEN": getBrowserStackAuth(config),
119125
},
@@ -157,13 +163,15 @@ export async function pollScenariosTestDetails(
157163
const scenariosMap: Record<string, Scenario> = {};
158164
const detailPromises: Promise<Record<string, any>>[] = [];
159165
let iteratorCount = 0;
166+
const tmBaseUrl = await getTMBaseURL(config);
167+
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
160168

161169
// Promisify interval-style polling using a wrapper
162170
await new Promise<void>((resolve, reject) => {
163171
const intervalId = setInterval(async () => {
164172
try {
165173
const poll = await apiClient.post({
166-
url: `${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
174+
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
167175
headers: {
168176
"API-TOKEN": getBrowserStackAuth(config),
169177
},
@@ -279,6 +287,8 @@ export async function bulkCreateTestCases(
279287
const total = Object.keys(scenariosMap).length;
280288
let doneCount = 0;
281289
let testCaseCount = 0;
290+
const tmBaseUrl = await getTMBaseURL(config);
291+
const BULK_CREATE_URL_VALUE = BULK_CREATE_URL(tmBaseUrl, projectId, folderId);
282292

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

301311
try {
302312
const resp = await apiClient.post({
303-
url: BULK_CREATE_URL(projectId, folderId),
313+
url: BULK_CREATE_URL_VALUE,
304314
headers: {
305315
"API-TOKEN": getBrowserStackAuth(config),
306316
"Content-Type": "application/json",
@@ -341,7 +351,8 @@ export async function projectIdentifierToId(
341351
projectId: string,
342352
config: BrowserStackConfig,
343353
): Promise<string> {
344-
const url = `https://test-management.browserstack.com/api/v1/projects/?q=${projectId}`;
354+
const tmBaseUrl = await getTMBaseURL(config);
355+
const url = `${tmBaseUrl}/api/v1/projects/?q=${projectId}`;
345356

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

373385
const response = await apiClient.get({
374386
url,
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
export const TCG_TRIGGER_URL =
2-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/suggest-test-cases";
3-
export const TCG_POLL_URL =
4-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/test-cases-polling";
5-
export const FETCH_DETAILS_URL =
6-
"https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/fetch-test-case-details";
7-
export const FORM_FIELDS_URL = (projectId: string): string =>
8-
`https://test-management.browserstack.com/api/v1/projects/${projectId}/form-fields-v2`;
9-
export const BULK_CREATE_URL = (projectId: string, folderId: string): string =>
10-
`https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
1+
export const TCG_TRIGGER_URL = (baseUrl: string) =>
2+
`${baseUrl}/api/v1/integration/tcg/test-generation/suggest-test-cases`;
3+
4+
export const TCG_POLL_URL = (baseUrl: string) =>
5+
`${baseUrl}/api/v1/integration/tcg/test-generation/test-cases-polling`;
6+
7+
export const FETCH_DETAILS_URL = (baseUrl: string) =>
8+
`${baseUrl}/api/v1/integration/tcg/test-generation/fetch-test-case-details`;
9+
10+
export const FORM_FIELDS_URL = (baseUrl: string, projectId: string) =>
11+
`${baseUrl}/api/v1/projects/${projectId}/form-fields-v2`;
12+
13+
export const BULK_CREATE_URL = (
14+
baseUrl: string,
15+
projectId: string,
16+
folderId: string,
17+
) =>
18+
`${baseUrl}/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;

src/tools/testmanagement-utils/add-test-result.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getBrowserStackAuth } from "../../lib/get-auth.js";
33
import { z } from "zod";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55
import { BrowserStackConfig } from "../../lib/types.js";
6+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
67

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

src/tools/testmanagement-utils/create-lca-steps.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { pollLCAStatus } from "./poll-lca-status.js";
99
import { getBrowserStackAuth } from "../../lib/get-auth.js";
1010
import { BrowserStackConfig } from "../../lib/types.js";
11+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
1112

1213
/**
1314
* Schema for creating LCA steps for a test case
@@ -81,7 +82,8 @@ export async function createLCASteps(
8182
config,
8283
);
8384

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

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

9698
await apiClient.post({

src/tools/testmanagement-utils/create-project-folder.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { formatAxiosError } from "../../lib/error.js";
55
import { projectIdentifierToId } from "../testmanagement-utils/TCG-utils/api.js";
66
import { getBrowserStackAuth } from "../../lib/get-auth.js";
77
import { BrowserStackConfig } from "../../lib/types.js";
8+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
89

910
// Schema for combined project/folder creation
1011
export const CreateProjFoldSchema = z.object({
@@ -65,8 +66,9 @@ export async function createProjectOrFolder(
6566
try {
6667
const authString = getBrowserStackAuth(config);
6768
const [username, password] = authString.split(":");
69+
const tmBaseUrl = await getTMBaseURL(config);
6870
const res = await apiClient.post({
69-
url: "https://test-management.browserstack.com/api/v2/projects",
71+
url: `${tmBaseUrl}/api/v2/projects`,
7072
headers: {
7173
"Content-Type": "application/json",
7274
Authorization:
@@ -95,8 +97,9 @@ export async function createProjectOrFolder(
9597
if (!projId)
9698
throw new Error("Cannot create folder without project_identifier.");
9799
try {
100+
const tmBaseUrl = await getTMBaseURL(config);
98101
const res = await apiClient.post({
99-
url: `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
102+
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
100103
projId,
101104
)}/folders`,
102105
headers: {
@@ -130,7 +133,7 @@ export async function createProjectOrFolder(
130133
- ID: ${folder.id}
131134
- Name: ${folder.name}
132135
- Project Identifier: ${projId}
133-
Access it here: https://test-management.browserstack.com/projects/${projectId}/folder/${folder.id}/`,
136+
Access it here: ${tmBaseUrl}/projects/${projectId}/folder/${folder.id}/`,
134137
},
135138
],
136139
};

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import { projectIdentifierToId } from "./TCG-utils/api.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
78

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

159160
try {
161+
const tmBaseUrl = await getTMBaseURL(config);
160162
const response = await apiClient.post({
161-
url: `https://test-management.browserstack.com/api/v2/projects/${encodeURIComponent(
163+
url: `${tmBaseUrl}/api/v2/projects/${encodeURIComponent(
162164
params.project_identifier,
163165
)}/folders/${encodeURIComponent(params.folder_id)}/test-cases`,
164166
headers: {
@@ -199,7 +201,7 @@ export async function createTestCase(
199201
- Identifier: ${tc.identifier}
200202
- Title: ${tc.title}
201203
202-
You can view it here: https://test-management.browserstack.com/projects/${projectId}/folder/search?q=${tc.identifier}`,
204+
You can view it here: ${tmBaseUrl}/projects/${projectId}/folder/search?q=${tc.identifier}`,
203205
},
204206
{
205207
type: "text",

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import { getBrowserStackAuth } from "../../lib/get-auth.js";
66
import { BrowserStackConfig } from "../../lib/types.js";
7+
import { getTMBaseURL } from "../../lib/tm-base-url.js";
78

89
/**
910
* Schema for creating a test run.
@@ -66,7 +67,8 @@ export async function createTestRun(
6667
};
6768
const args = CreateTestRunSchema.parse(inputArgs);
6869

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

0 commit comments

Comments
 (0)