Skip to content

feat!: use the hosted BrowserStack MCP server, and ask for OAuth first - #2

Closed
ruturaj-browserstack wants to merge 3 commits into
mainfrom
feat/remote-mcp
Closed

feat!: use the hosted BrowserStack MCP server, and ask for OAuth first#2
ruturaj-browserstack wants to merge 3 commits into
mainfrom
feat/remote-mcp

Conversation

@ruturaj-browserstack

Copy link
Copy Markdown
Collaborator

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=true is
present, and that parameter is what #101 adds. Merge this first and an install gets a
plugin with no tfaRcaTurn, listTestIds or fetchBuildInsights — it loads and has
nothing to call.

Remote MCP, and no credentials anywhere

All three clients now use https://mcp.browserstack.com/mcp?isTfaPlugin=true:

Client Config
Claude Code .mcp.json{"type": "http", "url": …}
Cursor .cursor-mcp.json — same shape
Codex codex-mcp.example.tomlurl + 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 .env to create. Schemas were
read rather than assumed: env does not apply to HTTP servers at all, and Codex's
auth = "oauth" is its documented fallback when no bearer token or static header is
set. No experimental flag is needed — experimental_environment = "remote" gates remote
stdio executors, a different feature.

?isTfaPlugin=true is documented as load-bearing in INTEGRATION.md § Notes, because
it 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 — reading
the published package:

1.2.27-beta.1   server-factory.js registers addTfaRcaCollaborationTools
                unconditionally  ->  tfaRcaTurn present. What used to run.
1.3.1           registerTools() no longer lists it at all
                ->  tfaRcaTurn ABSENT over stdio

The refactor pulled those tools out of default registration precisely so the remote
server could gate them. A fallback pinned at 1.3.1 starts a server with none of the
tools the plugin calls — an option that looks available and silently is not. Pinning it
back to 1.2.27-beta.1 was the alternative and worse: a fallback frozen on a superseded
beta. .env.example existed only to feed that route; nothing in bin/, lib/ or
workflows/ reads those variables, verified before removing it.

Ask for sign-in first

An unauthenticated session has none of the bstack tools, so every step fails for one
cause 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, and
never ask for a username or access key — on this route there is none, and asking invites
a secret into the transcript. SETUP.md gates its remaining steps the same way and
separates listed but unauthorised (sign in) from not listed at all (wiring), which
have different fixes.

CI

Semgrep failed on main with one blocking finding — .npmrc missing
min-release-age. Added min-release-age=7. Checked the interaction first: the pinned
MCP package was published 48 days ago so a 7-day floor cannot block it, and latest
being 4 days old is exactly what the setting is for.

CodeQL also flagged two no-op .replace() calls in the test suite (replacing a
character with itself). Removed rather than suppressed.

Verification

  • claude plugin validate . passes plain and --strict; the marketplace manifest
    validates on its own.
  • 339 tests pass.
  • A cross-check, not the suite, is what guards this change — no test touches the MCP
    config shape, so one script asserts all three configs against a single URL constant,
    that none retains command/args/env, that Codex carries auth, and that no file
    names the endpoint without the query parameter.
  • Guards added for the OAuth-first rule, mutation-tested: 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 all fail a test.

Known, not addressed here

SKILL.md Step 4 item 3 still describes the PR-window search and the deploy-state as
one combined action, while the supplied-PR-list path says no window search runs. The
code is correct — --prs preserves deployState — but an agent could read the prose as
licence to skip the deploy timeline too, which is the shipped-after falsification
signal. Left for a follow-up.

🤖 Generated with Claude Code

ruturaj-browserstack and others added 3 commits August 31, 2026 20:50
…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)
@ruturaj-browserstack
ruturaj-browserstack requested a review from a team as a code owner August 31, 2026 15:22
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