@browserstack/mcp-server 1.1.2 → 1.1.3
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/dist/config.js +0 -4
- package/dist/index.js +2 -3
- package/dist/lib/utils.js +0 -6
- package/dist/oninitialized.js +16 -0
- package/dist/tools/accessibility.js +5 -1
- package/dist/tools/accessiblity-utils/report-parser.js +5 -1
- package/dist/tools/failurelogs-utils/app-automate.js +22 -7
- package/dist/tools/failurelogs-utils/automate.js +38 -28
- package/dist/tools/failurelogs-utils/utils.js +20 -0
- package/dist/tools/getFailureLogs.js +14 -43
- package/dist/tools/testmanagement-utils/create-project-folder.js +7 -1
- package/dist/tools/testmanagement-utils/create-testcase.js +11 -2
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
if (!process.env.BROWSERSTACK_ACCESS_KEY ||
|
|
2
|
-
!process.env.BROWSERSTACK_USERNAME) {
|
|
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");
|
|
4
|
-
}
|
|
5
1
|
export class Config {
|
|
6
2
|
browserstackUsername;
|
|
7
3
|
browserstackAccessKey;
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import addTestManagementTools from "./tools/testmanagement.js";
|
|
|
14
14
|
import addAppAutomationTools from "./tools/appautomate.js";
|
|
15
15
|
import addFailureLogsTools from "./tools/getFailureLogs.js";
|
|
16
16
|
import addAutomateTools from "./tools/automate.js";
|
|
17
|
-
import {
|
|
17
|
+
import { setupOnInitialized } from "./oninitialized.js";
|
|
18
18
|
function registerTools(server) {
|
|
19
19
|
addSDKTools(server);
|
|
20
20
|
addAppLiveTools(server);
|
|
@@ -30,14 +30,13 @@ const server = new McpServer({
|
|
|
30
30
|
name: "BrowserStack MCP Server",
|
|
31
31
|
version: packageJson.version,
|
|
32
32
|
});
|
|
33
|
+
setupOnInitialized(server);
|
|
33
34
|
registerTools(server);
|
|
34
35
|
async function main() {
|
|
35
36
|
logger.info("Launching BrowserStack MCP server, version %s", packageJson.version);
|
|
36
37
|
// Start receiving messages on stdin and sending messages on stdout
|
|
37
38
|
const transport = new StdioServerTransport();
|
|
38
39
|
await server.connect(transport);
|
|
39
|
-
logger.info("MCP server started successfully");
|
|
40
|
-
trackMCP("started", server.server.getClientVersion());
|
|
41
40
|
}
|
|
42
41
|
main().catch(console.error);
|
|
43
42
|
// Ensure logs are flushed before exit
|
package/dist/lib/utils.js
CHANGED
|
@@ -24,9 +24,3 @@ export async function assertOkResponse(response, action) {
|
|
|
24
24
|
throw new Error(`Failed to fetch logs for ${action}: ${response.statusText}`);
|
|
25
25
|
}
|
|
26
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
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import config from "./config.js";
|
|
2
|
+
import { trackMCP } from "./lib/instrumentation.js";
|
|
3
|
+
export function setupOnInitialized(server) {
|
|
4
|
+
const nodeVersion = process.versions.node;
|
|
5
|
+
// Check for Node.js version
|
|
6
|
+
if (nodeVersion < "18.0.0") {
|
|
7
|
+
throw new Error("Node version is not supported. Please upgrade to 18.0.0 or later.");
|
|
8
|
+
}
|
|
9
|
+
// Check for BrowserStack credentials
|
|
10
|
+
if (!config.browserstackUsername || !config.browserstackAccessKey) {
|
|
11
|
+
throw new Error("BrowserStack credentials are missing. Please provide a valid username and access key.");
|
|
12
|
+
}
|
|
13
|
+
server.server.oninitialized = () => {
|
|
14
|
+
trackMCP("started", server.server.getClientVersion());
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -36,13 +36,17 @@ async function runAccessibilityScan(name, pageURL, context) {
|
|
|
36
36
|
}
|
|
37
37
|
// Fetch CSV report link
|
|
38
38
|
const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
|
|
39
|
-
const { records } = await parseAccessibilityReportFromCSV(reportLink);
|
|
39
|
+
const { records, page_length, total_issues } = await parseAccessibilityReportFromCSV(reportLink);
|
|
40
40
|
return {
|
|
41
41
|
content: [
|
|
42
42
|
{
|
|
43
43
|
type: "text",
|
|
44
44
|
text: `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `We found ${total_issues} issues. Below are the details of the ${page_length} most critical issues.`,
|
|
49
|
+
},
|
|
46
50
|
{
|
|
47
51
|
type: "text",
|
|
48
52
|
text: `Scan results: ${JSON.stringify(records, null, 2)}`,
|
|
@@ -17,6 +17,7 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
17
17
|
how_to_fix: row["How to fix this issue"],
|
|
18
18
|
severity: (row["Severity"] || "unknown").trim(),
|
|
19
19
|
}));
|
|
20
|
+
const totalIssues = all.length;
|
|
20
21
|
// 2) Sort by severity
|
|
21
22
|
const order = {
|
|
22
23
|
critical: 0,
|
|
@@ -43,9 +44,12 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
43
44
|
page.push(all[i]);
|
|
44
45
|
charCursor += recStr.length;
|
|
45
46
|
}
|
|
46
|
-
const
|
|
47
|
+
const pageLength = page.length;
|
|
48
|
+
const hasMore = idx + pageLength < totalIssues;
|
|
47
49
|
return {
|
|
48
50
|
records: page,
|
|
49
51
|
next_page: hasMore ? charCursor : null,
|
|
52
|
+
page_length: pageLength,
|
|
53
|
+
total_issues: totalIssues,
|
|
50
54
|
};
|
|
51
55
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import config from "../../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { filterLinesByKeywords, validateLogResponse } from "./utils.js";
|
|
3
3
|
const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
|
|
4
4
|
// DEVICE LOGS
|
|
5
5
|
export async function retrieveDeviceLogs(sessionId, buildId) {
|
|
@@ -10,9 +10,14 @@ export async function retrieveDeviceLogs(sessionId, buildId) {
|
|
|
10
10
|
Authorization: `Basic ${auth}`,
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
|
-
|
|
13
|
+
const validationError = validateLogResponse(response, "device logs");
|
|
14
|
+
if (validationError)
|
|
15
|
+
return validationError.message;
|
|
14
16
|
const logText = await response.text();
|
|
15
|
-
|
|
17
|
+
const logs = filterDeviceFailures(logText);
|
|
18
|
+
return logs.length > 0
|
|
19
|
+
? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
20
|
+
: "No device failures found";
|
|
16
21
|
}
|
|
17
22
|
// APPIUM LOGS
|
|
18
23
|
export async function retrieveAppiumLogs(sessionId, buildId) {
|
|
@@ -23,9 +28,14 @@ export async function retrieveAppiumLogs(sessionId, buildId) {
|
|
|
23
28
|
Authorization: `Basic ${auth}`,
|
|
24
29
|
},
|
|
25
30
|
});
|
|
26
|
-
|
|
31
|
+
const validationError = validateLogResponse(response, "Appium logs");
|
|
32
|
+
if (validationError)
|
|
33
|
+
return validationError.message;
|
|
27
34
|
const logText = await response.text();
|
|
28
|
-
|
|
35
|
+
const logs = filterAppiumFailures(logText);
|
|
36
|
+
return logs.length > 0
|
|
37
|
+
? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
38
|
+
: "No Appium failures found";
|
|
29
39
|
}
|
|
30
40
|
// CRASH LOGS
|
|
31
41
|
export async function retrieveCrashLogs(sessionId, buildId) {
|
|
@@ -36,9 +46,14 @@ export async function retrieveCrashLogs(sessionId, buildId) {
|
|
|
36
46
|
Authorization: `Basic ${auth}`,
|
|
37
47
|
},
|
|
38
48
|
});
|
|
39
|
-
|
|
49
|
+
const validationError = validateLogResponse(response, "crash logs");
|
|
50
|
+
if (validationError)
|
|
51
|
+
return validationError.message;
|
|
40
52
|
const logText = await response.text();
|
|
41
|
-
|
|
53
|
+
const logs = filterCrashFailures(logText);
|
|
54
|
+
return logs.length > 0
|
|
55
|
+
? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
56
|
+
: "No crash failures found";
|
|
42
57
|
}
|
|
43
58
|
// FILTER HELPERS
|
|
44
59
|
export function filterDeviceFailures(logText) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import config from "../../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { filterLinesByKeywords, validateLogResponse, } from "./utils.js";
|
|
3
3
|
const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
|
|
4
4
|
// NETWORK LOGS
|
|
5
5
|
export async function retrieveNetworkFailures(sessionId) {
|
|
@@ -11,30 +11,30 @@ export async function retrieveNetworkFailures(sessionId) {
|
|
|
11
11
|
Authorization: `Basic ${auth}`,
|
|
12
12
|
},
|
|
13
13
|
});
|
|
14
|
-
|
|
14
|
+
const validationError = validateLogResponse(response, "network logs");
|
|
15
|
+
if (validationError)
|
|
16
|
+
return validationError.message;
|
|
15
17
|
const networklogs = await response.json();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
time: entry.time,
|
|
37
|
-
}));
|
|
18
|
+
const failureEntries = networklogs.log.entries.filter((entry) => entry.response.status === 0 ||
|
|
19
|
+
entry.response.status >= 400 ||
|
|
20
|
+
entry.response._error !== undefined);
|
|
21
|
+
return failureEntries.length > 0
|
|
22
|
+
? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(failureEntries.map((entry) => ({
|
|
23
|
+
startedDateTime: entry.startedDateTime,
|
|
24
|
+
request: {
|
|
25
|
+
method: entry.request?.method,
|
|
26
|
+
url: entry.request?.url,
|
|
27
|
+
queryString: entry.request?.queryString,
|
|
28
|
+
},
|
|
29
|
+
response: {
|
|
30
|
+
status: entry.response?.status,
|
|
31
|
+
statusText: entry.response?.statusText,
|
|
32
|
+
_error: entry.response?._error,
|
|
33
|
+
},
|
|
34
|
+
serverIPAddress: entry.serverIPAddress,
|
|
35
|
+
time: entry.time,
|
|
36
|
+
})), null, 2)}`
|
|
37
|
+
: "No network failures found";
|
|
38
38
|
}
|
|
39
39
|
// SESSION LOGS
|
|
40
40
|
export async function retrieveSessionFailures(sessionId) {
|
|
@@ -45,9 +45,14 @@ export async function retrieveSessionFailures(sessionId) {
|
|
|
45
45
|
Authorization: `Basic ${auth}`,
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
|
-
|
|
48
|
+
const validationError = validateLogResponse(response, "session logs");
|
|
49
|
+
if (validationError)
|
|
50
|
+
return validationError.message;
|
|
49
51
|
const logText = await response.text();
|
|
50
|
-
|
|
52
|
+
const logs = filterSessionFailures(logText);
|
|
53
|
+
return logs.length > 0
|
|
54
|
+
? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
55
|
+
: "No session failures found";
|
|
51
56
|
}
|
|
52
57
|
// CONSOLE LOGS
|
|
53
58
|
export async function retrieveConsoleFailures(sessionId) {
|
|
@@ -58,9 +63,14 @@ export async function retrieveConsoleFailures(sessionId) {
|
|
|
58
63
|
Authorization: `Basic ${auth}`,
|
|
59
64
|
},
|
|
60
65
|
});
|
|
61
|
-
|
|
66
|
+
const validationError = validateLogResponse(response, "console logs");
|
|
67
|
+
if (validationError)
|
|
68
|
+
return validationError.message;
|
|
62
69
|
const logText = await response.text();
|
|
63
|
-
|
|
70
|
+
const logs = filterConsoleFailures(logText);
|
|
71
|
+
return logs.length > 0
|
|
72
|
+
? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
73
|
+
: "No console failures found";
|
|
64
74
|
}
|
|
65
75
|
// FILTER: session logs
|
|
66
76
|
export function filterSessionFailures(logText) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function validateLogResponse(response, logType) {
|
|
2
|
+
if (!response.ok) {
|
|
3
|
+
if (response.status === 404) {
|
|
4
|
+
return { message: `No ${logType} available for this session` };
|
|
5
|
+
}
|
|
6
|
+
if (response.status === 401 || response.status === 403) {
|
|
7
|
+
return {
|
|
8
|
+
message: `Unable to access ${logType} - please check your credentials`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return { message: `Unable to fetch ${logType} at this time` };
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function filterLinesByKeywords(logText, keywords) {
|
|
16
|
+
return logText
|
|
17
|
+
.split(/\r?\n/)
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter((line) => keywords.some((keyword) => line.toLowerCase().includes(keyword)));
|
|
20
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import logger from "../logger.js";
|
|
3
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
3
4
|
import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
|
|
4
5
|
import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
|
|
5
|
-
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
6
|
import { AppAutomateLogType, AutomateLogType, SessionType, } from "../lib/constants.js";
|
|
7
7
|
// Main log fetcher function
|
|
8
8
|
export async function getFailureLogs(args) {
|
|
@@ -47,68 +47,39 @@ export async function getFailureLogs(args) {
|
|
|
47
47
|
],
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
let response;
|
|
50
51
|
// eslint-disable-next-line no-useless-catch
|
|
51
52
|
try {
|
|
52
53
|
for (const logType of validLogTypes) {
|
|
53
54
|
switch (logType) {
|
|
54
55
|
case AutomateLogType.NetworkLogs: {
|
|
55
|
-
|
|
56
|
-
results.push({
|
|
57
|
-
type: "text",
|
|
58
|
-
text: logs.length > 0
|
|
59
|
-
? `Network Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
60
|
-
: "No network failures found",
|
|
61
|
-
});
|
|
56
|
+
response = await retrieveNetworkFailures(args.sessionId);
|
|
57
|
+
results.push({ type: "text", text: response });
|
|
62
58
|
break;
|
|
63
59
|
}
|
|
64
60
|
case AutomateLogType.SessionLogs: {
|
|
65
|
-
|
|
66
|
-
results.push({
|
|
67
|
-
type: "text",
|
|
68
|
-
text: logs.length > 0
|
|
69
|
-
? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
70
|
-
: "No session failures found",
|
|
71
|
-
});
|
|
61
|
+
response = await retrieveSessionFailures(args.sessionId);
|
|
62
|
+
results.push({ type: "text", text: response });
|
|
72
63
|
break;
|
|
73
64
|
}
|
|
74
65
|
case AutomateLogType.ConsoleLogs: {
|
|
75
|
-
|
|
76
|
-
results.push({
|
|
77
|
-
type: "text",
|
|
78
|
-
text: logs.length > 0
|
|
79
|
-
? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
80
|
-
: "No console failures found",
|
|
81
|
-
});
|
|
66
|
+
response = await retrieveConsoleFailures(args.sessionId);
|
|
67
|
+
results.push({ type: "text", text: response });
|
|
82
68
|
break;
|
|
83
69
|
}
|
|
84
70
|
case AppAutomateLogType.DeviceLogs: {
|
|
85
|
-
|
|
86
|
-
results.push({
|
|
87
|
-
type: "text",
|
|
88
|
-
text: logs.length > 0
|
|
89
|
-
? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
90
|
-
: "No device failures found",
|
|
91
|
-
});
|
|
71
|
+
response = await retrieveDeviceLogs(args.sessionId, args.buildId);
|
|
72
|
+
results.push({ type: "text", text: response });
|
|
92
73
|
break;
|
|
93
74
|
}
|
|
94
75
|
case AppAutomateLogType.AppiumLogs: {
|
|
95
|
-
|
|
96
|
-
results.push({
|
|
97
|
-
type: "text",
|
|
98
|
-
text: logs.length > 0
|
|
99
|
-
? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
100
|
-
: "No Appium failures found",
|
|
101
|
-
});
|
|
76
|
+
response = await retrieveAppiumLogs(args.sessionId, args.buildId);
|
|
77
|
+
results.push({ type: "text", text: response });
|
|
102
78
|
break;
|
|
103
79
|
}
|
|
104
80
|
case AppAutomateLogType.CrashLogs: {
|
|
105
|
-
|
|
106
|
-
results.push({
|
|
107
|
-
type: "text",
|
|
108
|
-
text: logs.length > 0
|
|
109
|
-
? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
110
|
-
: "No crash failures found",
|
|
111
|
-
});
|
|
81
|
+
response = await retrieveCrashLogs(args.sessionId, args.buildId);
|
|
82
|
+
results.push({ type: "text", text: response });
|
|
112
83
|
break;
|
|
113
84
|
}
|
|
114
85
|
}
|
|
@@ -2,6 +2,7 @@ import axios from "axios";
|
|
|
2
2
|
import config from "../../config.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { formatAxiosError } from "../../lib/error.js"; // or correct path
|
|
5
|
+
import { projectIdentifierToId } from "../testmanagement-utils/TCG-utils/api.js";
|
|
5
6
|
// Schema for combined project/folder creation
|
|
6
7
|
export const CreateProjFoldSchema = z.object({
|
|
7
8
|
project_name: z
|
|
@@ -78,11 +79,16 @@ export async function createProjectOrFolder(args) {
|
|
|
78
79
|
}
|
|
79
80
|
// Folder created successfully
|
|
80
81
|
const folder = res.data.folder;
|
|
82
|
+
const projectId = await projectIdentifierToId(projId);
|
|
81
83
|
return {
|
|
82
84
|
content: [
|
|
83
85
|
{
|
|
84
86
|
type: "text",
|
|
85
|
-
text: `Folder created:
|
|
87
|
+
text: `Folder successfully created:
|
|
88
|
+
- ID: ${folder.id}
|
|
89
|
+
- Name: ${folder.name}
|
|
90
|
+
- Project Identifier: ${projId}
|
|
91
|
+
Access it here: https://test-management.browserstack.com/projects/${projectId}/folder/${folder.id}/`,
|
|
86
92
|
},
|
|
87
93
|
],
|
|
88
94
|
};
|
|
@@ -2,6 +2,7 @@ import axios from "axios";
|
|
|
2
2
|
import config from "../../config.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { formatAxiosError } from "../../lib/error.js"; // or correct
|
|
5
|
+
import { projectIdentifierToId } from "./TCG-utils/api.js";
|
|
5
6
|
export const CreateTestCaseSchema = z.object({
|
|
6
7
|
project_identifier: z
|
|
7
8
|
.string()
|
|
@@ -90,13 +91,21 @@ export async function createTestCase(params) {
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
const tc = data.test_case;
|
|
94
|
+
const projectId = await projectIdentifierToId(params.project_identifier);
|
|
93
95
|
return {
|
|
94
96
|
content: [
|
|
95
97
|
{
|
|
96
98
|
type: "text",
|
|
97
|
-
text: `
|
|
99
|
+
text: `Test case successfully created:
|
|
100
|
+
- Identifier: ${tc.identifier}
|
|
101
|
+
- Title: ${tc.title}
|
|
102
|
+
|
|
103
|
+
You can view it here: https://test-management.browserstack.com/projects/${projectId}/folder/search?q=${tc.identifier}`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: JSON.stringify(tc, null, 2),
|
|
98
108
|
},
|
|
99
|
-
{ type: "text", text: JSON.stringify(tc, null, 2) },
|
|
100
109
|
],
|
|
101
110
|
};
|
|
102
111
|
}
|