@browserstack/mcp-server 1.0.13 → 1.0.15
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 +22 -0
- package/dist/config.js +2 -6
- package/dist/index.js +29 -32
- package/dist/lib/api.js +3 -57
- package/dist/lib/constants.js +14 -0
- package/dist/lib/device-cache.js +32 -33
- package/dist/lib/error.js +3 -6
- package/dist/lib/fuzzy.js +1 -4
- package/dist/lib/inmemory-store.js +1 -0
- package/dist/lib/instrumentation.js +12 -18
- package/dist/lib/local.js +27 -35
- package/dist/lib/utils.js +29 -4
- package/dist/logger.js +4 -9
- package/dist/tools/accessibility.js +51 -21
- package/dist/tools/accessiblity-utils/report-fetcher.js +28 -0
- package/dist/tools/accessiblity-utils/report-parser.js +51 -0
- package/dist/tools/accessiblity-utils/scanner.js +80 -0
- package/dist/tools/appautomate-utils/appautomate.js +95 -0
- package/dist/tools/appautomate.js +116 -0
- package/dist/tools/applive-utils/fuzzy-search.js +3 -6
- package/dist/tools/applive-utils/start-session.js +14 -20
- package/dist/tools/applive-utils/upload-app.js +12 -51
- package/dist/tools/applive.js +18 -25
- package/dist/tools/automate-utils/fetch-screenshots.js +59 -0
- package/dist/tools/automate.js +44 -37
- package/dist/tools/bstack-sdk.js +14 -18
- package/dist/tools/failurelogs-utils/app-automate.js +88 -0
- package/dist/tools/failurelogs-utils/automate.js +97 -0
- package/dist/tools/getFailureLogs.js +173 -0
- package/dist/tools/live-utils/desktop-filter.js +8 -11
- package/dist/tools/live-utils/mobile-filter.js +7 -10
- package/dist/tools/live-utils/start-session.js +17 -23
- package/dist/tools/live-utils/types.js +2 -5
- package/dist/tools/live-utils/version-resolver.js +1 -4
- package/dist/tools/live.js +23 -29
- package/dist/tools/observability.js +12 -19
- package/dist/tools/sdk-utils/constants.js +3 -9
- package/dist/tools/sdk-utils/instructions.js +4 -9
- package/dist/tools/sdk-utils/types.js +1 -2
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +259 -0
- package/dist/tools/testmanagement-utils/TCG-utils/config.js +5 -0
- package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +53 -0
- package/dist/tools/testmanagement-utils/TCG-utils/types.js +8 -0
- package/dist/tools/testmanagement-utils/add-test-result.js +18 -25
- package/dist/tools/testmanagement-utils/create-project-folder.js +21 -28
- package/dist/tools/testmanagement-utils/create-testcase.js +30 -38
- package/dist/tools/testmanagement-utils/create-testrun.js +23 -30
- package/dist/tools/testmanagement-utils/list-testcases.js +16 -23
- package/dist/tools/testmanagement-utils/list-testruns.js +13 -20
- package/dist/tools/testmanagement-utils/testcase-from-file.js +42 -0
- package/dist/tools/testmanagement-utils/update-testrun.js +16 -23
- package/dist/tools/testmanagement-utils/upload-file.js +101 -0
- package/dist/tools/testmanagement.js +98 -61
- package/package.json +13 -7
- package/dist/tools/accessiblity-utils/accessibility.js +0 -82
package/README.md
CHANGED
|
@@ -84,6 +84,28 @@ Use the following prompts to run/debug/fix your **automated tests** on BrowserSt
|
|
|
84
84
|
- Run tests written in Jest, Playwright, Selenium, and more on BrowserStack's [Test Platform](https://www.browserstack.com/test-platform)
|
|
85
85
|
- **Accessibility Testing**: Ensure WCAG and ADA compliance with our [Accessibility Testing](https://www.browserstack.com/accessibility-testing) tool
|
|
86
86
|
|
|
87
|
+
|
|
88
|
+
### 📋 Test Management
|
|
89
|
+
|
|
90
|
+
Use the following prompts to utilise capabilities of BrowserStack's [Test Management](https://www.browserstack.com/test-management) with MCP server.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Create project & folder structure
|
|
94
|
+
"create new Test management project named My Demo Project with two sub folders - Login & Checkout"
|
|
95
|
+
|
|
96
|
+
# Add test cases
|
|
97
|
+
"add invalid login test case in Test Management project named My Demo Project"
|
|
98
|
+
|
|
99
|
+
# List added test cases
|
|
100
|
+
"list high priority Login test cases from Test Management project - My Demo Project"
|
|
101
|
+
|
|
102
|
+
# Create test run
|
|
103
|
+
"create a test run for Login tests from Test Management project - My Demo Project"
|
|
104
|
+
|
|
105
|
+
# Update test results
|
|
106
|
+
"update test results as passed for Login tests test run from My Demo Project"
|
|
107
|
+
```
|
|
108
|
+
|
|
87
109
|
## 🛠️ Installation
|
|
88
110
|
|
|
89
111
|
1. **Create a BrowserStack Account**
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Config = void 0;
|
|
4
1
|
if (!process.env.BROWSERSTACK_ACCESS_KEY ||
|
|
5
2
|
!process.env.BROWSERSTACK_USERNAME) {
|
|
6
3
|
throw new Error("Unable to start MCP server. Please set the BROWSERSTACK_ACCESS_KEY and BROWSERSTACK_USERNAME environment variables. Go to https://www.browserstack.com/accounts/profile/details to access them");
|
|
7
4
|
}
|
|
8
|
-
class Config {
|
|
5
|
+
export class Config {
|
|
9
6
|
browserstackUsername;
|
|
10
7
|
browserstackAccessKey;
|
|
11
8
|
DEV_MODE;
|
|
@@ -15,6 +12,5 @@ class Config {
|
|
|
15
12
|
this.DEV_MODE = DEV_MODE;
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
|
-
exports.Config = Config;
|
|
19
15
|
const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY, process.env.DEV_MODE === "true");
|
|
20
|
-
|
|
16
|
+
export default config;
|
package/dist/index.js
CHANGED
|
@@ -1,47 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const accessibility_1 = __importDefault(require("./tools/accessibility"));
|
|
17
|
-
const automate_1 = __importDefault(require("./tools/automate"));
|
|
18
|
-
const testmanagement_1 = __importDefault(require("./tools/testmanagement"));
|
|
19
|
-
const instrumentation_1 = require("./lib/instrumentation");
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
import logger from "./logger.js";
|
|
7
|
+
import addSDKTools from "./tools/bstack-sdk.js";
|
|
8
|
+
import addAppLiveTools from "./tools/applive.js";
|
|
9
|
+
import addBrowserLiveTools from "./tools/live.js";
|
|
10
|
+
import addAccessibilityTools from "./tools/accessibility.js";
|
|
11
|
+
import addTestManagementTools from "./tools/testmanagement.js";
|
|
12
|
+
import addAppAutomationTools from "./tools/appautomate.js";
|
|
13
|
+
import addFailureLogsTools from "./tools/getFailureLogs.js";
|
|
14
|
+
import addAutomateTools from "./tools/automate.js";
|
|
15
|
+
import { trackMCP } from "./lib/instrumentation.js";
|
|
20
16
|
function registerTools(server) {
|
|
21
|
-
(
|
|
22
|
-
(
|
|
23
|
-
(
|
|
24
|
-
(
|
|
25
|
-
(
|
|
26
|
-
(
|
|
27
|
-
(
|
|
17
|
+
addSDKTools(server);
|
|
18
|
+
addAppLiveTools(server);
|
|
19
|
+
addBrowserLiveTools(server);
|
|
20
|
+
addAccessibilityTools(server);
|
|
21
|
+
addTestManagementTools(server);
|
|
22
|
+
addAppAutomationTools(server);
|
|
23
|
+
addFailureLogsTools(server);
|
|
24
|
+
addAutomateTools(server);
|
|
28
25
|
}
|
|
29
26
|
// Create an MCP server
|
|
30
|
-
const server = new
|
|
27
|
+
const server = new McpServer({
|
|
31
28
|
name: "BrowserStack MCP Server",
|
|
32
|
-
version:
|
|
29
|
+
version: packageJson.version,
|
|
33
30
|
});
|
|
34
31
|
registerTools(server);
|
|
35
32
|
async function main() {
|
|
36
|
-
|
|
33
|
+
logger.info("Launching BrowserStack MCP server, version %s", packageJson.version);
|
|
37
34
|
// Start receiving messages on stdin and sending messages on stdout
|
|
38
|
-
const transport = new
|
|
35
|
+
const transport = new StdioServerTransport();
|
|
39
36
|
await server.connect(transport);
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
logger.info("MCP server started successfully");
|
|
38
|
+
trackMCP("started", server.server.getClientVersion());
|
|
42
39
|
}
|
|
43
40
|
main().catch(console.error);
|
|
44
41
|
// Ensure logs are flushed before exit
|
|
45
42
|
process.on("exit", () => {
|
|
46
|
-
|
|
43
|
+
logger.flush();
|
|
47
44
|
});
|
package/dist/lib/api.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getLatestO11YBuildInfo = getLatestO11YBuildInfo;
|
|
7
|
-
exports.retrieveNetworkFailures = retrieveNetworkFailures;
|
|
8
|
-
const config_1 = __importDefault(require("../config"));
|
|
9
|
-
async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
1
|
+
import config from "../config.js";
|
|
2
|
+
export async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
10
3
|
const buildsUrl = `https://api-observability.browserstack.com/ext/v1/builds/latest?build_name=${encodeURIComponent(buildName)}&project_name=${encodeURIComponent(projectName)}`;
|
|
11
4
|
const buildsResponse = await fetch(buildsUrl, {
|
|
12
5
|
headers: {
|
|
13
|
-
Authorization: `Basic ${Buffer.from(`${
|
|
6
|
+
Authorization: `Basic ${Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64")}`,
|
|
14
7
|
},
|
|
15
8
|
});
|
|
16
9
|
if (!buildsResponse.ok) {
|
|
@@ -21,50 +14,3 @@ async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
|
21
14
|
}
|
|
22
15
|
return buildsResponse.json();
|
|
23
16
|
}
|
|
24
|
-
// Fetches network logs for a given session ID and returns only failure logs
|
|
25
|
-
async function retrieveNetworkFailures(sessionId) {
|
|
26
|
-
if (!sessionId) {
|
|
27
|
-
throw new Error("Session ID is required");
|
|
28
|
-
}
|
|
29
|
-
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/networklogs`;
|
|
30
|
-
const auth = Buffer.from(`${config_1.default.browserstackUsername}:${config_1.default.browserstackAccessKey}`).toString("base64");
|
|
31
|
-
const response = await fetch(url, {
|
|
32
|
-
method: "GET",
|
|
33
|
-
headers: {
|
|
34
|
-
"Content-Type": "application/json",
|
|
35
|
-
Authorization: `Basic ${auth}`,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
if (response.status === 404) {
|
|
40
|
-
throw new Error("Invalid session ID");
|
|
41
|
-
}
|
|
42
|
-
throw new Error(`Failed to fetch network logs: ${response.statusText}`);
|
|
43
|
-
}
|
|
44
|
-
const networklogs = await response.json();
|
|
45
|
-
// Filter for failure logs
|
|
46
|
-
const failureEntries = networklogs.log.entries.filter((entry) => {
|
|
47
|
-
return (entry.response.status === 0 ||
|
|
48
|
-
entry.response.status >= 400 ||
|
|
49
|
-
entry.response._error !== undefined);
|
|
50
|
-
});
|
|
51
|
-
// Return only the failure entries with some context
|
|
52
|
-
return {
|
|
53
|
-
failures: failureEntries.map((entry) => ({
|
|
54
|
-
startedDateTime: entry.startedDateTime,
|
|
55
|
-
request: {
|
|
56
|
-
method: entry.request?.method,
|
|
57
|
-
url: entry.request?.url,
|
|
58
|
-
queryString: entry.request?.queryString,
|
|
59
|
-
},
|
|
60
|
-
response: {
|
|
61
|
-
status: entry.response?.status,
|
|
62
|
-
statusText: entry.response?.statusText,
|
|
63
|
-
_error: entry.response?._error,
|
|
64
|
-
},
|
|
65
|
-
serverIPAddress: entry.serverIPAddress,
|
|
66
|
-
time: entry.time,
|
|
67
|
-
})),
|
|
68
|
-
totalFailures: failureEntries.length,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const SessionType = {
|
|
2
|
+
Automate: "automate",
|
|
3
|
+
AppAutomate: "app-automate",
|
|
4
|
+
};
|
|
5
|
+
export const AutomateLogType = {
|
|
6
|
+
NetworkLogs: "networkLogs",
|
|
7
|
+
SessionLogs: "sessionLogs",
|
|
8
|
+
ConsoleLogs: "consoleLogs",
|
|
9
|
+
};
|
|
10
|
+
export const AppAutomateLogType = {
|
|
11
|
+
DeviceLogs: "deviceLogs",
|
|
12
|
+
AppiumLogs: "appiumLogs",
|
|
13
|
+
CrashLogs: "crashLogs",
|
|
14
|
+
};
|
package/dist/lib/device-cache.js
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
exports.getDevicesAndBrowsers = getDevicesAndBrowsers;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "combined_cache");
|
|
11
|
-
const CACHE_FILE = path_1.default.join(CACHE_DIR, "data.json");
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
|
|
5
|
+
const CACHE_FILE = path.join(CACHE_DIR, "data.json");
|
|
12
6
|
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
7
|
+
export var BrowserStackProducts;
|
|
8
|
+
(function (BrowserStackProducts) {
|
|
9
|
+
BrowserStackProducts["LIVE"] = "live";
|
|
10
|
+
BrowserStackProducts["APP_LIVE"] = "app_live";
|
|
11
|
+
BrowserStackProducts["APP_AUTOMATE"] = "app_automate";
|
|
12
|
+
})(BrowserStackProducts || (BrowserStackProducts = {}));
|
|
13
13
|
const URLS = {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
[BrowserStackProducts.LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
|
|
15
|
+
[BrowserStackProducts.APP_LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
|
|
16
|
+
[BrowserStackProducts.APP_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_automate.json",
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
|
-
* Fetches and caches
|
|
19
|
+
* Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
|
|
19
20
|
*/
|
|
20
|
-
async function getDevicesAndBrowsers(type) {
|
|
21
|
-
if (!
|
|
22
|
-
|
|
21
|
+
export async function getDevicesAndBrowsers(type) {
|
|
22
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
23
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
23
24
|
}
|
|
24
25
|
let cache = {};
|
|
25
|
-
if (
|
|
26
|
-
const stats =
|
|
26
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
27
|
+
const stats = fs.statSync(CACHE_FILE);
|
|
27
28
|
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
28
29
|
try {
|
|
29
|
-
cache = JSON.parse(
|
|
30
|
-
|
|
30
|
+
cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
|
|
31
|
+
if (cache[type]) {
|
|
32
|
+
return cache[type];
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
35
|
catch (error) {
|
|
33
36
|
console.error("Error parsing cache file:", error);
|
|
@@ -35,18 +38,14 @@ async function getDevicesAndBrowsers(type) {
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
fetch
|
|
41
|
-
]);
|
|
42
|
-
if (!liveRes.ok || !appLiveRes.ok) {
|
|
43
|
-
throw new Error(`Failed to fetch configuration from BrowserStack : live=${liveRes.statusText}, app_live=${appLiveRes.statusText}`);
|
|
41
|
+
const liveRes = await fetch(URLS[type]);
|
|
42
|
+
if (!liveRes.ok) {
|
|
43
|
+
throw new Error(`Failed to fetch configuration from BrowserStack : ${type}=${liveRes.statusText}`);
|
|
44
44
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cache
|
|
50
|
-
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
45
|
+
const data = await liveRes.json();
|
|
46
|
+
cache = {
|
|
47
|
+
[type]: data,
|
|
48
|
+
};
|
|
49
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
51
50
|
return cache[type];
|
|
52
51
|
}
|
package/dist/lib/error.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatAxiosError = formatAxiosError;
|
|
4
|
-
const axios_1 = require("axios");
|
|
1
|
+
import { AxiosError } from "axios";
|
|
5
2
|
/**
|
|
6
3
|
* Formats an AxiosError into a CallToolResult with an appropriate message.
|
|
7
4
|
* @param err - The error object to format
|
|
8
5
|
* @param defaultText - The fallback error message
|
|
9
6
|
*/
|
|
10
|
-
function formatAxiosError(err, defaultText) {
|
|
7
|
+
export function formatAxiosError(err, defaultText) {
|
|
11
8
|
let text = defaultText;
|
|
12
|
-
if (err instanceof
|
|
9
|
+
if (err instanceof AxiosError && err.response?.data) {
|
|
13
10
|
const message = err.response.data.message ||
|
|
14
11
|
err.response.data.error ||
|
|
15
12
|
err.message ||
|
package/dist/lib/fuzzy.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.customFuzzySearch = customFuzzySearch;
|
|
4
1
|
// 1. Compute Levenshtein distance between two strings
|
|
5
2
|
function levenshtein(a, b) {
|
|
6
3
|
const dp = Array(a.length + 1)
|
|
@@ -39,7 +36,7 @@ function scoreItem(item, keys, queryTokens) {
|
|
|
39
36
|
return best;
|
|
40
37
|
}
|
|
41
38
|
// 3. The search entrypoint
|
|
42
|
-
function customFuzzySearch(list, keys, query, limit = 5, maxDistance = 0.6) {
|
|
39
|
+
export function customFuzzySearch(list, keys, query, limit = 5, maxDistance = 0.6) {
|
|
43
40
|
const q = query.toLowerCase().trim();
|
|
44
41
|
const queryTokens = q.split(/\s+/);
|
|
45
42
|
return list
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const signedUrlMap = new Map();
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const config_1 = __importDefault(require("../config"));
|
|
9
|
-
const package_json_1 = __importDefault(require("../../package.json"));
|
|
10
|
-
const axios_1 = __importDefault(require("axios"));
|
|
11
|
-
function trackMCP(toolName, clientInfo, error) {
|
|
12
|
-
if (config_1.default.DEV_MODE) {
|
|
13
|
-
logger_1.default.info("Tracking MCP is disabled in dev mode");
|
|
1
|
+
import logger from "../logger.js";
|
|
2
|
+
import config from "../config.js";
|
|
3
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
export function trackMCP(toolName, clientInfo, error) {
|
|
6
|
+
if (config.DEV_MODE) {
|
|
7
|
+
logger.info("Tracking MCP is disabled in dev mode");
|
|
14
8
|
return;
|
|
15
9
|
}
|
|
16
10
|
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
@@ -18,15 +12,15 @@ function trackMCP(toolName, clientInfo, error) {
|
|
|
18
12
|
const mcpClient = clientInfo?.name || "unknown";
|
|
19
13
|
// Log client information
|
|
20
14
|
if (clientInfo?.name) {
|
|
21
|
-
|
|
15
|
+
logger.info(`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`);
|
|
22
16
|
}
|
|
23
17
|
else {
|
|
24
|
-
|
|
18
|
+
logger.info("Client connected: unknown client");
|
|
25
19
|
}
|
|
26
20
|
const event = {
|
|
27
21
|
event_type: "MCPInstrumentation",
|
|
28
22
|
event_properties: {
|
|
29
|
-
mcp_version:
|
|
23
|
+
mcp_version: packageJson.version,
|
|
30
24
|
tool_name: toolName,
|
|
31
25
|
mcp_client: mcpClient,
|
|
32
26
|
success: isSuccess,
|
|
@@ -39,11 +33,11 @@ function trackMCP(toolName, clientInfo, error) {
|
|
|
39
33
|
event.event_properties.error_type =
|
|
40
34
|
error instanceof Error ? error.constructor.name : "Unknown";
|
|
41
35
|
}
|
|
42
|
-
|
|
36
|
+
axios
|
|
43
37
|
.post(instrumentationEndpoint, event, {
|
|
44
38
|
headers: {
|
|
45
39
|
"Content-Type": "application/json",
|
|
46
|
-
Authorization: `Basic ${Buffer.from(`${
|
|
40
|
+
Authorization: `Basic ${Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64")}`,
|
|
47
41
|
},
|
|
48
42
|
timeout: 2000,
|
|
49
43
|
})
|
package/dist/lib/local.js
CHANGED
|
@@ -1,47 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.killExistingBrowserStackLocalProcesses = killExistingBrowserStackLocalProcesses;
|
|
7
|
-
exports.ensureLocalBinarySetup = ensureLocalBinarySetup;
|
|
8
|
-
exports.isLocalURL = isLocalURL;
|
|
9
|
-
const logger_1 = __importDefault(require("../logger"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
|
-
const browserstack_local_1 = require("browserstack-local");
|
|
12
|
-
const config_1 = __importDefault(require("../config"));
|
|
1
|
+
import logger from "../logger.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { Local } from "browserstack-local";
|
|
4
|
+
import config from "../config.js";
|
|
13
5
|
async function isBrowserStackLocalRunning() {
|
|
14
6
|
// Check if BrowserStackLocal binary is already running
|
|
15
7
|
try {
|
|
16
8
|
if (process.platform === "win32") {
|
|
17
|
-
const result =
|
|
9
|
+
const result = execSync('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', {
|
|
18
10
|
encoding: "utf8",
|
|
19
11
|
});
|
|
20
12
|
if (result.includes("BrowserStackLocal.exe")) {
|
|
21
|
-
|
|
13
|
+
logger.info("BrowserStackLocal binary is already running");
|
|
22
14
|
return true;
|
|
23
15
|
}
|
|
24
16
|
}
|
|
25
17
|
else {
|
|
26
|
-
const result =
|
|
18
|
+
const result = execSync("pgrep -f BrowserStackLocal", {
|
|
27
19
|
encoding: "utf8",
|
|
28
20
|
stdio: "pipe",
|
|
29
21
|
}).toString();
|
|
30
22
|
if (result) {
|
|
31
|
-
|
|
23
|
+
logger.info("BrowserStackLocal binary is already running");
|
|
32
24
|
return true;
|
|
33
25
|
}
|
|
34
26
|
}
|
|
35
|
-
|
|
27
|
+
logger.info("BrowserStackLocal binary is not running");
|
|
36
28
|
return false;
|
|
37
29
|
}
|
|
38
30
|
catch (error) {
|
|
39
|
-
|
|
31
|
+
logger.info("Error checking BrowserStackLocal status, assuming not running ... " +
|
|
40
32
|
error);
|
|
41
33
|
return false;
|
|
42
34
|
}
|
|
43
35
|
}
|
|
44
|
-
async function killExistingBrowserStackLocalProcesses() {
|
|
36
|
+
export async function killExistingBrowserStackLocalProcesses() {
|
|
45
37
|
const isRunning = await isBrowserStackLocalRunning();
|
|
46
38
|
if (!isRunning) {
|
|
47
39
|
return;
|
|
@@ -50,50 +42,50 @@ async function killExistingBrowserStackLocalProcesses() {
|
|
|
50
42
|
try {
|
|
51
43
|
if (process.platform === "win32") {
|
|
52
44
|
// Check if process exists on Windows
|
|
53
|
-
const checkResult =
|
|
45
|
+
const checkResult = execSync('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', { encoding: "utf8" });
|
|
54
46
|
if (checkResult.includes("BrowserStackLocal.exe")) {
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
execSync("taskkill /F /IM BrowserStackLocal.exe", { stdio: "ignore" });
|
|
48
|
+
logger.info("Successfully killed existing BrowserStackLocal processes");
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
else {
|
|
60
52
|
// Check if process exists on Unix-like systems
|
|
61
|
-
const checkResult =
|
|
53
|
+
const checkResult = execSync("pgrep -f BrowserStackLocal", {
|
|
62
54
|
encoding: "utf8",
|
|
63
55
|
stdio: "pipe",
|
|
64
56
|
}).toString();
|
|
65
57
|
if (checkResult) {
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
execSync("pkill -f BrowserStackLocal", { stdio: "ignore" });
|
|
59
|
+
logger.info("Successfully killed existing BrowserStackLocal processes");
|
|
68
60
|
}
|
|
69
61
|
}
|
|
70
62
|
}
|
|
71
63
|
catch (error) {
|
|
72
|
-
|
|
64
|
+
logger.info(`Error checking/killing BrowserStackLocal processes: ${error}`);
|
|
73
65
|
// Continue execution as there may not be any processes running
|
|
74
66
|
}
|
|
75
67
|
}
|
|
76
|
-
async function ensureLocalBinarySetup() {
|
|
77
|
-
|
|
78
|
-
const localBinary = new
|
|
68
|
+
export async function ensureLocalBinarySetup() {
|
|
69
|
+
logger.info("Ensuring local binary setup as it is required for private URLs...");
|
|
70
|
+
const localBinary = new Local();
|
|
79
71
|
await killExistingBrowserStackLocalProcesses();
|
|
80
72
|
return await new Promise((resolve, reject) => {
|
|
81
73
|
localBinary.start({
|
|
82
|
-
key:
|
|
83
|
-
username:
|
|
74
|
+
key: config.browserstackAccessKey,
|
|
75
|
+
username: config.browserstackUsername,
|
|
84
76
|
}, (error) => {
|
|
85
77
|
if (error) {
|
|
86
|
-
|
|
78
|
+
logger.error(`Unable to start BrowserStack Local... please check your credentials and try again. Error: ${error}`);
|
|
87
79
|
reject(new Error(`Unable to configure local tunnel binary, please check your credentials and try again. Error: ${error}`));
|
|
88
80
|
}
|
|
89
81
|
else {
|
|
90
|
-
|
|
82
|
+
logger.info("Successfully started BrowserStack Local");
|
|
91
83
|
resolve();
|
|
92
84
|
}
|
|
93
85
|
});
|
|
94
86
|
});
|
|
95
87
|
}
|
|
96
|
-
function isLocalURL(url) {
|
|
88
|
+
export function isLocalURL(url) {
|
|
97
89
|
try {
|
|
98
90
|
const urlObj = new URL(url);
|
|
99
91
|
const hostname = urlObj.hostname.toLowerCase();
|
|
@@ -103,7 +95,7 @@ function isLocalURL(url) {
|
|
|
103
95
|
hostname.endsWith(".localhost"));
|
|
104
96
|
}
|
|
105
97
|
catch (error) {
|
|
106
|
-
|
|
98
|
+
logger.error(`Error checking if URL is local: ${error}`);
|
|
107
99
|
return false;
|
|
108
100
|
}
|
|
109
101
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.sanitizeUrlParam = sanitizeUrlParam;
|
|
4
|
-
function sanitizeUrlParam(param) {
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
export function sanitizeUrlParam(param) {
|
|
5
3
|
// Remove any characters that could be used for command injection
|
|
6
4
|
return param.replace(/[;&|`$(){}[\]<>]/g, "");
|
|
7
5
|
}
|
|
6
|
+
const ONE_MB = 1048576;
|
|
7
|
+
//Compresses a base64 image intelligently to keep it under 1 MB if needed.
|
|
8
|
+
export async function maybeCompressBase64(base64) {
|
|
9
|
+
const buffer = Buffer.from(base64, "base64");
|
|
10
|
+
if (buffer.length <= ONE_MB) {
|
|
11
|
+
return base64;
|
|
12
|
+
}
|
|
13
|
+
const sizeRatio = 1048576 / buffer.length;
|
|
14
|
+
const estimatedQuality = Math.floor(sizeRatio * 100);
|
|
15
|
+
const quality = Math.min(95, Math.max(30, estimatedQuality));
|
|
16
|
+
const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer();
|
|
17
|
+
return compressedBuffer.toString("base64");
|
|
18
|
+
}
|
|
19
|
+
export async function assertOkResponse(response, action) {
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (response.status === 404) {
|
|
22
|
+
throw new Error(`Invalid session ID for ${action}`);
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Failed to fetch logs for ${action}: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function filterLinesByKeywords(logText, keywords) {
|
|
28
|
+
return logText
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => keywords.some((keyword) => line.toLowerCase().includes(keyword)));
|
|
32
|
+
}
|
package/dist/logger.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const pino_1 = __importDefault(require("pino"));
|
|
1
|
+
import { pino } from "pino";
|
|
7
2
|
let logger;
|
|
8
3
|
if (process.env.NODE_ENV === "development") {
|
|
9
|
-
logger = (
|
|
4
|
+
logger = pino({
|
|
10
5
|
level: "debug",
|
|
11
6
|
transport: {
|
|
12
7
|
targets: [
|
|
@@ -27,7 +22,7 @@ if (process.env.NODE_ENV === "development") {
|
|
|
27
22
|
}
|
|
28
23
|
else {
|
|
29
24
|
// NULL logger
|
|
30
|
-
logger = (
|
|
25
|
+
logger = pino({
|
|
31
26
|
level: "info",
|
|
32
27
|
transport: {
|
|
33
28
|
target: "pino/file",
|
|
@@ -37,4 +32,4 @@ else {
|
|
|
37
32
|
},
|
|
38
33
|
});
|
|
39
34
|
}
|
|
40
|
-
|
|
35
|
+
export default logger;
|