feat!: use the hosted BrowserStack MCP server, and ask for OAuth first - #2
Closed
ruturaj-browserstack wants to merge 3 commits into
Closed
feat!: use the hosted BrowserStack MCP server, and ask for OAuth first#2ruturaj-browserstack wants to merge 3 commits into
ruturaj-browserstack wants to merge 3 commits into
Conversation
…places
Semgrep CI failed with one blocking finding. The log did not say which, so it came
from GitHub code scanning, where the uploaded SARIF had it:
.npmrc:1 npm-missing-minimum-release-age
"This .npmrc does not set a minimum release age or sets it too low."
`min-release-age=7` added. The rest of that file was already hardened —
`ignore-scripts`, `strict-ssl`, `save-exact` — this was the one control missing: a
cooldown so a package compromised at publish time is usually caught and unpublished
before anything resolves it. Requires npm >= 11.10; older npm ignores an unknown key
rather than failing, so it is safe to set unconditionally.
Checked the interaction before setting it, because this repo's `.mcp.json` runs a
BETA package: `@browserstack/mcp-server@1.2.27-beta.1` was published 2026-07-14, 48
days ago, so a 7-day floor cannot block it. Worth knowing for later — `latest` is
`1.3.0` from four days ago, which this setting WOULD delay. The exact pin is what
makes that a non-issue, and bumping to a fresh release will now wait a week by
design.
The same scan surfaced two CodeQL findings, both mine and both real dead code in
`tests/wiring.test.mjs`: `field.replace(/ /gu, " ")` and `field.replace(":", ":")` —
each replacing a character with itself, doing nothing while reading as though some
escaping were happening. Removed. CodeQL was not blocking; they were still worth
fixing rather than suppressing.
Verifying those two assertions still fired then turned up three weak ones, all the
same defect: an assertion claiming "the screen shows X" that matched X anywhere in the
file.
- `subpaths` survived deleting its screen row, because the word also appears twice in
the surrounding prose. Anchoring to the screen's fenced block was not enough either —
it appears again INSIDE the block, in the warnings example. Now matched as a row
label, `^ <field>:`, which is what the claim actually means.
- `matchedBy` is not a row label; it qualifies the profile row. Now matched as
`^ profile:.*matchedBy`.
- And that exposed a genuine gap rather than a weak assertion: `(<matchedBy>)` appears
on BOTH the gate screen and the review screen, and one whole-file assertion covered
them jointly — so deleting it from the gate screen passed, because the review
screen's copy satisfied the match. Each screen's block is now pinned separately, and
a mutation against either one alone fails.
337 tests, unchanged in count. Seven mutations proven across the two screens and the
suspect packet.
Process note, third instance this session: two of the "survivors" that led here were
non-global `perl s///` replaces hitting only the first of two occurrences. A mutation
that fails to apply fully reports as a survivor and reads as a gap. Worth checking the
occurrence count before believing one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4b10042bc65cb3eae7237dfdd1a5c1c7ed9fdd07)
All three clients now point at `https://mcp.browserstack.com/mcp?isTfaPlugin=true` instead of launching `npx @browserstack/mcp-server` over stdio. The main win is that credentials leave the picture entirely. The hosted endpoint authenticates by OAuth, so the client runs the sign-in flow and no config file carries a username, an access key, or a `${VAR}` placeholder for one. There is no `.env` to create on the normal path, and no npx download on first launch. `?isTfaPlugin=true` is load-bearing, not decorative. The server registers the TFA RCA collaboration tools per-request only when it is present (remote-mcp-server#101); drop it and the plugin loads with nothing to call, which presents as a broken install rather than a missing flag. Documented as such in INTEGRATION.md § Notes, because it is the kind of thing that gets tidied out of a URL. Per client, with the schema checked rather than assumed: - **Claude Code / Cursor** — `{"type": "http", "url": …}`. Query parameters in `url` are supported; `env` does not apply to HTTP servers at all. - **Codex** — `url` plus `auth = "oauth"`, which is Codex's own documented fallback when no bearer token or static header is configured. No experimental flag: I had half-remembered `experimental_environment = "remote"` as gating this, and it does not — that gates remote *stdio executors*, a different feature. An earlier draft of this commit left Codex on stdio with the remote form commented out and labelled unverified; that was me hedging instead of reading the reference, and it was wrong. Docs follow the config rather than lagging it: - **README** — the credentials row is gone from the prerequisites table, replaced by a statement that there is nothing to configure. Install no longer copies `.env.example`. - **SETUP.md** — step 1 was entirely about `BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY`. It now checks whether the tools are present and points at the client's sign-in, and it explicitly says **never ask for a username, access key or token**: on this route such a request is both useless and an invitation to paste a secret into a transcript. Step 2 keeps its honesty rule — the connection being up is not proof this account can read builds, so "connected, not yet exercised" is the accurate sentence when no build was read. - **INTEGRATION.md** — per-client wiring rewritten; the `codex mcp add` one-liner is now `--url` rather than four `--env` flags. - **`.env.example`** — kept, not deleted, and reframed. It is dead on the default route, but the local stdio fallback in `codex-mcp.example.toml` genuinely needs those two vars, and deleting the file would leave that fallback referencing undocumented variables. `O11Y_TFA_RCA_BASE_URL` is gone: the tenant is the endpoint now. BREAKING for anyone already installed: the transport changes, so the client will ask them to sign in to BrowserStack once. Existing `BROWSERSTACK_*` exports become inert rather than wrong. Verified: a script cross-checks all three configs against one URL constant and asserts none retains `command`/`args`/`env`, that Codex carries `auth = "oauth"`, and that no file mentions the endpoint without the query parameter. `claude plugin validate .` passes plain and `--strict`. 337 tests pass — none touched the MCP config shape, which is why the switch needed the cross-check rather than the suite to catch it. Ordering: this depends on remote-mcp-server#101, which is mergeable and lands before this does. Until it deploys, `?isTfaPlugin=true` registers nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 889ae98a4969f403e27d535abc307657197ccb0d)
Three things, one of which was a trap rather than dead weight.
**Ask for sign-in before anything else.** The hosted server authenticates by OAuth, so
an unauthenticated session has no `tfaRcaTurn`, `listTestIds` or `fetchBuildInsights` —
every step of the skill then fails for one cause, and the symptom (tools missing,
nothing works) reads as a broken plugin. The check is now the first thing in
`SKILL.md`, above Step 0, and says what NOT to do: no interview, no context read, and
never ask for a username or access key, because on this route there is none to give and
asking invites a secret into the transcript. `SETUP.md` gates its remaining steps the
same way, and distinguishes *listed but unauthorised* (sign in) from *not listed at all*
(wiring — INTEGRATION.md), which have different fixes.
**Deleted: the local stdio fallback and `.env.example`.** Not tidying — the fallback
could not have worked. Reading the published package:
1.2.27-beta.1 server-factory.js registers addTfaRcaCollaborationTools
unconditionally -> tfaRcaTurn present. This is what used to run.
1.3.1 registerTools() no longer lists it at all
-> tfaRcaTurn ABSENT over stdio
The refactor pulled the RCA collaboration tools out of the default registration
*precisely so* the remote server could gate them behind `?isTfaPlugin=true`. My fallback
pinned `1.3.1`, so it would have started a server with none of the tools the plugin
calls — an option that looks available and silently is not, which is worse than no
option. Pinning it back to `1.2.27-beta.1` was the alternative and is worse still: a
fallback frozen on a superseded beta that drifts further every release.
`.env.example` existed only to feed that fallback, so it went with it. Nothing else
reads `BROWSERSTACK_USERNAME` or `BROWSERSTACK_ACCESS_KEY` — verified across `bin/`,
`lib/` and `workflows/` before removing it.
**Checked for dead code and found none worth deleting.** Twelve `lib/` exports are
referenced only by tests, but all twelve are on `tests/wiring.test.mjs`'s INTERNAL
allowlist — an existing deliberate decision that they are internal helpers, not
undocumented mistakes. Removing them would be overriding that decision, and several are
used inside their own module. The unreferenced-file scan returned only false positives:
test files are found by convention, not by name.
339 tests, up from 337. Eight mutations proven, including deleting the notice, moving it
below Step 0, dropping the what-not-to-do clause, dropping the never-ask-for-a-key rule,
weakening the frontmatter description, and each of SETUP.md's three gating statements.
Process, and it is the third instance in this file: the new guard first failed because I
normalised with `\s+ -> " "` and the notice is a `>` blockquote, so the wrapped marker
survived mid-sentence ("Do not > start the interview") and the match never fired. The
neighbouring guards already strip blockquote markers first for exactly this reason; this
one now does too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9a0293a2427065606d131ca943f4a7a4c8f93317)
SavioBS629
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Points the plugin at BrowserStack's hosted MCP server, adds an OAuth-first check, and
clears the Semgrep CI failure on
main.Depends on
browserstack/remote-mcp-server#101 must merge and deploy before this does.
The RCA collaboration tools are registered per-request only when
?isTfaPlugin=trueispresent, and that parameter is what #101 adds. Merge this first and an install gets a
plugin with no
tfaRcaTurn,listTestIdsorfetchBuildInsights— it loads and hasnothing to call.
Remote MCP, and no credentials anywhere
All three clients now use
https://mcp.browserstack.com/mcp?isTfaPlugin=true:.mcp.json—{"type": "http", "url": …}.cursor-mcp.json— same shapecodex-mcp.example.toml—url+auth = "oauth"The endpoint authenticates by OAuth, so no config file carries a username, an access
key, or a
${VAR}placeholder for one, and there is no.envto create. Schemas wereread rather than assumed:
envdoes not apply to HTTP servers at all, and Codex'sauth = "oauth"is its documented fallback when no bearer token or static header isset. No experimental flag is needed —
experimental_environment = "remote"gates remotestdio executors, a different feature.
?isTfaPlugin=trueis documented as load-bearing inINTEGRATION.md § Notes, becauseit is exactly the kind of thing that gets tidied out of a URL.
Deleted: a fallback that could not have worked
The local stdio route is gone, along with
.env.example. This is not tidying — readingthe published package:
The refactor pulled those tools out of default registration precisely so the remote
server could gate them. A fallback pinned at
1.3.1starts a server with none of thetools the plugin calls — an option that looks available and silently is not. Pinning it
back to
1.2.27-beta.1was the alternative and worse: a fallback frozen on a supersededbeta.
.env.exampleexisted only to feed that route; nothing inbin/,lib/orworkflows/reads those variables, verified before removing it.Ask for sign-in first
An unauthenticated session has none of the
bstacktools, so every step fails for onecause and the symptom reads as a broken plugin. The check is now the first thing in
SKILL.md, above Step 0, and says what not to do: no interview, no context read, andnever ask for a username or access key — on this route there is none, and asking invites
a secret into the transcript.
SETUP.mdgates its remaining steps the same way andseparates listed but unauthorised (sign in) from not listed at all (wiring), which
have different fixes.
CI
Semgrep failed on
mainwith one blocking finding —.npmrcmissingmin-release-age. Addedmin-release-age=7. Checked the interaction first: the pinnedMCP package was published 48 days ago so a 7-day floor cannot block it, and
latestbeing 4 days old is exactly what the setting is for.
CodeQL also flagged two no-op
.replace()calls in the test suite (replacing acharacter with itself). Removed rather than suppressed.
Verification
claude plugin validate .passes plain and--strict; the marketplace manifestvalidates on its own.
config shape, so one script asserts all three configs against a single URL constant,
that none retains
command/args/env, that Codex carriesauth, and that no filenames the endpoint without the query parameter.
below Step 0, dropping the what-not-to-do clause, dropping the never-ask-for-a-key
rule, weakening the frontmatter description, and each of
SETUP.md's three gatingstatements all fail a test.
Known, not addressed here
SKILL.mdStep 4 item 3 still describes the PR-window search and the deploy-state asone combined action, while the supplied-PR-list path says no window search runs. The
code is correct —
--prspreservesdeployState— but an agent could read the prose aslicence to skip the deploy timeline too, which is the
shipped-afterfalsificationsignal. Left for a follow-up.
🤖 Generated with Claude Code