@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.
Files changed (95) hide show
  1. package/README.md +32 -48
  2. package/capability/loadtesting.capability-index.json +1876 -0
  3. package/capability/tm.capability-index.json +50104 -0
  4. package/dist/config.d.ts +1 -4
  5. package/dist/config.js +2 -23
  6. package/dist/index.d.ts +1 -2
  7. package/dist/index.js +3 -7
  8. package/dist/lib/constants.d.ts +0 -3
  9. package/dist/lib/constants.js +0 -3
  10. package/dist/lib/instrumentation.d.ts +2 -14
  11. package/dist/lib/instrumentation.js +29 -104
  12. package/dist/lib/upload-validator.d.ts +2 -3
  13. package/dist/lib/upload-validator.js +15 -18
  14. package/dist/lib/utils.js +3 -10
  15. package/dist/lib/version-resolver.d.ts +5 -0
  16. package/dist/lib/version-resolver.js +2 -8
  17. package/dist/logger.js +10 -1
  18. package/dist/server-factory.js +6 -10
  19. package/dist/tools/accessibility.js +5 -9
  20. package/dist/tools/accessiblity-utils/accessibility-rag.js +1 -3
  21. package/dist/tools/appautomate-utils/native-execution/constants.js +2 -4
  22. package/dist/tools/appautomate.js +1 -3
  23. package/dist/tools/applive.js +1 -1
  24. package/dist/tools/automate.d.ts +0 -7
  25. package/dist/tools/automate.js +1 -104
  26. package/dist/tools/bstack-sdk.js +2 -2
  27. package/dist/tools/build-insights.js +2 -41
  28. package/dist/tools/capability-registry/bind.d.ts +29 -0
  29. package/dist/tools/capability-registry/bind.js +329 -0
  30. package/dist/tools/capability-registry/config.d.ts +62 -0
  31. package/dist/tools/capability-registry/config.js +218 -0
  32. package/dist/tools/capability-registry/discovery.d.ts +44 -0
  33. package/dist/tools/capability-registry/discovery.js +99 -0
  34. package/dist/tools/capability-registry/egress.d.ts +44 -0
  35. package/dist/tools/capability-registry/egress.js +174 -0
  36. package/dist/tools/capability-registry/index-loader.d.ts +133 -0
  37. package/dist/tools/capability-registry/index-loader.js +404 -0
  38. package/dist/tools/capability-registry/register.d.ts +48 -0
  39. package/dist/tools/capability-registry/register.js +604 -0
  40. package/dist/tools/capability-registry/resolve.d.ts +38 -0
  41. package/dist/tools/capability-registry/resolve.js +45 -0
  42. package/dist/tools/capability-registry/search.d.ts +170 -0
  43. package/dist/tools/capability-registry/search.js +709 -0
  44. package/dist/tools/capability-registry/types.d.ts +282 -0
  45. package/dist/tools/capability-registry/types.js +33 -0
  46. package/dist/tools/failurelogs-utils/app-automate.js +3 -4
  47. package/dist/tools/failurelogs-utils/automate.js +4 -5
  48. package/dist/tools/get-failure-logs.js +11 -30
  49. package/dist/tools/observability.js +1 -3
  50. package/dist/tools/percy-sdk.js +9 -9
  51. package/dist/tools/rca-agent-utils/format-rca.js +4 -5
  52. package/dist/tools/rca-agent-utils/get-failed-test-id.js +0 -12
  53. package/dist/tools/rca-agent-utils/types.d.ts +0 -1
  54. package/dist/tools/rca-agent.js +2 -5
  55. package/dist/tools/review-agent.js +1 -2
  56. package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
  57. package/dist/tools/sdk-utils/common/constants.js +1 -2
  58. package/dist/tools/selfheal.js +3 -6
  59. package/dist/tools/testmanagement-utils/TCG-utils/api.js +7 -26
  60. package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +0 -2
  61. package/dist/tools/testmanagement-utils/TCG-utils/config.js +0 -5
  62. package/dist/tools/testmanagement-utils/testcase-from-file.js +1 -2
  63. package/dist/tools/testmanagement-utils/upload-file.js +2 -16
  64. package/dist/tools/testmanagement.js +17 -39
  65. package/package.json +11 -5
  66. package/dist/lib/node-version-notice.d.ts +0 -1
  67. package/dist/lib/node-version-notice.js +0 -9
  68. package/dist/lib/tool-latency.d.ts +0 -4
  69. package/dist/lib/tool-latency.js +0 -16
  70. package/dist/lib/untrusted-content.d.ts +0 -15
  71. package/dist/lib/untrusted-content.js +0 -24
  72. package/dist/tools/ask-browserstack/central-oauth.d.ts +0 -120
  73. package/dist/tools/ask-browserstack/central-oauth.js +0 -277
  74. package/dist/tools/ask-browserstack/config.d.ts +0 -102
  75. package/dist/tools/ask-browserstack/config.js +0 -140
  76. package/dist/tools/ask-browserstack/egress.d.ts +0 -34
  77. package/dist/tools/ask-browserstack/egress.js +0 -31
  78. package/dist/tools/ask-browserstack/register.d.ts +0 -61
  79. package/dist/tools/ask-browserstack/register.js +0 -416
  80. package/dist/tools/ask-browserstack/relay.d.ts +0 -201
  81. package/dist/tools/ask-browserstack/relay.js +0 -577
  82. package/dist/tools/ask-browserstack/stream.d.ts +0 -116
  83. package/dist/tools/ask-browserstack/stream.js +0 -236
  84. package/dist/tools/ask-browserstack/types.d.ts +0 -196
  85. package/dist/tools/ask-browserstack/types.js +0 -14
  86. package/dist/tools/automate-utils/list-session-ids.d.ts +0 -28
  87. package/dist/tools/automate-utils/list-session-ids.js +0 -87
  88. package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +0 -30
  89. package/dist/tools/automate-utils/resolve-hashed-build-id.js +0 -124
  90. package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +0 -2
  91. package/dist/tools/failurelogs-utils/resolve-app-build-id.js +0 -5
  92. package/dist/tools/failurelogs-utils/video.d.ts +0 -3
  93. package/dist/tools/failurelogs-utils/video.js +0 -25
  94. package/dist/tools/tool-handoff.d.ts +0 -66
  95. 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
- readonly ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY: boolean;
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
- ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY;
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, (process.env.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY || "").toLowerCase() ===
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, withToolCall } from "./lib/instrumentation.js";
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. Optional-called because an exit handler must not
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, withToolCall } from "./lib/instrumentation.js";
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;
@@ -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];
@@ -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 type ClientInfo = {
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
- const INSTRUMENTATION_ENDPOINT = "https://api.browserstack.com/sdk/v1/event";
10
- function baseProperties(toolName, clientInfo) {
11
- return {
12
- mcp_version: packageJson.version,
13
- tool_name: toolName,
14
- mcp_client: clientInfo?.name || "unknown",
15
- node_version: process.versions.node,
16
- is_remote: globalConfig.REMOTE_MCP,
17
- };
18
- }
19
- function errorProperties(error) {
20
- return {
21
- error_message: error instanceof Error ? error.message : String(error),
22
- error_type: error instanceof Error ? error.constructor.name : "Unknown",
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
- function sendEvent(event, config) {
26
- let authHeader;
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: INSTRUMENTATION_ENDPOINT,
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: string | undefined;
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 mandatory: uploads are refused unless it is configured
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 mandatory: uploads are refused unless it is configured
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 (!options.allowedBaseDir) {
53
- throw new Error("Upload rejected: MCP_UPLOAD_BASE_DIR is not set. Set it to the directory " +
54
- "containing the files to upload, then restart the MCP server.");
55
- }
56
- let baseCanonical;
57
- try {
58
- baseCanonical = fs.realpathSync(path.resolve(options.allowedBaseDir));
59
- }
60
- catch {
61
- throw new Error(`Upload rejected: configured MCP_UPLOAD_BASE_DIR does not exist (${options.allowedBaseDir}).`);
62
- }
63
- const baseWithSep = baseCanonical.endsWith(path.sep)
64
- ? baseCanonical
65
- : baseCanonical + path.sep;
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
- try {
19
- const { default: sharp } = await import("sharp");
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) {
@@ -1 +1,6 @@
1
+ /**
2
+ * If req === "latest" or "oldest", returns max/min numeric (or lex)
3
+ * Else if exact match, returns that
4
+ * Else picks the numerically closest (or first)
5
+ */
1
6
  export declare function resolveVersion(requested: string, available: string[]): string;
@@ -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 = candidates
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 = candidates.slice().sort();
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
- currentLogger = pino({ level: "info", enabled: false });
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({}, {
@@ -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 addAskBrowserStackAITool from "./tools/ask-browserstack/register.js";
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
- }, { instructions: nodeUpgradeNotice() || undefined });
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
- // Hands a plain-language task to BrowserStack's agent and relays its mid-run
58
- // permission asks back to this client, so a write can be confirmed by the human
59
- // sitting in front of it rather than refused for want of anyone to ask.
60
- addAskBrowserStackAITool,
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: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
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: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
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: true,
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.\n" +
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.\n" +
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. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
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: [
@@ -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. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
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,
@@ -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>;