Skip to content

Commit 0d59bb3

Browse files
Merge branch 'browserstack:main' into main
2 parents 3668b0c + a237b3d commit 0d59bb3

File tree

14 files changed

+224
-180
lines changed

14 files changed

+224
-180
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@browserstack/mcp-server",
3-
"version": "1.0.14",
3+
"version": "1.1.3",
44
"description": "BrowserStack's Official MCP Server",
55
"main": "dist/index.js",
66
"repository": {

src/config.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
if (
2-
!process.env.BROWSERSTACK_ACCESS_KEY ||
3-
!process.env.BROWSERSTACK_USERNAME
4-
) {
5-
throw new Error(
6-
"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-
);
8-
}
9-
101
export class Config {
112
constructor(
123
public readonly browserstackUsername: string,

src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5-
import packageJson from "../package.json" with { type: "json" };
5+
import { createRequire } from "module";
6+
const require = createRequire(import.meta.url);
7+
const packageJson = require("../package.json");
68
import "dotenv/config";
79
import logger from "./logger.js";
810
import addSDKTools from "./tools/bstack-sdk.js";
@@ -13,7 +15,7 @@ import addTestManagementTools from "./tools/testmanagement.js";
1315
import addAppAutomationTools from "./tools/appautomate.js";
1416
import addFailureLogsTools from "./tools/getFailureLogs.js";
1517
import addAutomateTools from "./tools/automate.js";
16-
import { trackMCP } from "./lib/instrumentation.js";
18+
import { setupOnInitialized } from "./oninitialized.js";
1719

1820
function registerTools(server: McpServer) {
1921
addSDKTools(server);
@@ -32,6 +34,8 @@ const server: McpServer = new McpServer({
3234
version: packageJson.version,
3335
});
3436

37+
setupOnInitialized(server);
38+
3539
registerTools(server);
3640

3741
async function main() {
@@ -43,9 +47,6 @@ async function main() {
4347
// Start receiving messages on stdin and sending messages on stdout
4448
const transport = new StdioServerTransport();
4549
await server.connect(transport);
46-
47-
logger.info("MCP server started successfully");
48-
trackMCP("started", server.server.getClientVersion()!);
4950
}
5051

5152
main().catch(console.error);

src/lib/instrumentation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logger from "../logger.js";
22
import config from "../config.js";
3-
import packageJson from "../../package.json" with { type: "json" };
3+
import { createRequire } from "module";
4+
const require = createRequire(import.meta.url);
5+
const packageJson = require("../../package.json");
46
import axios from "axios";
57

68
interface MCPEventPayload {

src/lib/utils.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,6 @@ export function sanitizeUrlParam(param: string): string {
55
return param.replace(/[;&|`$(){}[\]<>]/g, "");
66
}
77

8-
export interface HarFile {
9-
log: {
10-
entries: HarEntry[];
11-
};
12-
}
13-
14-
export interface HarEntry {
15-
startedDateTime: string;
16-
request: {
17-
method: string;
18-
url: string;
19-
queryString?: { name: string; value: string }[];
20-
};
21-
response: {
22-
status: number;
23-
statusText?: string;
24-
_error?: string;
25-
};
26-
serverIPAddress?: string;
27-
time?: number;
28-
}
29-
308
const ONE_MB = 1048576;
319

3210
//Compresses a base64 image intelligently to keep it under 1 MB if needed.
@@ -56,15 +34,3 @@ export async function assertOkResponse(response: Response, action: string) {
5634
);
5735
}
5836
}
59-
60-
export function filterLinesByKeywords(
61-
logText: string,
62-
keywords: string[],
63-
): string[] {
64-
return logText
65-
.split(/\r?\n/)
66-
.map((line) => line.trim())
67-
.filter((line) =>
68-
keywords.some((keyword) => line.toLowerCase().includes(keyword)),
69-
);
70-
}

src/oninitialized.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import config from "./config.js";
2+
import { trackMCP } from "./lib/instrumentation.js";
3+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4+
5+
export function setupOnInitialized(server: McpServer) {
6+
const nodeVersion = process.versions.node;
7+
8+
// Check for Node.js version
9+
if (nodeVersion < "18.0.0") {
10+
throw new Error(
11+
"Node version is not supported. Please upgrade to 18.0.0 or later.",
12+
);
13+
}
14+
15+
// Check for BrowserStack credentials
16+
if (!config.browserstackUsername || !config.browserstackAccessKey) {
17+
throw new Error(
18+
"BrowserStack credentials are missing. Please provide a valid username and access key.",
19+
);
20+
}
21+
server.server.oninitialized = () => {
22+
trackMCP("started", server.server.getClientVersion()!);
23+
};
24+
}

src/tools/failurelogs-utils/app-automate.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import config from "../../config.js";
2-
import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
2+
import { filterLinesByKeywords, validateLogResponse } from "./utils.js";
33

44
const auth = Buffer.from(
55
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
@@ -9,7 +9,7 @@ const auth = Buffer.from(
99
export async function retrieveDeviceLogs(
1010
sessionId: string,
1111
buildId: string,
12-
): Promise<string[]> {
12+
): Promise<string> {
1313
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/deviceLogs`;
1414

1515
const response = await fetch(url, {
@@ -19,17 +19,21 @@ export async function retrieveDeviceLogs(
1919
},
2020
});
2121

22-
await assertOkResponse(response, "device logs");
22+
const validationError = validateLogResponse(response, "device logs");
23+
if (validationError) return validationError.message!;
2324

2425
const logText = await response.text();
25-
return filterDeviceFailures(logText);
26+
const logs = filterDeviceFailures(logText);
27+
return logs.length > 0
28+
? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
29+
: "No device failures found";
2630
}
2731

2832
// APPIUM LOGS
2933
export async function retrieveAppiumLogs(
3034
sessionId: string,
3135
buildId: string,
32-
): Promise<string[]> {
36+
): Promise<string> {
3337
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/appiumlogs`;
3438

3539
const response = await fetch(url, {
@@ -39,17 +43,21 @@ export async function retrieveAppiumLogs(
3943
},
4044
});
4145

42-
await assertOkResponse(response, "Appium logs");
46+
const validationError = validateLogResponse(response, "Appium logs");
47+
if (validationError) return validationError.message!;
4348

4449
const logText = await response.text();
45-
return filterAppiumFailures(logText);
50+
const logs = filterAppiumFailures(logText);
51+
return logs.length > 0
52+
? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
53+
: "No Appium failures found";
4654
}
4755

4856
// CRASH LOGS
4957
export async function retrieveCrashLogs(
5058
sessionId: string,
5159
buildId: string,
52-
): Promise<string[]> {
60+
): Promise<string> {
5361
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/crashlogs`;
5462

5563
const response = await fetch(url, {
@@ -59,14 +67,17 @@ export async function retrieveCrashLogs(
5967
},
6068
});
6169

62-
await assertOkResponse(response, "crash logs");
70+
const validationError = validateLogResponse(response, "crash logs");
71+
if (validationError) return validationError.message!;
6372

6473
const logText = await response.text();
65-
return filterCrashFailures(logText);
74+
const logs = filterCrashFailures(logText);
75+
return logs.length > 0
76+
? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
77+
: "No crash failures found";
6678
}
6779

6880
// FILTER HELPERS
69-
7081
export function filterDeviceFailures(logText: string): string[] {
7182
const keywords = [
7283
"error",

src/tools/failurelogs-utils/automate.ts

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import config from "../../config.js";
2-
import { HarEntry, HarFile } from "../../lib/utils.js";
3-
import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
2+
import {
3+
HarEntry,
4+
HarFile,
5+
filterLinesByKeywords,
6+
validateLogResponse,
7+
} from "./utils.js";
48

59
const auth = Buffer.from(
610
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
711
).toString("base64");
812

913
// NETWORK LOGS
10-
export async function retrieveNetworkFailures(sessionId: string): Promise<any> {
14+
export async function retrieveNetworkFailures(
15+
sessionId: string,
16+
): Promise<string> {
1117
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/networklogs`;
1218

1319
const response = await fetch(url, {
@@ -18,43 +24,44 @@ export async function retrieveNetworkFailures(sessionId: string): Promise<any> {
1824
},
1925
});
2026

21-
await assertOkResponse(response, "network logs");
27+
const validationError = validateLogResponse(response, "network logs");
28+
if (validationError) return validationError.message!;
2229

2330
const networklogs: HarFile = await response.json();
24-
25-
// Filter for failure logs
2631
const failureEntries: HarEntry[] = networklogs.log.entries.filter(
27-
(entry: HarEntry) => {
28-
return (
29-
entry.response.status === 0 ||
30-
entry.response.status >= 400 ||
31-
entry.response._error !== undefined
32-
);
33-
},
32+
(entry: HarEntry) =>
33+
entry.response.status === 0 ||
34+
entry.response.status >= 400 ||
35+
entry.response._error !== undefined,
3436
);
3537

36-
// Return only the failure entries with some context
37-
return failureEntries.map((entry: any) => ({
38-
startedDateTime: entry.startedDateTime,
39-
request: {
40-
method: entry.request?.method,
41-
url: entry.request?.url,
42-
queryString: entry.request?.queryString,
43-
},
44-
response: {
45-
status: entry.response?.status,
46-
statusText: entry.response?.statusText,
47-
_error: entry.response?._error,
48-
},
49-
serverIPAddress: entry.serverIPAddress,
50-
time: entry.time,
51-
}));
38+
return failureEntries.length > 0
39+
? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(
40+
failureEntries.map((entry: any) => ({
41+
startedDateTime: entry.startedDateTime,
42+
request: {
43+
method: entry.request?.method,
44+
url: entry.request?.url,
45+
queryString: entry.request?.queryString,
46+
},
47+
response: {
48+
status: entry.response?.status,
49+
statusText: entry.response?.statusText,
50+
_error: entry.response?._error,
51+
},
52+
serverIPAddress: entry.serverIPAddress,
53+
time: entry.time,
54+
})),
55+
null,
56+
2,
57+
)}`
58+
: "No network failures found";
5259
}
5360

5461
// SESSION LOGS
5562
export async function retrieveSessionFailures(
5663
sessionId: string,
57-
): Promise<string[]> {
64+
): Promise<string> {
5865
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/logs`;
5966

6067
const response = await fetch(url, {
@@ -64,16 +71,20 @@ export async function retrieveSessionFailures(
6471
},
6572
});
6673

67-
await assertOkResponse(response, "session logs");
74+
const validationError = validateLogResponse(response, "session logs");
75+
if (validationError) return validationError.message!;
6876

6977
const logText = await response.text();
70-
return filterSessionFailures(logText);
78+
const logs = filterSessionFailures(logText);
79+
return logs.length > 0
80+
? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
81+
: "No session failures found";
7182
}
7283

7384
// CONSOLE LOGS
7485
export async function retrieveConsoleFailures(
7586
sessionId: string,
76-
): Promise<string[]> {
87+
): Promise<string> {
7788
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/consolelogs`;
7889

7990
const response = await fetch(url, {
@@ -83,10 +94,14 @@ export async function retrieveConsoleFailures(
8394
},
8495
});
8596

86-
await assertOkResponse(response, "console logs");
97+
const validationError = validateLogResponse(response, "console logs");
98+
if (validationError) return validationError.message!;
8799

88100
const logText = await response.text();
89-
return filterConsoleFailures(logText);
101+
const logs = filterConsoleFailures(logText);
102+
return logs.length > 0
103+
? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
104+
: "No console failures found";
90105
}
91106

92107
// FILTER: session logs

0 commit comments

Comments
 (0)