Skip to content

fix: source per-user creds and region from BrowserStackConfig (remote-MCP safety)#295

Merged
gaurav-singh-9227 merged 2 commits into
browserstack:mainfrom
ruturaj-browserstack:fix/pass-config-creds-at-runtime
May 15, 2026
Merged

fix: source per-user creds and region from BrowserStackConfig (remote-MCP safety)#295
gaurav-singh-9227 merged 2 commits into
browserstack:mainfrom
ruturaj-browserstack:fix/pass-config-creds-at-runtime

Conversation

@ruturaj-browserstack
Copy link
Copy Markdown
Collaborator

Summary

Three correctness bugs surfaced when this package runs as a library inside the remote MCP wrapper (multi-tenant Node process). Each one either silently fails or risks cross-user state leakage in that deployment; all are no-ops in local stdio mode.

  1. triggerEspressoBuild read creds from process.env at request time → silent 401 or wrong user's creds in remote mode.
  2. getMavenCommandForWindows read creds from process.env and emitted them into a Maven command shown to the user → user sees the literal string undefined (or a stale value) in remote mode.
  3. getTMBaseURL's module-level cachedBaseUrl is process-shared → in a multi-region remote deployment, the first user's region URL is served to every subsequent user.

Why this matters

@browserstack/mcp-server is consumed as a library by the remote MCP wrapper, a long-lived multi-tenant HTTP service. A single Node process serves many concurrent users. The wrapper builds a per-request BrowserStackConfig from each user's OAuth-decoded session and passes it to new BrowserStackMcpServer(config) — it does not mutate process.env.BROWSERSTACK_USERNAME per request, and the module-level cache cannot be made user-aware without code changes here.

Sibling code paths already do the right thing (triggerXcuiBuild accepts config; getMavenCommandForUnix accepts username/accessKey params; device-cache.ts already gates writes on config.REMOTE_MCP). This PR brings the inconsistent siblings into line.

Changes

1. triggerEspressoBuild (appautomate-utils/native-execution/appautomate.ts)

  • Now accepts config: BrowserStackConfig and derives auth via getBrowserStackAuth(config). Matches triggerXcuiBuild.
  • Caller in src/tools/appautomate.ts threads config through (already in scope from the tool registration).
  • Bonus: throws a clear error when creds are missing instead of base64-encoding undefined:undefined.

2. getMavenCommandForWindows (sdk-utils/bstack/commands.ts)

  • Now accepts username/accessKey parameters, matching getMavenCommandForUnix.
  • Previously the Windows branch read process.env while the Unix branch took params — an inconsistency that made the Windows variant emit literal undefined into the Maven command shown to the user in remote mode.

3. getTMBaseURL (src/lib/tm-base-url.ts)

  • Imports the singleton appConfig from src/config.js and guards both the read and write of cachedBaseUrl on !appConfig.REMOTE_MCP.
  • Stdio mode: unchanged — discover once, cache, reuse.
  • Remote mode: cache stays empty; each caller re-discovers their own region.
  • Follows the existing precedent in device-cache.ts (shouldSendStartedEvent already checks config.REMOTE_MCP).

Tests

  • tests/tools/triggerEspressoBuild.test.ts (new) — sets process.env.BROWSERSTACK_USERNAME to a sentinel, calls triggerEspressoBuild with a different value in config, decodes the Authorization header, and asserts the config value wins. Covers the missing-creds error path.
  • tests/tools/sdk-utils-commands.test.ts (new) — verifies getSDKPrefixCommand (nodejs / java-unix / java-windows) renders the passed username/accessKey and never the literal undefined or process.env. The Windows case is a direct regression test.
  • tests/lib/tm-base-url.test.ts (new) — two regression tests: stdio mode still caches across calls; remote mode re-discovers each call (verified via call-count assertions on the mocked HTTP client across simulated two users in different regions).

npm run build (lint + format + test + tsc) is clean. 19 test files / 133 tests pass.

What's NOT in this PR (intentional)

  • Module-level user-data globals in src/lib/inmemory-store.ts (signedUrlMap, _storedPercyResults) and src/tools/sdk-utils/percy-web/fetchPercyToken.ts (globalPercyToken, globalProjectName). All consumer tools are already on the remote wrapper's INITIALLY_DISABLED_TOOLS list, so they're contained today. Refactoring to request-scoped state is a larger separate change.

  • Code-template emitters that contain the literal text process.env.BROWSERSTACK_USERNAME:

    • src/tools/appautomate-utils/appium-sdk/languages/{ruby,nodejs}.ts
    • src/tools/sdk-utils/bstack/constants.ts (inside a ```js block)
    • src/tools/sdk-utils/percy-automate/constants.ts (inside a ```js block, with || 'YOUR_USERNAME' fallback)

    These emit code for the user to paste into their own integration. They're producing output, not reading env at runtime. Left as-is.

Drive-by

The single-line diff on src/tools/testmanagement-utils/update-testcase.ts is an unrelated prettier reformat applied by npm run format (part of the build gate). Included rather than reverted so future build runs don't dirty the tree.

Test plan

  • npm run build passes locally (lint + format + test + tsc)
  • New unit tests added; all 131 prior tests still pass (133 total now)
  • Reviewer to spot-check: grep -rn "process.env.BROWSERSTACK_USERNAME" src/ should now only return entries in appium-sdk/languages/*.ts, sdk-utils/bstack/constants.ts, sdk-utils/percy-automate/constants.ts (all inside code-block templates), and src/index.ts (bootstrap)
  • Once merged, paired follow-up: confirm the appautomate Espresso tool and TM tools work correctly when invoked through the remote MCP wrapper for users in different regions

🤖 Generated with Claude Code

….env

Two runtime paths were reading BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY
directly from process.env. In remote (multi-tenant) mode the per-user creds
flow through the BrowserStackConfig object passed to each tool, not env vars,
so these reads silently return undefined (401) or — worse — return a stale
value left over from container startup, leaking another user's credentials.

Changes:

1. triggerEspressoBuild (appautomate-utils/native-execution/appautomate.ts)
   Now takes a BrowserStackConfig parameter and derives auth via
   getBrowserStackAuth(config). Matches the existing pattern in the sibling
   triggerXcuiBuild. Caller in src/tools/appautomate.ts updated to thread
   config through. Throws a clear error when config is missing creds rather
   than silently base64-encoding "undefined:undefined".

2. getMavenCommandForWindows (sdk-utils/bstack/commands.ts)
   Now takes username/accessKey parameters, matching the sibling
   getMavenCommandForUnix. Previously the Windows branch read process.env
   while the Unix branch correctly took params. In remote mode the Windows
   variant emitted "undefined" into the Maven command shown to the user.

Tests:
- tests/tools/triggerEspressoBuild.test.ts — verifies creds come from
  config, not process.env (sets process.env to a sentinel and asserts the
  Authorization header decodes to the config values). Also covers the
  missing-creds error path.
- tests/tools/sdk-utils-commands.test.ts — verifies getSDKPrefixCommand
  (nodejs / java unix / java windows) renders the passed username/accessKey
  and never the literal "undefined" or "process.env".

Out of scope (separate follow-ups):
- Module-level user-data globals in src/lib/inmemory-store.ts,
  src/tools/sdk-utils/percy-web/fetchPercyToken.ts, src/lib/tm-base-url.ts.
  All consumer tools are already on the remote wrapper's
  INITIALLY_DISABLED_TOOLS list; refactoring to request-scoped state is a
  larger change.
- Code-template emitters under src/tools/*/appium-sdk/languages/*.ts and
  the WebdriverIO config snippets in sdk-utils/bstack/constants.ts and
  sdk-utils/percy-automate/constants.ts contain the literal text
  "process.env.BROWSERSTACK_USERNAME" — these are emitted as code for the
  user to paste into their own integration; they are not runtime reads
  and are intentionally left as-is.

The prettier reformat on update-testcase.ts is an unrelated drive-by
cleanup applied by `npm run format` (part of the build gate).
… leak

The module-level `cachedBaseUrl` is process-shared. In remote (multi-tenant)
mode, the first user to call `getTMBaseURL` populates it with their region's
URL, and every subsequent user — including users on a different region's
BrowserStack account — receives that same URL. Their requests then hit the
wrong region's Test Management backend.

Guard both the read and write of `cachedBaseUrl` behind
`!appConfig.REMOTE_MCP`. In stdio (single-tenant) mode behavior is unchanged:
discover once, cache, reuse. In remote mode the cache stays empty and each
caller re-discovers their own region.

Per-credentials caching (instead of skipping the cache entirely) would be
faster but requires a larger change; the immediate priority is correctness.

Adds tests/lib/tm-base-url.test.ts with two regression tests: one verifying
the stdio mode still caches, one verifying remote mode re-discovers across
calls with different region responses.
@gaurav-singh-9227 gaurav-singh-9227 merged commit eced804 into browserstack:main May 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants