@browserstack/mcp-server 1.2.19 → 1.2.20

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.
@@ -55,7 +55,6 @@ function getAxiosAgent() {
55
55
  host: proxyHost,
56
56
  port: Number(proxyPort),
57
57
  ca,
58
- rejectUnauthorized: false, // Set to true if you want strict SSL
59
58
  });
60
59
  }
61
60
  else {
@@ -67,7 +66,6 @@ function getAxiosAgent() {
67
66
  // CA only
68
67
  return new https.Agent({
69
68
  ca: fs.readFileSync(caCertPath),
70
- rejectUnauthorized: false, // Set to true for strict SSL
71
69
  });
72
70
  }
73
71
  // Default agent (no proxy, no CA)
@@ -5,7 +5,6 @@ import { uploadApp } from "./upload-app.js";
5
5
  import { getBrowserStackAuth } from "../../lib/get-auth.js";
6
6
  import { findDeviceByName } from "./device-search.js";
7
7
  import { pickVersion } from "./version-utils.js";
8
- import childProcess from "child_process";
9
8
  import envConfig from "../../config.js";
10
9
  /**
11
10
  * Start an App Live session: filter, select, upload, and open.
@@ -69,33 +68,44 @@ export async function startSession(args, options) {
69
68
  });
70
69
  const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
71
70
  if (!envConfig.REMOTE_MCP) {
72
- openBrowser(launchUrl);
71
+ const openCommand = getOpenBrowserCommand(launchUrl);
72
+ if (openCommand) {
73
+ return [
74
+ `App Live session URL: ${launchUrl}${note}`,
75
+ ``,
76
+ `To open the session in the default browser, run:`,
77
+ ` ${openCommand}`,
78
+ ].join("\n");
79
+ }
73
80
  }
74
81
  return launchUrl + note;
75
82
  }
76
83
  /**
77
- * Opens the launch URL in the default browser.
78
- * @param launchUrl - The URL to open.
79
- * @throws Will throw an error if the browser fails to open.
84
+ * Returns the platform-appropriate shell command to open `launchUrl` in the
85
+ * default browser, or null if the URL is not a trusted BrowserStack URL.
86
+ *
87
+ * The command is returned to the MCP client so the host agent can prompt the
88
+ * user before executing it. The server itself never spawns a process, which
89
+ * eliminates the command-injection surface entirely.
80
90
  */
81
- function openBrowser(launchUrl) {
91
+ function getOpenBrowserCommand(launchUrl) {
92
+ let parsed;
82
93
  try {
83
- const command = process.platform === "darwin"
84
- ? ["open", launchUrl]
85
- : process.platform === "win32"
86
- ? ["cmd", "/c", "start", launchUrl]
87
- : ["xdg-open", launchUrl];
88
- // nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
89
- const child = childProcess.spawn(command[0], command.slice(1), {
90
- stdio: "ignore",
91
- detached: true,
92
- });
93
- child.on("error", (error) => {
94
- logger.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
95
- });
96
- child.unref();
94
+ parsed = new URL(launchUrl);
95
+ }
96
+ catch {
97
+ logger.error(`Refusing to surface malformed URL: ${launchUrl}`);
98
+ return null;
97
99
  }
98
- catch (error) {
99
- logger.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
100
+ if (parsed.protocol !== "https:" ||
101
+ !/(^|\.)browserstack\.com$/i.test(parsed.hostname)) {
102
+ logger.error(`Refusing to surface untrusted URL: ${launchUrl}`);
103
+ return null;
100
104
  }
105
+ const quoted = `"${parsed.toString()}"`;
106
+ if (process.platform === "darwin")
107
+ return `open ${quoted}`;
108
+ if (process.platform === "win32")
109
+ return `cmd /c start "" ${quoted}`;
110
+ return `xdg-open ${quoted}`;
101
111
  }
@@ -1,5 +1,4 @@
1
1
  import logger from "../../logger.js";
2
- import childProcess from "child_process";
3
2
  import { filterDesktop } from "./desktop-filter.js";
4
3
  import { filterMobile } from "./mobile-filter.js";
5
4
  import { PlatformType, } from "./types.js";
@@ -39,10 +38,19 @@ export async function startBrowserSession(args, config) {
39
38
  const url = args.platformType === PlatformType.DESKTOP
40
39
  ? buildDesktopUrl(args, entry, isLocal)
41
40
  : buildMobileUrl(args, entry, isLocal);
41
+ const note = entry.notes ? `, ${entry.notes}` : "";
42
42
  if (!envConfig.REMOTE_MCP) {
43
- openBrowser(url);
43
+ const openCommand = getOpenBrowserCommand(url);
44
+ if (openCommand) {
45
+ return [
46
+ `Live session URL: ${url}${note}`,
47
+ ``,
48
+ `To open the session in the default browser, run:`,
49
+ ` ${openCommand}`,
50
+ ].join("\n");
51
+ }
44
52
  }
45
- return entry.notes ? `${url}, ${entry.notes}` : url;
53
+ return `${url}${note}`;
46
54
  }
47
55
  function buildDesktopUrl(args, e, isLocal) {
48
56
  const params = new URLSearchParams({
@@ -79,24 +87,33 @@ function buildMobileUrl(args, d, isLocal) {
79
87
  });
80
88
  return `https://live.browserstack.com/dashboard#${params.toString()}`;
81
89
  }
82
- // ——— Open a browser window ———
83
- function openBrowser(launchUrl) {
90
+ // ——— Build a browser-open command for the host agent ———
91
+ /**
92
+ * Returns the platform-appropriate shell command to open `launchUrl` in the
93
+ * default browser, or null if the URL is not a trusted BrowserStack URL.
94
+ *
95
+ * The command is returned to the MCP client so the host agent can prompt the
96
+ * user before executing it. The server itself never spawns a process, which
97
+ * eliminates the command-injection surface entirely.
98
+ */
99
+ function getOpenBrowserCommand(launchUrl) {
100
+ let parsed;
84
101
  try {
85
- const command = process.platform === "darwin"
86
- ? ["open", launchUrl]
87
- : process.platform === "win32"
88
- ? ["cmd", "/c", "start", `""`, `"${launchUrl}"`]
89
- : ["xdg-open", launchUrl];
90
- // nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
91
- const child = childProcess.spawn(command[0], command.slice(1), {
92
- stdio: "ignore",
93
- detached: true,
94
- ...(process.platform === "win32" ? { shell: true } : {}),
95
- });
96
- child.on("error", (err) => logger.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
97
- child.unref();
102
+ parsed = new URL(launchUrl);
103
+ }
104
+ catch {
105
+ logger.error(`Refusing to surface malformed URL: ${launchUrl}`);
106
+ return null;
98
107
  }
99
- catch (err) {
100
- logger.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
108
+ if (parsed.protocol !== "https:" ||
109
+ !/(^|\.)browserstack\.com$/i.test(parsed.hostname)) {
110
+ logger.error(`Refusing to surface untrusted URL: ${launchUrl}`);
111
+ return null;
101
112
  }
113
+ const quoted = `"${parsed.toString()}"`;
114
+ if (process.platform === "darwin")
115
+ return `open ${quoted}`;
116
+ if (process.platform === "win32")
117
+ return `cmd /c start "" ${quoted}`;
118
+ return `xdg-open ${quoted}`;
102
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.2.19",
3
+ "version": "1.2.20",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",