@browserstack/mcp-server 2.1.0 → 2.1.1

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.
@@ -6,6 +6,7 @@ const require = createRequire(import.meta.url);
6
6
  const packageJson = require("../../package.json");
7
7
  import { apiClient } from "./apiClient.js";
8
8
  import globalConfig from "../config.js";
9
+ import { redact } from "../tools/capability-registry/redact.js";
9
10
  const INSTRUMENTATION_ENDPOINT = "https://api.browserstack.com/sdk/v1/event";
10
11
  function baseProperties(toolName, clientInfo) {
11
12
  return {
@@ -25,8 +26,13 @@ function errorProperties(error) {
25
26
  function sendEvent(event, config) {
26
27
  let authHeader;
27
28
  if (config) {
28
- const authString = getBrowserStackAuth(config);
29
- authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
29
+ try {
30
+ const authString = getBrowserStackAuth(config);
31
+ authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
32
+ }
33
+ catch {
34
+ // noop
35
+ }
30
36
  }
31
37
  apiClient
32
38
  .post({
@@ -80,6 +86,19 @@ export function trackMCP(toolName, clientInfo, error, config, extras) {
80
86
  };
81
87
  sendEvent(event, config);
82
88
  }
89
+ /** The text a tool showed the user. */
90
+ function errorTextOf(result) {
91
+ const content = result?.content;
92
+ if (!Array.isArray(content))
93
+ return undefined;
94
+ const text = content
95
+ .map((part) => typeof part?.text === "string"
96
+ ? part.text
97
+ : "")
98
+ .filter(Boolean)
99
+ .join(" ");
100
+ return redact(text);
101
+ }
83
102
  function isErrorResult(result) {
84
103
  return (typeof result === "object" &&
85
104
  result !== null &&
@@ -93,10 +112,14 @@ export async function withToolCall(toolName, getClientInfo, config, fn) {
93
112
  const ctx = { toolName, clientInfo: {}, config };
94
113
  const startedAt = performance.now();
95
114
  let outcome = "ok";
115
+ let errorText;
96
116
  try {
97
117
  const result = await callContext.run(ctx, fn);
98
- if (isErrorResult(result))
118
+ if (isErrorResult(result)) {
99
119
  outcome = "error_result";
120
+ // A returned failure never sets ctx.error, so the reason is read off the result.
121
+ errorText ??= errorTextOf(result);
122
+ }
100
123
  return result;
101
124
  }
102
125
  catch (error) {
@@ -123,6 +146,9 @@ export async function withToolCall(toolName, getClientInfo, config, fn) {
123
146
  duration_ms: Math.max(0, Math.round(performance.now() - startedAt)),
124
147
  outcome,
125
148
  ...(ctx.error !== undefined ? errorProperties(ctx.error) : {}),
149
+ ...(outcome === "error_result" && ctx.error === undefined && errorText
150
+ ? { error_message: errorText }
151
+ : {}),
126
152
  ...(ctx.extras ?? {}),
127
153
  },
128
154
  };
@@ -74,7 +74,7 @@ export class AccessibilityScanner {
74
74
  });
75
75
  const data = response.data;
76
76
  if (!data.success)
77
- throw new Error(`Unable to start scan: ${data.errors?.join(", ")}`);
77
+ throw new Error(`Unable to start scan: ${data.errors?.join(", ") || JSON.stringify(data)}`);
78
78
  return data;
79
79
  }
80
80
  catch (err) {
@@ -25,13 +25,13 @@ const RULES = [
25
25
  },
26
26
  {
27
27
  name: "email",
28
- pattern: /\b[\w.%+-]+@[\w.-]+\.[a-z]{2,}\b/gi,
28
+ pattern: /\b[\w.%+-]{1,128}@[\w.-]{1,128}\.[a-z]{2,24}\b/gi,
29
29
  replacement: "[email]",
30
30
  },
31
31
  // JWT: three base64url segments.
32
32
  {
33
33
  name: "jwt",
34
- pattern: /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g,
34
+ pattern: /\beyJ[\w-]{8,1024}\.[\w-]{8,1024}\.[\w-]{8,1024}\b/g,
35
35
  replacement: "[token]",
36
36
  },
37
37
  // A long opaque run — access keys, API keys, session ids. Requires BOTH a digit and a
@@ -41,7 +41,7 @@ const RULES = [
41
41
  // wording a product uses is exactly the assumption that fails.
42
42
  {
43
43
  name: "opaque_token",
44
- pattern: /\b(?=[A-Za-z0-9_-]*\d)(?=[A-Za-z0-9_-]*[A-Za-z])[A-Za-z0-9_-]{20,}\b/g,
44
+ pattern: /\b(?=[A-Za-z0-9_-]{0,256}\d)(?=[A-Za-z0-9_-]{0,256}[A-Za-z])[A-Za-z0-9_-]{20,256}\b/g,
45
45
  replacement: "[token]",
46
46
  },
47
47
  {
@@ -53,7 +53,7 @@ const RULES = [
53
53
  // separator or a `+`, so a bare digit run falls to long_number instead.
54
54
  {
55
55
  name: "phone",
56
- pattern: /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{2,4}\)[\s.-]?)?\d{3,5}[\s.-]\d{3,5}(?:[\s.-]\d{2,5})?\b/g,
56
+ pattern: /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{2,4}\)[\s.-]?)?\b\d{3,5}[\s.-]\d{3,5}(?:[\s.-]\d{2,5})?\b/g,
57
57
  replacement: "[phone]",
58
58
  },
59
59
  // Long bare digit runs: account ids, card numbers, unseparated phone numbers.
@@ -81,7 +81,9 @@ export function redact(text) {
81
81
  try {
82
82
  if (typeof text !== "string")
83
83
  return undefined;
84
- let out = text.replace(/\s+/g, " ").trim();
84
+ // Slice before the rules run; a multi-KB digit run makes them backtrack.
85
+ const SCAN_LIMIT = MAX_LENGTH * 8;
86
+ let out = text.slice(0, SCAN_LIMIT).replace(/\s+/g, " ").trim();
85
87
  if (!out)
86
88
  return undefined;
87
89
  for (const rule of RULES)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",