@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
|
-
|
|
29
|
-
|
|
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.%+-]
|
|
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_-]
|
|
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
|
-
|
|
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)
|