@browserstack/mcp-server 1.5.0-beta.21 → 1.5.0-beta.23
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.
- package/README.md +32 -48
- package/capability/loadtesting.capability-index.json +1876 -0
- package/capability/tm.capability-index.json +50104 -0
- package/dist/config.d.ts +1 -4
- package/dist/config.js +2 -23
- package/dist/index.d.ts +1 -2
- package/dist/index.js +3 -7
- package/dist/lib/constants.d.ts +0 -3
- package/dist/lib/constants.js +0 -3
- package/dist/lib/instrumentation.d.ts +2 -14
- package/dist/lib/instrumentation.js +29 -104
- package/dist/lib/upload-validator.d.ts +2 -3
- package/dist/lib/upload-validator.js +15 -18
- package/dist/lib/utils.js +3 -10
- package/dist/lib/version-resolver.d.ts +5 -0
- package/dist/lib/version-resolver.js +2 -8
- package/dist/logger.js +10 -1
- package/dist/server-factory.js +6 -10
- package/dist/tools/accessibility.js +5 -9
- package/dist/tools/accessiblity-utils/accessibility-rag.js +1 -3
- package/dist/tools/appautomate-utils/native-execution/constants.js +2 -4
- package/dist/tools/appautomate.js +1 -3
- package/dist/tools/applive.js +1 -1
- package/dist/tools/automate.d.ts +0 -7
- package/dist/tools/automate.js +1 -104
- package/dist/tools/bstack-sdk.js +2 -2
- package/dist/tools/build-insights.js +2 -41
- package/dist/tools/capability-registry/bind.d.ts +29 -0
- package/dist/tools/capability-registry/bind.js +329 -0
- package/dist/tools/capability-registry/config.d.ts +62 -0
- package/dist/tools/capability-registry/config.js +218 -0
- package/dist/tools/capability-registry/discovery.d.ts +44 -0
- package/dist/tools/capability-registry/discovery.js +99 -0
- package/dist/tools/capability-registry/egress.d.ts +44 -0
- package/dist/tools/capability-registry/egress.js +174 -0
- package/dist/tools/capability-registry/index-loader.d.ts +133 -0
- package/dist/tools/capability-registry/index-loader.js +404 -0
- package/dist/tools/capability-registry/register.d.ts +48 -0
- package/dist/tools/capability-registry/register.js +604 -0
- package/dist/tools/capability-registry/resolve.d.ts +38 -0
- package/dist/tools/capability-registry/resolve.js +45 -0
- package/dist/tools/capability-registry/search.d.ts +170 -0
- package/dist/tools/capability-registry/search.js +709 -0
- package/dist/tools/capability-registry/types.d.ts +282 -0
- package/dist/tools/capability-registry/types.js +33 -0
- package/dist/tools/failurelogs-utils/app-automate.js +3 -4
- package/dist/tools/failurelogs-utils/automate.js +4 -5
- package/dist/tools/get-failure-logs.js +11 -30
- package/dist/tools/observability.js +1 -3
- package/dist/tools/percy-sdk.js +9 -9
- package/dist/tools/rca-agent-utils/format-rca.js +4 -5
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +0 -12
- package/dist/tools/rca-agent-utils/types.d.ts +0 -1
- package/dist/tools/rca-agent.js +2 -5
- package/dist/tools/review-agent.js +1 -2
- package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
- package/dist/tools/sdk-utils/common/constants.js +1 -2
- package/dist/tools/selfheal.js +3 -6
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +7 -26
- package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +0 -2
- package/dist/tools/testmanagement-utils/TCG-utils/config.js +0 -5
- package/dist/tools/testmanagement-utils/testcase-from-file.js +1 -2
- package/dist/tools/testmanagement-utils/upload-file.js +2 -16
- package/dist/tools/testmanagement.js +17 -39
- package/package.json +11 -5
- package/dist/lib/node-version-notice.d.ts +0 -1
- package/dist/lib/node-version-notice.js +0 -9
- package/dist/lib/tool-latency.d.ts +0 -4
- package/dist/lib/tool-latency.js +0 -16
- package/dist/lib/untrusted-content.d.ts +0 -15
- package/dist/lib/untrusted-content.js +0 -24
- package/dist/tools/ask-browserstack/central-oauth.d.ts +0 -120
- package/dist/tools/ask-browserstack/central-oauth.js +0 -277
- package/dist/tools/ask-browserstack/config.d.ts +0 -102
- package/dist/tools/ask-browserstack/config.js +0 -140
- package/dist/tools/ask-browserstack/egress.d.ts +0 -34
- package/dist/tools/ask-browserstack/egress.js +0 -31
- package/dist/tools/ask-browserstack/register.d.ts +0 -61
- package/dist/tools/ask-browserstack/register.js +0 -416
- package/dist/tools/ask-browserstack/relay.d.ts +0 -201
- package/dist/tools/ask-browserstack/relay.js +0 -577
- package/dist/tools/ask-browserstack/stream.d.ts +0 -116
- package/dist/tools/ask-browserstack/stream.js +0 -236
- package/dist/tools/ask-browserstack/types.d.ts +0 -196
- package/dist/tools/ask-browserstack/types.js +0 -14
- package/dist/tools/automate-utils/list-session-ids.d.ts +0 -28
- package/dist/tools/automate-utils/list-session-ids.js +0 -87
- package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +0 -30
- package/dist/tools/automate-utils/resolve-hashed-build-id.js +0 -124
- package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +0 -2
- package/dist/tools/failurelogs-utils/resolve-app-build-id.js +0 -5
- package/dist/tools/failurelogs-utils/video.d.ts +0 -3
- package/dist/tools/failurelogs-utils/video.js +0 -25
- package/dist/tools/tool-handoff.d.ts +0 -66
- package/dist/tools/tool-handoff.js +0 -78
package/dist/config.d.ts
CHANGED
|
@@ -11,10 +11,7 @@ export declare class Config {
|
|
|
11
11
|
readonly O11Y_TFA_RCA_BASE_URL: string;
|
|
12
12
|
readonly BROWSERSTACK_AUTOMATION_BASE_URL: string;
|
|
13
13
|
readonly BROWSERSTACK_O11Y_UI_BASE_URL: string;
|
|
14
|
-
|
|
15
|
-
readonly ASK_BROWSERSTACK_ATLAS_URL: string | undefined;
|
|
16
|
-
readonly ASK_BROWSERSTACK_AUTH_TOKEN_URL: string | undefined;
|
|
17
|
-
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean, UPLOAD_BASE_DIR: string | undefined, O11Y_TFA_RCA_BASE_URL: string, BROWSERSTACK_AUTOMATION_BASE_URL: string, BROWSERSTACK_O11Y_UI_BASE_URL: string, ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY: boolean, ASK_BROWSERSTACK_ATLAS_URL: string | undefined, ASK_BROWSERSTACK_AUTH_TOKEN_URL: string | undefined);
|
|
14
|
+
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean, UPLOAD_BASE_DIR: string | undefined, O11Y_TFA_RCA_BASE_URL: string, BROWSERSTACK_AUTOMATION_BASE_URL: string, BROWSERSTACK_O11Y_UI_BASE_URL: string);
|
|
18
15
|
}
|
|
19
16
|
declare const config: Config;
|
|
20
17
|
export default config;
|
package/dist/config.js
CHANGED
|
@@ -47,18 +47,7 @@ export class Config {
|
|
|
47
47
|
O11Y_TFA_RCA_BASE_URL;
|
|
48
48
|
BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
49
49
|
BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
50
|
-
|
|
51
|
-
ASK_BROWSERSTACK_ATLAS_URL;
|
|
52
|
-
ASK_BROWSERSTACK_AUTH_TOKEN_URL;
|
|
53
|
-
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP, UPLOAD_BASE_DIR, O11Y_TFA_RCA_BASE_URL, BROWSERSTACK_AUTOMATION_BASE_URL, BROWSERSTACK_O11Y_UI_BASE_URL,
|
|
54
|
-
// askBrowserStackAI's process-startup settings. Declared here rather than read from
|
|
55
|
-
// process.env inside src/tools/, per rules/tool-design.md — and so the remote wrapper,
|
|
56
|
-
// which only forwards env it knows about, has one place to look.
|
|
57
|
-
//
|
|
58
|
-
// ASK_BROWSERSTACK_DISABLED is deliberately NOT here: it is a kill switch, and reading
|
|
59
|
-
// it per call keeps it effective without a restart. Fixing it at boot would mean a pod
|
|
60
|
-
// roll to disable the tool, which is slowest exactly when you need it fastest.
|
|
61
|
-
ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY, ASK_BROWSERSTACK_ATLAS_URL, ASK_BROWSERSTACK_AUTH_TOKEN_URL) {
|
|
50
|
+
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP, UPLOAD_BASE_DIR, O11Y_TFA_RCA_BASE_URL, BROWSERSTACK_AUTOMATION_BASE_URL, BROWSERSTACK_O11Y_UI_BASE_URL) {
|
|
62
51
|
this.DEV_MODE = DEV_MODE;
|
|
63
52
|
this.browserstackLocalOptions = browserstackLocalOptions;
|
|
64
53
|
this.USE_OWN_LOCAL_BINARY_PROCESS = USE_OWN_LOCAL_BINARY_PROCESS;
|
|
@@ -67,9 +56,6 @@ export class Config {
|
|
|
67
56
|
this.O11Y_TFA_RCA_BASE_URL = O11Y_TFA_RCA_BASE_URL;
|
|
68
57
|
this.BROWSERSTACK_AUTOMATION_BASE_URL = BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
69
58
|
this.BROWSERSTACK_O11Y_UI_BASE_URL = BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
70
|
-
this.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY = ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY;
|
|
71
|
-
this.ASK_BROWSERSTACK_ATLAS_URL = ASK_BROWSERSTACK_ATLAS_URL;
|
|
72
|
-
this.ASK_BROWSERSTACK_AUTH_TOKEN_URL = ASK_BROWSERSTACK_AUTH_TOKEN_URL;
|
|
73
59
|
}
|
|
74
60
|
}
|
|
75
61
|
const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOptions, process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true", process.env.REMOTE_MCP === "true", process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0
|
|
@@ -83,12 +69,5 @@ const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOpti
|
|
|
83
69
|
: DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL, process.env.BROWSERSTACK_O11Y_UI_BASE_URL &&
|
|
84
70
|
process.env.BROWSERSTACK_O11Y_UI_BASE_URL.length > 0
|
|
85
71
|
? process.env.BROWSERSTACK_O11Y_UI_BASE_URL
|
|
86
|
-
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL
|
|
87
|
-
"true", process.env.ASK_BROWSERSTACK_ATLAS_URL &&
|
|
88
|
-
process.env.ASK_BROWSERSTACK_ATLAS_URL.trim().length > 0
|
|
89
|
-
? process.env.ASK_BROWSERSTACK_ATLAS_URL
|
|
90
|
-
: undefined, process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL &&
|
|
91
|
-
process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL.trim().length > 0
|
|
92
|
-
? process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL
|
|
93
|
-
: undefined);
|
|
72
|
+
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL);
|
|
94
73
|
export default config;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
export { setLogger } from "./logger.js";
|
|
4
4
|
export { BrowserStackMcpServer } from "./server-factory.js";
|
|
5
|
-
export { trackMCP
|
|
6
|
-
export { instrumentToolLatency } from "./lib/tool-latency.js";
|
|
5
|
+
export { trackMCP } from "./lib/instrumentation.js";
|
|
7
6
|
export { default as addTfaRcaCollaborationTools } from "./tools/tfa-rca-collaboration.js";
|
|
8
7
|
export declare const PackageJsonVersion: any;
|
package/dist/index.js
CHANGED
|
@@ -29,16 +29,12 @@ async function main() {
|
|
|
29
29
|
await mcpServer.getInstance().connect(transport);
|
|
30
30
|
}
|
|
31
31
|
main().catch(console.error);
|
|
32
|
-
// Ensure logs are flushed before exit
|
|
33
|
-
// be the thing that throws: `flush` is absent from every `vi.mock("src/logger")` in the
|
|
34
|
-
// suite, so any test file that loads this module used to fail the run with
|
|
35
|
-
// `default.flush is not a function` depending on how vitest scheduled workers.
|
|
32
|
+
// Ensure logs are flushed before exit
|
|
36
33
|
process.on("exit", () => {
|
|
37
|
-
logger.flush
|
|
34
|
+
logger.flush();
|
|
38
35
|
});
|
|
39
36
|
export { setLogger } from "./logger.js";
|
|
40
37
|
export { BrowserStackMcpServer } from "./server-factory.js";
|
|
41
|
-
export { trackMCP
|
|
42
|
-
export { instrumentToolLatency } from "./lib/tool-latency.js";
|
|
38
|
+
export { trackMCP } from "./lib/instrumentation.js";
|
|
43
39
|
export { default as addTfaRcaCollaborationTools } from "./tools/tfa-rca-collaboration.js";
|
|
44
40
|
export const PackageJsonVersion = packageJson.version;
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -2,18 +2,15 @@ export declare const SessionType: {
|
|
|
2
2
|
readonly Automate: "automate";
|
|
3
3
|
readonly AppAutomate: "app-automate";
|
|
4
4
|
};
|
|
5
|
-
export declare const SessionVideoLogType: "video";
|
|
6
5
|
export declare const AutomateLogType: {
|
|
7
6
|
readonly NetworkLogs: "networkLogs";
|
|
8
7
|
readonly SessionLogs: "sessionLogs";
|
|
9
8
|
readonly ConsoleLogs: "consoleLogs";
|
|
10
|
-
readonly Video: "video";
|
|
11
9
|
};
|
|
12
10
|
export declare const AppAutomateLogType: {
|
|
13
11
|
readonly DeviceLogs: "deviceLogs";
|
|
14
12
|
readonly AppiumLogs: "appiumLogs";
|
|
15
13
|
readonly CrashLogs: "crashLogs";
|
|
16
|
-
readonly Video: "video";
|
|
17
14
|
};
|
|
18
15
|
export type SessionType = (typeof SessionType)[keyof typeof SessionType];
|
|
19
16
|
export type AutomateLogType = (typeof AutomateLogType)[keyof typeof AutomateLogType];
|
package/dist/lib/constants.js
CHANGED
|
@@ -2,16 +2,13 @@ export const SessionType = {
|
|
|
2
2
|
Automate: "automate",
|
|
3
3
|
AppAutomate: "app-automate",
|
|
4
4
|
};
|
|
5
|
-
export const SessionVideoLogType = "video";
|
|
6
5
|
export const AutomateLogType = {
|
|
7
6
|
NetworkLogs: "networkLogs",
|
|
8
7
|
SessionLogs: "sessionLogs",
|
|
9
8
|
ConsoleLogs: "consoleLogs",
|
|
10
|
-
Video: SessionVideoLogType,
|
|
11
9
|
};
|
|
12
10
|
export const AppAutomateLogType = {
|
|
13
11
|
DeviceLogs: "deviceLogs",
|
|
14
12
|
AppiumLogs: "appiumLogs",
|
|
15
13
|
CrashLogs: "crashLogs",
|
|
16
|
-
Video: SessionVideoLogType,
|
|
17
14
|
};
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare function trackMCP(toolName: string, clientInfo: {
|
|
2
2
|
name?: string;
|
|
3
3
|
version?: string;
|
|
4
|
-
};
|
|
5
|
-
export type ToolOutcome = "ok" | "error_result" | "threw";
|
|
6
|
-
/**
|
|
7
|
-
* Inside `withToolCall` this only records into the call's context; the single row is
|
|
8
|
-
* written when the handler settles. Outside one (`started` heartbeat, unwrapped host
|
|
9
|
-
* tools) it posts a row immediately, as before.
|
|
10
|
-
*/
|
|
11
|
-
export declare function trackMCP(toolName: string, clientInfo: ClientInfo, error?: unknown, config?: any): void;
|
|
12
|
-
/**
|
|
13
|
-
* Runs a tool handler and writes one MCPInstrumentation row when it settles: success,
|
|
14
|
-
* duration_ms, outcome (ok / error_result / threw), error fields on failure.
|
|
15
|
-
*/
|
|
16
|
-
export declare function withToolCall<T>(toolName: string, getClientInfo: () => ClientInfo, config: any, fn: () => Promise<T> | T): Promise<T>;
|
|
4
|
+
}, error?: unknown, config?: any): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
1
|
import logger from "../logger.js";
|
|
3
2
|
import { getBrowserStackAuth } from "./get-auth.js";
|
|
4
3
|
import { createRequire } from "module";
|
|
@@ -6,31 +5,42 @@ const require = createRequire(import.meta.url);
|
|
|
6
5
|
const packageJson = require("../../package.json");
|
|
7
6
|
import { apiClient } from "./apiClient.js";
|
|
8
7
|
import globalConfig from "../config.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
export function trackMCP(toolName, clientInfo, error, config) {
|
|
9
|
+
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
10
|
+
const isSuccess = !error;
|
|
11
|
+
const mcpClient = clientInfo?.name || "unknown";
|
|
12
|
+
// Log client information
|
|
13
|
+
if (clientInfo?.name) {
|
|
14
|
+
logger.info(`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
logger.info("Client connected: unknown client");
|
|
18
|
+
}
|
|
19
|
+
const event = {
|
|
20
|
+
event_type: "MCPInstrumentation",
|
|
21
|
+
event_properties: {
|
|
22
|
+
mcp_version: packageJson.version,
|
|
23
|
+
tool_name: toolName,
|
|
24
|
+
mcp_client: mcpClient,
|
|
25
|
+
success: isSuccess,
|
|
26
|
+
is_remote: globalConfig.REMOTE_MCP,
|
|
27
|
+
},
|
|
23
28
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
// Add error details if applicable
|
|
30
|
+
if (error) {
|
|
31
|
+
event.event_properties.error_message =
|
|
32
|
+
error instanceof Error ? error.message : String(error);
|
|
33
|
+
event.event_properties.error_type =
|
|
34
|
+
error instanceof Error ? error.constructor.name : "Unknown";
|
|
35
|
+
}
|
|
36
|
+
let authHeader = undefined;
|
|
27
37
|
if (config) {
|
|
28
38
|
const authString = getBrowserStackAuth(config);
|
|
29
39
|
authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
|
|
30
40
|
}
|
|
31
41
|
apiClient
|
|
32
42
|
.post({
|
|
33
|
-
url:
|
|
43
|
+
url: instrumentationEndpoint,
|
|
34
44
|
body: event,
|
|
35
45
|
headers: {
|
|
36
46
|
"Content-Type": "application/json",
|
|
@@ -41,88 +51,3 @@ function sendEvent(event, config) {
|
|
|
41
51
|
})
|
|
42
52
|
.catch(() => { });
|
|
43
53
|
}
|
|
44
|
-
const callContext = new AsyncLocalStorage();
|
|
45
|
-
/**
|
|
46
|
-
* Inside `withToolCall` this only records into the call's context; the single row is
|
|
47
|
-
* written when the handler settles. Outside one (`started` heartbeat, unwrapped host
|
|
48
|
-
* tools) it posts a row immediately, as before.
|
|
49
|
-
*/
|
|
50
|
-
export function trackMCP(toolName, clientInfo, error, config) {
|
|
51
|
-
const ctx = callContext.getStore();
|
|
52
|
-
if (ctx) {
|
|
53
|
-
if (clientInfo?.name && !ctx.clientInfo?.name)
|
|
54
|
-
ctx.clientInfo = clientInfo;
|
|
55
|
-
if (config && !ctx.config)
|
|
56
|
-
ctx.config = config;
|
|
57
|
-
if (error)
|
|
58
|
-
ctx.error = error;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (clientInfo?.name) {
|
|
62
|
-
logger.info(`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
logger.info("Client connected: unknown client");
|
|
66
|
-
}
|
|
67
|
-
const event = {
|
|
68
|
-
event_type: "MCPInstrumentation",
|
|
69
|
-
event_properties: {
|
|
70
|
-
...baseProperties(toolName, clientInfo),
|
|
71
|
-
success: !error,
|
|
72
|
-
...(error ? errorProperties(error) : {}),
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
sendEvent(event, config);
|
|
76
|
-
}
|
|
77
|
-
function isErrorResult(result) {
|
|
78
|
-
return (typeof result === "object" &&
|
|
79
|
-
result !== null &&
|
|
80
|
-
result.isError === true);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Runs a tool handler and writes one MCPInstrumentation row when it settles: success,
|
|
84
|
-
* duration_ms, outcome (ok / error_result / threw), error fields on failure.
|
|
85
|
-
*/
|
|
86
|
-
export async function withToolCall(toolName, getClientInfo, config, fn) {
|
|
87
|
-
const ctx = { toolName, clientInfo: {}, config };
|
|
88
|
-
const startedAt = performance.now();
|
|
89
|
-
let outcome = "ok";
|
|
90
|
-
try {
|
|
91
|
-
const result = await callContext.run(ctx, fn);
|
|
92
|
-
if (isErrorResult(result))
|
|
93
|
-
outcome = "error_result";
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
outcome = "threw";
|
|
98
|
-
ctx.error ??= error;
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
try {
|
|
103
|
-
let clientInfo = ctx.clientInfo;
|
|
104
|
-
try {
|
|
105
|
-
const live = getClientInfo();
|
|
106
|
-
if (live?.name)
|
|
107
|
-
clientInfo = live;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
/* client info is optional */
|
|
111
|
-
}
|
|
112
|
-
const event = {
|
|
113
|
-
event_type: "MCPInstrumentation",
|
|
114
|
-
event_properties: {
|
|
115
|
-
...baseProperties(toolName, clientInfo),
|
|
116
|
-
success: ctx.error === undefined && outcome !== "threw",
|
|
117
|
-
duration_ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
118
|
-
outcome,
|
|
119
|
-
...(ctx.error !== undefined ? errorProperties(ctx.error) : {}),
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
sendEvent(event, ctx.config ?? config);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
/* telemetry must never affect the call */
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface UploadValidationOptions {
|
|
2
2
|
allowedExtensions: readonly string[];
|
|
3
3
|
maxSizeBytes: number;
|
|
4
|
-
allowedBaseDir
|
|
4
|
+
allowedBaseDir?: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Canonicalizes and validates a user-supplied upload path. Returns the resolved
|
|
@@ -13,8 +13,7 @@ export interface UploadValidationOptions {
|
|
|
13
13
|
* - File extension is in `allowedExtensions` (case-insensitive)
|
|
14
14
|
* - No path segment is a hidden dir/file (starts with `.`); blocks ~/.ssh,
|
|
15
15
|
* ~/.aws, .env, etc. even after symlink resolution
|
|
16
|
-
* - `allowedBaseDir` is
|
|
17
|
-
* (via MCP_UPLOAD_BASE_DIR), and the canonical path must live inside it
|
|
16
|
+
* - If `allowedBaseDir` is set, the canonical path must live inside it
|
|
18
17
|
*/
|
|
19
18
|
export declare function validateUploadPath(filePath: string, options: UploadValidationOptions): string;
|
|
20
19
|
export declare const APP_BINARY_EXTENSIONS: readonly [".apk", ".aab", ".ipa", ".app", ".zip"];
|
|
@@ -10,8 +10,7 @@ import path from "path";
|
|
|
10
10
|
* - File extension is in `allowedExtensions` (case-insensitive)
|
|
11
11
|
* - No path segment is a hidden dir/file (starts with `.`); blocks ~/.ssh,
|
|
12
12
|
* ~/.aws, .env, etc. even after symlink resolution
|
|
13
|
-
* - `allowedBaseDir` is
|
|
14
|
-
* (via MCP_UPLOAD_BASE_DIR), and the canonical path must live inside it
|
|
13
|
+
* - If `allowedBaseDir` is set, the canonical path must live inside it
|
|
15
14
|
*/
|
|
16
15
|
export function validateUploadPath(filePath, options) {
|
|
17
16
|
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
@@ -49,22 +48,20 @@ export function validateUploadPath(filePath, options) {
|
|
|
49
48
|
if (!allowed.includes(ext)) {
|
|
50
49
|
throw new Error(`Upload rejected: file extension "${ext || "(none)"}" is not in the allowed list (${allowed.join(", ")}).`);
|
|
51
50
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (canonical !== baseCanonical && !canonical.startsWith(baseWithSep)) {
|
|
67
|
-
throw new Error(`Upload rejected: file must be located inside ${baseCanonical}.`);
|
|
51
|
+
if (options.allowedBaseDir) {
|
|
52
|
+
let baseCanonical;
|
|
53
|
+
try {
|
|
54
|
+
baseCanonical = fs.realpathSync(path.resolve(options.allowedBaseDir));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`Upload rejected: configured MCP_UPLOAD_BASE_DIR does not exist (${options.allowedBaseDir}).`);
|
|
58
|
+
}
|
|
59
|
+
const baseWithSep = baseCanonical.endsWith(path.sep)
|
|
60
|
+
? baseCanonical
|
|
61
|
+
: baseCanonical + path.sep;
|
|
62
|
+
if (canonical !== baseCanonical && !canonical.startsWith(baseWithSep)) {
|
|
63
|
+
throw new Error(`Upload rejected: file must be located inside ${baseCanonical}.`);
|
|
64
|
+
}
|
|
68
65
|
}
|
|
69
66
|
return canonical;
|
|
70
67
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
1
2
|
import { getBrowserStackAuth } from "./get-auth.js";
|
|
2
3
|
import { trackMCP } from "../index.js";
|
|
3
|
-
import logger from "../logger.js";
|
|
4
4
|
export function sanitizeUrlParam(param) {
|
|
5
5
|
// Remove any characters that could be used for command injection
|
|
6
6
|
return param.replace(/[;&|`$(){}[\]<>]/g, "");
|
|
@@ -15,15 +15,8 @@ export async function maybeCompressBase64(base64) {
|
|
|
15
15
|
const sizeRatio = 1048576 / buffer.length;
|
|
16
16
|
const estimatedQuality = Math.floor(sizeRatio * 100);
|
|
17
17
|
const quality = Math.min(95, Math.max(30, estimatedQuality));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer();
|
|
21
|
-
return compressedBuffer.toString("base64");
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
logger.warn("Image compression unavailable (sharp failed to load); returning the uncompressed image. %s", err instanceof Error ? err.message : String(err));
|
|
25
|
-
return base64;
|
|
26
|
-
}
|
|
18
|
+
const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer();
|
|
19
|
+
return compressedBuffer.toString("base64");
|
|
27
20
|
}
|
|
28
21
|
export async function assertOkResponse(response, action) {
|
|
29
22
|
if (!response.ok) {
|
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
* Else if exact match, returns that
|
|
4
4
|
* Else picks the numerically closest (or first)
|
|
5
5
|
*/
|
|
6
|
-
const PRERELEASE_CHANNEL = /\b(beta|dev|alpha|canary|nightly|preview)\b/i;
|
|
7
6
|
export function resolveVersion(requested, available) {
|
|
8
7
|
// strip duplicates & sort
|
|
9
8
|
const uniq = Array.from(new Set(available));
|
|
10
9
|
// pick min/max
|
|
11
10
|
if (requested === "latest" || requested === "oldest") {
|
|
12
|
-
// Prefer stable releases: BrowserStack lists pre-release channels such as
|
|
13
|
-
// "154.0 beta" / "155.0 dev" alongside stable versions, and "latest"
|
|
14
|
-
// should never resolve to one of those while a stable version exists.
|
|
15
|
-
const stable = uniq.filter((v) => !PRERELEASE_CHANNEL.test(v));
|
|
16
|
-
const candidates = stable.length > 0 ? stable : uniq;
|
|
17
11
|
// try numeric
|
|
18
|
-
const nums =
|
|
12
|
+
const nums = uniq
|
|
19
13
|
.map((v) => ({ v, n: parseFloat(v) }))
|
|
20
14
|
.filter((x) => !isNaN(x.n))
|
|
21
15
|
.sort((a, b) => a.n - b.n);
|
|
@@ -23,7 +17,7 @@ export function resolveVersion(requested, available) {
|
|
|
23
17
|
return requested === "latest" ? nums[nums.length - 1].v : nums[0].v;
|
|
24
18
|
}
|
|
25
19
|
// fallback lex
|
|
26
|
-
const lex =
|
|
20
|
+
const lex = uniq.slice().sort();
|
|
27
21
|
return requested === "latest" ? lex[lex.length - 1] : lex[0];
|
|
28
22
|
}
|
|
29
23
|
// exact match?
|
package/dist/logger.js
CHANGED
|
@@ -22,7 +22,16 @@ if (process.env.NODE_ENV === "development") {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
|
-
|
|
25
|
+
// Null logger (logs go to /dev/null or NUL)
|
|
26
|
+
currentLogger = pino({
|
|
27
|
+
level: "info",
|
|
28
|
+
transport: {
|
|
29
|
+
target: "pino/file",
|
|
30
|
+
options: {
|
|
31
|
+
destination: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
26
35
|
}
|
|
27
36
|
// 2. Proxy logger: always delegates to the currentLogger
|
|
28
37
|
const logger = new Proxy({}, {
|
package/dist/server-factory.js
CHANGED
|
@@ -16,9 +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
|
|
20
|
-
import { instrumentToolLatency } from "./lib/tool-latency.js";
|
|
21
|
-
import { nodeUpgradeNotice } from "./lib/node-version-notice.js";
|
|
19
|
+
import addCapabilityRegistryTools from "./tools/capability-registry/register.js";
|
|
22
20
|
/**
|
|
23
21
|
* Wrapper class for BrowserStack MCP Server
|
|
24
22
|
* Stores a map of registered tools by name
|
|
@@ -33,7 +31,7 @@ export class BrowserStackMcpServer {
|
|
|
33
31
|
this.server = new McpServer({
|
|
34
32
|
name: "BrowserStack MCP Server",
|
|
35
33
|
version: packageJson.version,
|
|
36
|
-
}
|
|
34
|
+
});
|
|
37
35
|
setupOnInitialized(this.server, this.config);
|
|
38
36
|
this.registerTools();
|
|
39
37
|
}
|
|
@@ -54,18 +52,16 @@ export class BrowserStackMcpServer {
|
|
|
54
52
|
addSelfHealTools,
|
|
55
53
|
addBuildInsightsTools,
|
|
56
54
|
addRCATools,
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
55
|
+
// Driven by a prebuilt index rather than hand-written per endpoint. Registers
|
|
56
|
+
// nothing (and logs why) when the artifact is absent, so a packaging problem cannot
|
|
57
|
+
// take the other products' tools down with it.
|
|
58
|
+
addCapabilityRegistryTools,
|
|
61
59
|
];
|
|
62
60
|
toolAdders.forEach((adder) => {
|
|
63
61
|
// Each adder now returns a Record<string, Tool>
|
|
64
62
|
const added = adder(this.server, this.config);
|
|
65
63
|
Object.assign(this.tools, added);
|
|
66
64
|
});
|
|
67
|
-
// Client info is read at call time; it is empty until initialize arrives.
|
|
68
|
-
instrumentToolLatency(this.tools, () => this.server.server.getClientVersion() ?? {}, this.config);
|
|
69
65
|
}
|
|
70
66
|
/**
|
|
71
67
|
* Expose the underlying MCP server instance
|
|
@@ -4,12 +4,10 @@ import { AccessibilityReportFetcher } from "./accessiblity-utils/report-fetcher.
|
|
|
4
4
|
import { AccessibilityAuthConfig, safeAuthConfigData, } from "./accessiblity-utils/auth-config.js";
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
6
|
import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
|
|
7
|
-
import { wrapUntrusted } from "../lib/untrusted-content.js";
|
|
8
7
|
import { queryAccessibilityRAG } from "./accessiblity-utils/accessibility-rag.js";
|
|
9
8
|
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
10
9
|
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
11
10
|
import logger from "../logger.js";
|
|
12
|
-
import { NEEDS_A11Y_CONFIG_ID, NEEDS_A11Y_SCAN_ID } from "./tool-handoff.js";
|
|
13
11
|
function setupAuth(config) {
|
|
14
12
|
const authString = getBrowserStackAuth(config);
|
|
15
13
|
const [username, password] = authString.split(":");
|
|
@@ -91,7 +89,7 @@ async function fetchAccessibilityIssues(scanId, scanRunId, config, cursor = 0) {
|
|
|
91
89
|
const remainingIssues = total_issues - currentlyShown;
|
|
92
90
|
const messages = [
|
|
93
91
|
`Retrieved ${page_length} accessibility issues (Total: ${total_issues})`,
|
|
94
|
-
`Issues: ${
|
|
92
|
+
`Issues: ${JSON.stringify(records, null, 2)}`,
|
|
95
93
|
];
|
|
96
94
|
if (next_page !== null) {
|
|
97
95
|
messages.push(`${remainingIssues} more issues available. Use fetchAccessibilityIssues with cursor: ${next_page} to get the next batch.`);
|
|
@@ -187,7 +185,7 @@ function createScanSuccessResponse(name, totalIssues, pageLength, records, scanI
|
|
|
187
185
|
`Scan ID: ${scanId} and Scan Run ID: ${scanRunId}`,
|
|
188
186
|
`You can also download the full report from the following link: ${reportUrl}`,
|
|
189
187
|
`We found ${totalIssues} issues. Below are the details of the ${pageLength} most critical issues.`,
|
|
190
|
-
`Scan results: ${
|
|
188
|
+
`Scan results: ${JSON.stringify(records, null, 2)}`,
|
|
191
189
|
];
|
|
192
190
|
if (cursor !== null) {
|
|
193
191
|
messages.push(`More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and cursor: ${cursor} to get the next batch.`);
|
|
@@ -238,7 +236,7 @@ export default function addAccessibilityTools(server, config) {
|
|
|
238
236
|
}, {
|
|
239
237
|
title: "Start Accessibility Scan",
|
|
240
238
|
readOnlyHint: false,
|
|
241
|
-
openWorldHint:
|
|
239
|
+
openWorldHint: false,
|
|
242
240
|
destructiveHint: false,
|
|
243
241
|
idempotentHint: false,
|
|
244
242
|
}, async (args, context) => {
|
|
@@ -305,8 +303,7 @@ export default function addAccessibilityTools(server, config) {
|
|
|
305
303
|
return handleMCPError("createAccessibilityAuthConfig", server, config, error);
|
|
306
304
|
}
|
|
307
305
|
});
|
|
308
|
-
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID."
|
|
309
|
-
NEEDS_A11Y_CONFIG_ID, {
|
|
306
|
+
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID.", {
|
|
310
307
|
configId: z.number().describe("ID of the auth configuration to retrieve"),
|
|
311
308
|
}, {
|
|
312
309
|
title: "Get Accessibility Auth Config",
|
|
@@ -317,8 +314,7 @@ export default function addAccessibilityTools(server, config) {
|
|
|
317
314
|
}, async (args) => {
|
|
318
315
|
return await executeGetAuthConfig(args, server, config);
|
|
319
316
|
});
|
|
320
|
-
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results."
|
|
321
|
-
NEEDS_A11Y_SCAN_ID, {
|
|
317
|
+
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results.", {
|
|
322
318
|
scanId: z
|
|
323
319
|
.string()
|
|
324
320
|
.describe("The scan ID from a completed accessibility scan"),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
-
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
3
2
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
4
3
|
export async function queryAccessibilityRAG(userQuery, config) {
|
|
5
4
|
const url = "https://accessibility.browserstack.com/api/tcg-proxy/search";
|
|
@@ -46,8 +45,7 @@ export async function queryAccessibilityRAG(userQuery, config) {
|
|
|
46
45
|
const formattedChunks = chunks
|
|
47
46
|
.map((chunk, index) => `${index + 1}: Source: ${chunk.url}\n\n${chunk.content}`)
|
|
48
47
|
.join("\n\n---\n\n");
|
|
49
|
-
const formattedResponse = instruction +
|
|
50
|
-
wrapUntrusted("BrowserStack accessibility documentation", formattedChunks);
|
|
48
|
+
const formattedResponse = instruction + formattedChunks;
|
|
51
49
|
return {
|
|
52
50
|
content: [
|
|
53
51
|
{
|
|
@@ -20,8 +20,7 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
|
|
|
20
20
|
" xcodebuild clean -scheme YOUR_SCHEME && \\\n" +
|
|
21
21
|
" xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" +
|
|
22
22
|
" xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" +
|
|
23
|
-
"If in other directory, provide existing app path
|
|
24
|
-
"The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
23
|
+
"If in other directory, provide existing app path"),
|
|
25
24
|
testSuitePath: z
|
|
26
25
|
.string()
|
|
27
26
|
.describe("Path to your test suite file:\n" +
|
|
@@ -31,8 +30,7 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
|
|
|
31
30
|
" xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" +
|
|
32
31
|
" cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" +
|
|
33
32
|
" zip -r Tests.zip *.xctestrun *-Runner.app\n\n" +
|
|
34
|
-
"If in other directory, provide existing test file path
|
|
35
|
-
"The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
33
|
+
"If in other directory, provide existing test file path"),
|
|
36
34
|
devices: z
|
|
37
35
|
.array(MobileDeviceSchema)
|
|
38
36
|
.max(3)
|
|
@@ -206,7 +206,7 @@ export default function addAppAutomationTools(server, config) {
|
|
|
206
206
|
.describe("Platform to run the app on. Either 'android' or 'ios'."),
|
|
207
207
|
appPath: z
|
|
208
208
|
.string()
|
|
209
|
-
.describe("The path to the .apk or .ipa file. Required for app installation.
|
|
209
|
+
.describe("The path to the .apk or .ipa file. Required for app installation."),
|
|
210
210
|
}, {
|
|
211
211
|
title: "Take App Screenshot",
|
|
212
212
|
readOnlyHint: false,
|
|
@@ -266,11 +266,9 @@ export default function addAppAutomationTools(server, config) {
|
|
|
266
266
|
idempotentHint: true,
|
|
267
267
|
}, async (args) => {
|
|
268
268
|
try {
|
|
269
|
-
trackMCP("setupBrowserStackAppAutomateTests", server.server.getClientVersion(), undefined, config);
|
|
270
269
|
return await setupAppAutomateHandler(args, config);
|
|
271
270
|
}
|
|
272
271
|
catch (error) {
|
|
273
|
-
trackMCP("setupBrowserStackAppAutomateTests", server.server.getClientVersion(), error, config);
|
|
274
272
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
275
273
|
return {
|
|
276
274
|
content: [
|
package/dist/tools/applive.js
CHANGED
|
@@ -66,7 +66,7 @@ export default function addAppLiveTools(server, config) {
|
|
|
66
66
|
.describe("Which platform to run on, examples: 'android', 'ios'. Set this based on the app path provided."),
|
|
67
67
|
appPath: z
|
|
68
68
|
.string()
|
|
69
|
-
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it.
|
|
69
|
+
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it."),
|
|
70
70
|
}, {
|
|
71
71
|
title: "Run App Live Session",
|
|
72
72
|
readOnlyHint: false,
|
package/dist/tools/automate.d.ts
CHANGED
|
@@ -6,11 +6,4 @@ export declare function fetchAutomationScreenshotsTool(args: {
|
|
|
6
6
|
sessionId: string;
|
|
7
7
|
sessionType: SessionType;
|
|
8
8
|
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
9
|
-
export declare function listSessionIdsTool(args: {
|
|
10
|
-
sessionType: SessionType;
|
|
11
|
-
buildId: string;
|
|
12
|
-
limit?: number;
|
|
13
|
-
offset?: number;
|
|
14
|
-
status?: string;
|
|
15
|
-
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
16
9
|
export default function addAutomationTools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
|