Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
30ca137
Add the capability registry tool surface
sarve-shreyas Aug 19, 2026
a0f5a20
Wire the capability registry into the server factory
sarve-shreyas Aug 19, 2026
034b2c2
Resolve base_url the way Atlas does, and stop hardcoding the TM host
sarve-shreyas Aug 19, 2026
000229f
Support multiple environments for base_url, mirroring Atlas
sarve-shreyas Aug 19, 2026
43eba70
Return the product's response instead of interpreting it
sarve-shreyas Aug 19, 2026
2f3d194
Ask BrowserStack AI, and relay its permission asks to the human
sarve-shreyas Aug 24, 2026
5091263
Align to the verified Atlas response, and frame the prompt
sarve-shreyas Aug 24, 2026
2628aa4
Authenticate /agent the way /agent actually authenticates
sarve-shreyas Aug 24, 2026
57be2d7
Stop claiming the approval channel was used when nothing was asked
sarve-shreyas Aug 24, 2026
8584c13
Read the approval trail from Atlas instead of guessing at it
sarve-shreyas Aug 24, 2026
aebf512
Let the body decide whether a run happened, not the HTTP status
sarve-shreyas Aug 24, 2026
6026201
Sign in with a central JWT instead of a shared secret
sarve-shreyas Aug 24, 2026
df77700
Request ai_agent_notify, and fail loudly if it will not be issued
sarve-shreyas Aug 24, 2026
8067a0d
Let the approve button approve
sarve-shreyas Aug 25, 2026
9e07674
Ask nothing in the form, and let the action be the answer
sarve-shreyas Aug 25, 2026
20b2dcf
Let TM region discovery be pointed at a non-production environment
sarve-shreyas Aug 25, 2026
bc61dd9
Do not attempt the approval relay in the hosted deployment
sarve-shreyas Aug 26, 2026
420c20e
Ship the Atlas hosts, so an install needs a name and not a URL
sarve-shreyas Aug 26, 2026
ec58c62
Hardcode one staging host, and say loudly that it is temporary
sarve-shreyas Aug 26, 2026
2de13db
Tell the user when AI is not enabled for their account
sarve-shreyas Aug 26, 2026
3ec33ca
Take the capability registry out of this release
sarve-shreyas Aug 26, 2026
d6f5281
Say in the description that this is the fallback
sarve-shreyas Aug 26, 2026
792461f
feat(ask): A1 stream transport — nothing dials in
sarve-shreyas Aug 26, 2026
fb17e70
feat(ask): wire the tool onto A1 — no port, no callback, no NAT problem
sarve-shreyas Aug 26, 2026
9f39eec
fix(ask): stop claiming a person answered when nobody was there
sarve-shreyas Aug 26, 2026
ed04fa8
Stop reporting a 5xx from auth as rejected credentials
sarve-shreyas Aug 26, 2026
c307c32
Remove the A2 callback transport from the MCP half
sarve-shreyas Aug 26, 2026
a593ebc
Stop claiming Atlas route-checks the approval description
sarve-shreyas Aug 27, 2026
8b7f625
Merge origin/main into askrelay/central-mcp
sarve-shreyas Aug 27, 2026
3e16fab
feat(ask): the hosted deployment may offer the relay, when it opts in
sarve-shreyas Aug 27, 2026
49a30c9
fix(ask): route each elicitation onto the tool call's own stream
sarve-shreyas Aug 27, 2026
fa11a3a
askBrowserstackAI: point the compiled defaults at production
sarve-shreyas Aug 27, 2026
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
70 changes: 66 additions & 4 deletions src/lib/tm-base-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,88 @@ import { BrowserStackConfig } from "./types.js";
import { getBrowserStackAuth } from "./get-auth.js";
import appConfig from "../config.js";

const TM_BASE_URLS = [
/**
* The production regions, probed in this order. UNCHANGED, and the default in every
* deployment: the override below exists for test harnesses, not for shipping.
*/
export const TM_BASE_URLS = [
"https://test-management.browserstack.com",
"https://test-management-eu.browserstack.com",
"https://test-management-in.browserstack.com",
] as const;

/**
* A TEST-HARNESS AFFORDANCE. Point region discovery at a non-production environment.
*
* Plural because the probe loop takes a list. It REPLACES the built-in list rather than
* extending it — appending would leave the
* production hosts probed first, which is the whole problem it exists to avoid: preprod-only
* credentials 401 against production, and a 401 can send the model off to retry with a
* different tool, so a tool-selection measurement stops meaning what it says.
*
* An override that parses to nothing falls back to the built-in list rather than leaving an
* empty probe loop, which would surface as "unable to connect" with no detail. That fallback
* is a WARNING, not a silent one: quietly using production when someone asked for preprod is
* exactly the failure this is meant to prevent.
*/
export const TM_BASE_URLS_ENV = "BROWSERSTACK_TM_BASE_URLS";

export interface ResolvedBaseUrls {
urls: string[];
/** Where the list came from, so a run pointed at the wrong environment is visible. */
source: "built-in" | "env" | "built-in (override unusable)";
}

export function resolveTMBaseUrls(): ResolvedBaseUrls {
const raw = process.env[TM_BASE_URLS_ENV];
if (!raw || !raw.trim()) return { urls: [...TM_BASE_URLS], source: "built-in" };

const urls = raw
.split(",")
.map((entry) => entry.trim().replace(/\/+$/, ""))
// Anything without a scheme is a typo, not a host: silently probing it would produce a
// confusing connection error rather than naming the real mistake.
.filter((entry) => /^https?:\/\/\S+$/i.test(entry));

if (!urls.length) {
return { urls: [...TM_BASE_URLS], source: "built-in (override unusable)" };
}
return { urls, source: "env" };
}

let cachedBaseUrl: string | null = null;
/**
* Which list the cached URL was discovered under.
*
* Keyed rather than skipped, so a value minted against production can never be served to a
* run pointed at preprod (or the reverse) — the override would otherwise appear to work while
* silently returning the previous environment's host.
*/
let cachedFor: string | null = null;

export async function getTMBaseURL(
config: BrowserStackConfig,
): Promise<string> {
const { urls, source } = resolveTMBaseUrls();
const listKey = urls.join(",");

// Skip the module-level cache in remote (multi-tenant) mode: it is process-shared,
// so the first user's region would be served to every subsequent user — breaking
// requests for users on a different region's BrowserStack account.
if (!appConfig.REMOTE_MCP && cachedBaseUrl) {
if (!appConfig.REMOTE_MCP && cachedBaseUrl && cachedFor === listKey) {
logger.debug(`Using cached TM base URL: ${cachedBaseUrl}`);
return cachedBaseUrl;
}

if (source === "built-in (override unusable)") {
logger.warn(
`${TM_BASE_URLS_ENV} was set but no entry looked like an http(s) URL; falling back ` +
`to the built-in production list. Requests will go to production.`,
);
}
logger.info(
"No cached TM base URL found, testing available URLs with authentication",
`No cached TM base URL found, testing available URLs with authentication ` +
`(list from ${source}: ${listKey})`,
);

const authString = getBrowserStackAuth(config);
Expand All @@ -34,7 +95,7 @@ export async function getTMBaseURL(

const failures: string[] = [];

for (const baseUrl of TM_BASE_URLS) {
for (const baseUrl of urls) {
try {
const res = await apiClient.get({
url: `${baseUrl}/api/v2/projects/`,
Expand All @@ -51,6 +112,7 @@ export async function getTMBaseURL(
// the cache must stay empty so each user discovers their own region.
if (!appConfig.REMOTE_MCP) {
cachedBaseUrl = baseUrl;
cachedFor = listKey;
}
logger.info(`Selected TM base URL: ${baseUrl}`);
return baseUrl;
Expand Down
5 changes: 5 additions & 0 deletions src/server-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import addBuildInsightsTools from "./tools/build-insights.js";
import { setupOnInitialized } from "./oninitialized.js";
import { BrowserStackConfig } from "./lib/types.js";
import addRCATools from "./tools/rca-agent.js";
import addAskBrowserstackAITool from "./tools/ask-browserstack/register.js";

/**
* Wrapper class for BrowserStack MCP Server
Expand Down Expand Up @@ -61,6 +62,10 @@ export class BrowserStackMcpServer {
addSelfHealTools,
addBuildInsightsTools,
addRCATools,
// Hands a plain-language task to BrowserStack's agent and relays its mid-run
// permission asks back to this client, so a write can be confirmed by the human
// sitting in front of it rather than refused for want of anyone to ask.
addAskBrowserstackAITool,
];

toolAdders.forEach((adder) => {
Expand Down
Loading