@browserstack/mcp-server 1.4.0-beta.1 → 1.4.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ import addAppLiveTools from "./tools/applive.js";
16
16
  import addBuildInsightsTools from "./tools/build-insights.js";
17
17
  import { setupOnInitialized } from "./oninitialized.js";
18
18
  import addRCATools from "./tools/rca-agent.js";
19
- import addAskBrowserstackAITool from "./tools/ask-browserstack/register.js";
19
+ import addAskBrowserStackAITool from "./tools/ask-browserstack/register.js";
20
20
  /**
21
21
  * Wrapper class for BrowserStack MCP Server
22
22
  * Stores a map of registered tools by name
@@ -55,7 +55,7 @@ export class BrowserStackMcpServer {
55
55
  // Hands a plain-language task to BrowserStack's agent and relays its mid-run
56
56
  // permission asks back to this client, so a write can be confirmed by the human
57
57
  // sitting in front of it rather than refused for want of anyone to ask.
58
- addAskBrowserstackAITool,
58
+ addAskBrowserStackAITool,
59
59
  ];
60
60
  toolAdders.forEach((adder) => {
61
61
  // Each adder now returns a Record<string, Tool>
@@ -258,7 +258,7 @@ export async function mintCentralToken(url, credentials, transport, now = Date.n
258
258
  const pending = mintOnce(url, credentials, transport)
259
259
  .then(({ token, lifetimeMs }) => {
260
260
  cache.set(key, { token, expiresAt: now + lifetimeMs });
261
- logger.info("askBrowserstackAI: signed in as %s (lifetime %ss)", credentials.username, Math.round(lifetimeMs / 1000));
261
+ logger.info("askBrowserStackAI: signed in as %s (lifetime %ss)", credentials.username, Math.round(lifetimeMs / 1000));
262
262
  return token;
263
263
  })
264
264
  .catch((error) => {
@@ -98,7 +98,7 @@ function announce(what, url, source) {
98
98
  if (announced.has(line))
99
99
  return;
100
100
  announced.add(line);
101
- logger.info("askBrowserstackAI: %s is %s (source: %s)", what, url, source);
101
+ logger.info("askBrowserStackAI: %s is %s (source: %s)", what, url, source);
102
102
  }
103
103
  /**
104
104
  * Resolve Atlas's base URL:
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `askBrowserstackAI` — one tool call in, one tool result out, with a human's approval
2
+ * `askBrowserStackAI` — one tool call in, one tool result out, with a human's approval
3
3
  * relayed through the middle of it.
4
4
  *
5
5
  * The shape, and why:
@@ -55,7 +55,7 @@ export interface AskDeps {
55
55
  decisionTransport?: DecisionTransport;
56
56
  }
57
57
  export declare function relayMode(server: McpServer): RelayMode;
58
- export declare function addAskBrowserstackAITool(server: McpServer, deps: AskDeps, config?: BrowserStackConfig): Record<string, RegisteredTool>;
58
+ export declare function addAskBrowserStackAITool(server: McpServer, deps: AskDeps, config?: BrowserStackConfig): Record<string, RegisteredTool>;
59
59
  /** The tool-adder the server factory calls. */
60
- export declare function addAskBrowserstackAIToolFromConfig(server: McpServer, config: BrowserStackConfig): Record<string, RegisteredTool>;
61
- export default addAskBrowserstackAIToolFromConfig;
60
+ export declare function addAskBrowserStackAIToolFromConfig(server: McpServer, config: BrowserStackConfig): Record<string, RegisteredTool>;
61
+ export default addAskBrowserStackAIToolFromConfig;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `askBrowserstackAI` — one tool call in, one tool result out, with a human's approval
2
+ * `askBrowserStackAI` — one tool call in, one tool result out, with a human's approval
3
3
  * relayed through the middle of it.
4
4
  *
5
5
  * The shape, and why:
@@ -47,7 +47,16 @@ import { PRODUCTS, } from "./types.js";
47
47
  * words alone, before any call is made — so the ordering is deliberate: when to reach for it
48
48
  * first, what it does second, and the consent behaviour last.
49
49
  */
50
- const DESCRIPTION = "Use this when no other BrowserStack tool here fits the task, or when the ones you tried " +
50
+ const DESCRIPTION =
51
+ // Alpha status leads, deliberately. The model reads this before deciding to call, and a
52
+ // tool that is not enabled for the account cannot do the job at all — so "there is a
53
+ // per-account gate, fall back to the individual tools" is the most useful thing to say
54
+ // first. Parenthesised so it reads as a status note, not as the tool's purpose.
55
+ "(Alpha, limited availability. Enabled per account and per product; if it is not enabled " +
56
+ "the call returns an entitlement error, nothing runs, and you should complete the task " +
57
+ "with the individual tools instead. To request access, the user should contact their " +
58
+ "BrowserStack account owner.) " +
59
+ "Use this when no other BrowserStack tool here fits the task, or when the ones you tried " +
51
60
  "did not get you there. Prefer a specific tool whenever one fits: it is faster and more " +
52
61
  "predictable than handing the job to an agent. " +
53
62
  "Otherwise, describe what you want in plain language and BrowserStack's agent decides " +
@@ -147,7 +156,7 @@ async function relayOneAsk(server, ask, approvals, relatedRequestId) {
147
156
  // The SHAPE of the answer only — a fixed action enum and a boolean, never the description
148
157
  // or anything a user typed. Logged so that what a client actually submits can be read next
149
158
  // time rather than inferred from a compiled binary.
150
- logger.info("askBrowserstackAI: elicitation answered %s", elicitationShape(answer));
159
+ logger.info("askBrowserStackAI: elicitation answered %s", elicitationShape(answer));
151
160
  const { decision, reason } = decide(answer);
152
161
  approvals.push({ description: ask.description, decision, reason });
153
162
  return { perm_id: ask.perm_id, decision, reason };
@@ -224,14 +233,14 @@ async function runStreamed(server, streamTransport, decisionTransport, url, head
224
233
  // description cannot produce an answerable prompt, so it must not produce a prompt.
225
234
  const ask = parseAsk(event.data);
226
235
  if (!ask) {
227
- logger.error("askBrowserstackAI: unusable permission ask on the stream; ignoring");
236
+ logger.error("askBrowserStackAI: unusable permission ask on the stream; ignoring");
228
237
  continue;
229
238
  }
230
239
  if (!runId) {
231
240
  // Atlas emits `run` before any ask precisely so this cannot happen. If it does,
232
241
  // there is nowhere to send a decision — so do not prompt a human for an answer
233
242
  // that could never be delivered.
234
- logger.error("askBrowserstackAI: permission ask arrived before run_id; cannot answer");
243
+ logger.error("askBrowserStackAI: permission ask arrived before run_id; cannot answer");
235
244
  continue;
236
245
  }
237
246
  // `relayOneAsk` RETHROWS on an unexpected elicitation failure. Under A2 that was
@@ -246,7 +255,7 @@ async function runStreamed(server, streamTransport, decisionTransport, url, head
246
255
  decision = await relayOneAsk(server, ask, approvals, relatedRequestId);
247
256
  }
248
257
  catch (error) {
249
- logger.warn("askBrowserstackAI: elicitation failed, denying explicitly: %s", error instanceof Error ? error.message : String(error));
258
+ logger.warn("askBrowserStackAI: elicitation failed, denying explicitly: %s", error instanceof Error ? error.message : String(error));
250
259
  decision = { perm_id: ask.perm_id, decision: "deny", reason: "error" };
251
260
  }
252
261
  const status = await decisionTransport(decisionUrl(url, runId), headers, {
@@ -259,7 +268,7 @@ async function runStreamed(server, streamTransport, decisionTransport, url, head
259
268
  // own expiry, so a lost decision is safe — it can only cost an approval, never
260
269
  // grant one. Retrying risks the opposite: a duplicate that 409s, or worse, an
261
270
  // approval applied to a step the run has already moved past.
262
- logger.warn("askBrowserstackAI: decision for %s was not accepted (HTTP %s)", decision.perm_id, status);
271
+ logger.warn("askBrowserStackAI: decision for %s was not accepted (HTTP %s)", decision.perm_id, status);
263
272
  }
264
273
  }
265
274
  if (!sawResult) {
@@ -283,7 +292,7 @@ export function relayMode(server) {
283
292
  ? "offered"
284
293
  : "no_human";
285
294
  }
286
- export function addAskBrowserstackAITool(server, deps, config) {
295
+ export function addAskBrowserStackAITool(server, deps, config) {
287
296
  // A1 (CONTRACT v2) is the only path; A2 is gone. No version flag is needed to talk to
288
297
  // an Atlas that predates the stream: such a server answers `POST /agent` with ordinary
289
298
  // JSON, the parser sees no `text/event-stream`, and the run degrades to a read-only
@@ -300,7 +309,7 @@ export function addAskBrowserstackAITool(server, deps, config) {
300
309
  // Telemetry must not decide whether a tool call succeeds.
301
310
  }
302
311
  };
303
- tools.askBrowserstackAI = server.tool("askBrowserstackAI", DESCRIPTION, {
312
+ tools.askBrowserStackAI = server.tool("askBrowserStackAI", DESCRIPTION, {
304
313
  product: z
305
314
  .enum(PRODUCTS)
306
315
  .describe("Which product to work in: tm (Test Management), a11y (Accessibility), " +
@@ -314,9 +323,9 @@ export function addAskBrowserstackAITool(server, deps, config) {
314
323
  // sets it false: consent is not a licence to delete.
315
324
  readOnlyHint: false,
316
325
  destructiveHint: false,
317
- title: "Ask BrowserStack AI",
326
+ title: "Ask BrowserStack AI (Alpha)",
318
327
  }, async ({ product, query }, extra) => {
319
- track("askBrowserstackAI");
328
+ track("askBrowserStackAI");
320
329
  const approvals = [];
321
330
  // Negotiated before anything else so the failure paths below report the mode they
322
331
  // would have run in.
@@ -347,7 +356,7 @@ export function addAskBrowserstackAITool(server, deps, config) {
347
356
  else {
348
357
  // Omitted ENTIRELY, not sent empty: its absence is what selects Atlas's
349
358
  // read-only HeadlessGate.
350
- logger.info("askBrowserstackAI: no permission relay (%s); running read-only", mode);
359
+ logger.info("askBrowserStackAI: no permission relay (%s); running read-only", mode);
351
360
  }
352
361
  // `product` reaches the result so an entitlement refusal can name it: the flags
353
362
  // are per product, and a bare "not enabled" sends the user to their admin
@@ -361,7 +370,7 @@ export function addAskBrowserstackAITool(server, deps, config) {
361
370
  const message = error instanceof AskError || error instanceof Error
362
371
  ? error.message
363
372
  : String(error);
364
- logger.error("askBrowserstackAI failed: %s", message);
373
+ logger.error("askBrowserStackAI failed: %s", message);
365
374
  // No `canElicit` argument: the request never left this process, so whether the
366
375
  // client could have been prompted is not what the reader needs to know.
367
376
  return toResult(errorResult(message, approvals));
@@ -373,9 +382,9 @@ export function addAskBrowserstackAITool(server, deps, config) {
373
382
  return tools;
374
383
  }
375
384
  /** The tool-adder the server factory calls. */
376
- export function addAskBrowserstackAIToolFromConfig(server, config) {
385
+ export function addAskBrowserStackAIToolFromConfig(server, config) {
377
386
  if (!isEnabled()) {
378
- logger.info("askBrowserstackAI disabled by ASK_BROWSERSTACK_DISABLED");
387
+ logger.info("askBrowserStackAI disabled by ASK_BROWSERSTACK_DISABLED");
379
388
  return {};
380
389
  }
381
390
  const credentials = () => ({
@@ -383,7 +392,7 @@ export function addAskBrowserstackAIToolFromConfig(server, config) {
383
392
  accessKey: config["browserstack-access-key"],
384
393
  });
385
394
  const tokenTransport = fetchTokenTransport();
386
- return addAskBrowserstackAITool(server, {
395
+ return addAskBrowserStackAITool(server, {
387
396
  // Both resolved per call. An unconfigured host surfaces as a named error from the
388
397
  // tool rather than as a missing tool, so the cause is visible to whoever hits it.
389
398
  agentUrl,
@@ -391,4 +400,4 @@ export function addAskBrowserstackAIToolFromConfig(server, config) {
391
400
  credentialsFor: credentials,
392
401
  }, config);
393
402
  }
394
- export default addAskBrowserstackAIToolFromConfig;
403
+ export default addAskBrowserStackAIToolFromConfig;
@@ -58,10 +58,11 @@ export const RELAY_OFF_DETAILS = {
58
58
  // Not a refusal by anyone and not a relay problem at all: the account is not on the
59
59
  // product's agent flag. The product-specific sentence and what to do about it live in
60
60
  // `error`, so this one points there rather than duplicating the plumbing.
61
- not_entitled: "NOBODY DECLINED THIS AND NOTHING RAN. BrowserStack AI is not enabled for this account, " +
62
- "so the request was refused before the agent started. `error` says which product and " +
63
- "what to do about it. This is an entitlement on the account, not a problem with your " +
64
- "credentials and not a decision anyone made about your request.",
61
+ not_entitled: "NOBODY DECLINED THIS AND NOTHING RAN. Ask AI (Alpha) is not enabled on this account. " +
62
+ "Ask AI is in limited alpha and available only to enrolled accounts, so the request was " +
63
+ "refused before the agent started. `error` names the product. This is an entitlement on " +
64
+ "the account, not a problem with your credentials and not a decision anyone made about " +
65
+ "your request.",
65
66
  // The request never got as far as the agent. Distinct from `disabled` (the agent ran, with
66
67
  // the relay switched off) and from a decline (someone was asked and said no), because the
67
68
  // three call for completely different things from whoever reads them.
@@ -137,9 +138,10 @@ export function isNotEntitled(response) {
137
138
  */
138
139
  export const NOT_ENTITLED_DETAIL = (product) => {
139
140
  const scope = product && product.trim() ? ` for \`${product.trim()}\`` : "";
140
- return (`BrowserStack AI is not enabled${scope} on your account. Please contact your admin. ` +
141
- `YOUR CREDENTIALS ARE FINE they authenticated successfully; this is a per-product ` +
142
- `entitlement on the account. Nothing was run and nobody declined anything.`);
141
+ return (`Ask AI (Alpha) is not enabled${scope} on this account. Ask AI is in limited alpha and ` +
142
+ `available only to enrolled accounts. Authentication succeeded and nothing was run. ` +
143
+ `To request access, contact your BrowserStack account owner, or reach out at ` +
144
+ `https://www.browserstack.com/contact-sales`);
143
145
  };
144
146
  export function neverReachedAgent(response) {
145
147
  // No response at all: nothing could have run.
@@ -120,7 +120,7 @@ export function parseFrame(frame) {
120
120
  // A frame we cannot read is not a frame we may guess at. Dropping it is safe
121
121
  // because the only consequence is that an ask goes unanswered and the gate denies
122
122
  // on its own expiry — never that something is approved.
123
- logger.warn("askBrowserstackAI: unparseable stream frame, ignoring");
123
+ logger.warn("askBrowserStackAI: unparseable stream frame, ignoring");
124
124
  return null;
125
125
  }
126
126
  }
@@ -8,7 +8,7 @@
8
8
  * where the missing identifier comes from.
9
9
  *
10
10
  * Point at a sibling tool whenever one can produce the id — it is faster and more
11
- * predictable than an agent. Point at `askBrowserstackAI` only when NO tool here can.
11
+ * predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
12
12
  *
13
13
  * The one that matters most: 15 of the 17 Test Management tools require a project
14
14
  * identifier and NONE of them accepts its absence, yet no tool in this server lists
@@ -8,7 +8,7 @@
8
8
  * where the missing identifier comes from.
9
9
  *
10
10
  * Point at a sibling tool whenever one can produce the id — it is faster and more
11
- * predictable than an agent. Point at `askBrowserstackAI` only when NO tool here can.
11
+ * predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
12
12
  *
13
13
  * The one that matters most: 15 of the 17 Test Management tools require a project
14
14
  * identifier and NONE of them accepts its absence, yet no tool in this server lists
@@ -21,7 +21,7 @@
21
21
  */
22
22
  /** No tool lists projects, so this genuinely has to go to the agent. */
23
23
  export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
24
- 'have one, call askBrowserstackAI with product "tm" and ask which projects exist, then ' +
24
+ 'have one, call askBrowserStackAI with product "tm" and ask which projects exist, then ' +
25
25
  "retry this tool with the identifier it returns.";
26
26
  /** A sibling tool can produce the id — prefer it over the agent. */
27
27
  export function needsIdFrom(idLabel, sourceTool) {
@@ -33,15 +33,15 @@ export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "
33
33
  export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
34
34
  /** Session ids are not listable by any tool here. */
35
35
  export const NEEDS_SESSION_ID = " Requires a session id, which no tool here lists. If you only know the build, call " +
36
- "getBuildId or listBuildId; if you have neither, call askBrowserstackAI with product " +
36
+ "getBuildId or listBuildId; if you have neither, call askBrowserStackAI with product " +
37
37
  '"tra" and describe the run you mean.';
38
38
  /** A completed scan's ids come from startAccessibilityScan, or from the agent. */
39
39
  export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan. They are returned by startAccessibilityScan; " +
40
- 'for a scan run earlier, call askBrowserstackAI with product "a11y" to locate it, since ' +
40
+ 'for a scan run earlier, call askBrowserStackAI with product "a11y" to locate it, since ' +
41
41
  "no tool here lists past scans.";
42
42
  /** Auth-config ids are not listable by any tool here. */
43
43
  export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
44
- "existing configurations, so if you do not have the id, call askBrowserstackAI with " +
44
+ "existing configurations, so if you do not have the id, call askBrowserStackAI with " +
45
45
  'product "a11y".';
46
46
  /** Test ids come from listTestIds, which itself needs a build id. */
47
47
  export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.4.0-beta.1",
3
+ "version": "1.4.0-beta.2",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",