Skip to content

Commit 9a67c26

Browse files
committed
Merge branch 'main' into bstack-sdk
2 parents f00a5b7 + e00b27b commit 9a67c26

File tree

16 files changed

+441
-118
lines changed

16 files changed

+441
-118
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
138138
139139
## 🛠️ Installation
140140
141+
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor)
142+
141143
1. **Create a BrowserStack Account**
142144
143145
- Sign up for [BrowserStack](https://www.browserstack.com/users/sign_up) if you don't have an account already.

package-lock.json

Lines changed: 63 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ process.on("exit", () => {
5050

5151
export { setLogger } from "./logger.js";
5252
export { BrowserStackMcpServer } from "./server-factory.js";
53+
export { trackMCP } from "./lib/instrumentation.js";

src/lib/device-cache.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import fs from "fs";
22
import os from "os";
33
import path from "path";
44
import { apiClient } from "./apiClient.js";
5+
import config from "../config.js";
56

67
const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
78
const CACHE_FILE = path.join(CACHE_DIR, "data.json");
89
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
10+
const TTL_STARTED_MS = 3 * 60 * 60 * 1000; // 3 Hours
911

1012
export enum BrowserStackProducts {
1113
LIVE = "live",
@@ -64,3 +66,29 @@ export async function getDevicesAndBrowsers(
6466

6567
return cache[type];
6668
}
69+
70+
// Rate limiter for started event (3H)
71+
export function shouldSendStartedEvent(): boolean {
72+
try {
73+
if (config && config.REMOTE_MCP) {
74+
return false;
75+
}
76+
if (!fs.existsSync(CACHE_DIR)) {
77+
fs.mkdirSync(CACHE_DIR, { recursive: true });
78+
}
79+
let cache: Record<string, any> = {};
80+
if (fs.existsSync(CACHE_FILE)) {
81+
const raw = fs.readFileSync(CACHE_FILE, "utf8");
82+
cache = JSON.parse(raw || "{}");
83+
const last = parseInt(cache.lastStartedEvent, 10);
84+
if (!isNaN(last) && Date.now() - last < TTL_STARTED_MS) {
85+
return false;
86+
}
87+
}
88+
cache.lastStartedEvent = Date.now();
89+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
90+
return true;
91+
} catch {
92+
return true;
93+
}
94+
}

src/oninitialized.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { trackMCP } from "./lib/instrumentation.js";
22
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import { shouldSendStartedEvent } from "./lib/device-cache.js";
34

45
export function setupOnInitialized(server: McpServer, config?: any) {
56
const nodeVersion = process.versions.node;
@@ -12,6 +13,8 @@ export function setupOnInitialized(server: McpServer, config?: any) {
1213
}
1314

1415
server.server.oninitialized = () => {
15-
trackMCP("started", server.server.getClientVersion()!, undefined, config);
16+
if (shouldSendStartedEvent()) {
17+
trackMCP("started", server.server.getClientVersion()!, undefined, config);
18+
}
1619
};
1720
}

src/tools/appautomate-utils/appautomate.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,17 @@ export function resolveVersion(
9090
export function validateArgs(args: {
9191
desiredPlatform: string;
9292
desiredPlatformVersion: string;
93-
appPath: string;
93+
appPath?: string;
9494
desiredPhone: string;
95+
browserstackAppUrl?: string;
9596
}): void {
96-
const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone } =
97-
args;
97+
const {
98+
desiredPlatform,
99+
desiredPlatformVersion,
100+
appPath,
101+
desiredPhone,
102+
browserstackAppUrl,
103+
} = args;
98104

99105
if (!desiredPlatform || !desiredPhone) {
100106
throw new Error(
@@ -108,16 +114,19 @@ export function validateArgs(args: {
108114
);
109115
}
110116

111-
if (!appPath) {
112-
throw new Error("You must provide an appPath.");
117+
if (!appPath && !browserstackAppUrl) {
118+
throw new Error("Either appPath or browserstackAppUrl must be provided");
113119
}
114120

115-
if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
116-
throw new Error("You must provide a valid Android app path (.apk).");
117-
}
121+
// Only validate app path format if appPath is provided
122+
if (appPath) {
123+
if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
124+
throw new Error("You must provide a valid Android app path (.apk).");
125+
}
118126

119-
if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
120-
throw new Error("You must provide a valid iOS app path (.ipa).");
127+
if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
128+
throw new Error("You must provide a valid iOS app path (.ipa).");
129+
}
121130
}
122131
}
123132

0 commit comments

Comments
 (0)