@browserstack/mcp-server 1.0.12 → 1.0.14
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 +31 -30
- 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 +9 -12
- package/dist/tools/accessiblity-utils/accessibility.js +14 -22
- 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 +61 -0
- 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 +115 -46
- package/package.json +10 -6
package/dist/tools/automate.js
CHANGED
|
@@ -1,62 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const zod_1 = require("zod");
|
|
9
|
-
const logger_1 = __importDefault(require("../logger"));
|
|
10
|
-
const api_1 = require("../lib/api");
|
|
11
|
-
const instrumentation_1 = require("../lib/instrumentation");
|
|
12
|
-
/**
|
|
13
|
-
* Fetches failed network requests from a BrowserStack Automate session.
|
|
14
|
-
* Returns network requests that resulted in errors or failed to complete.
|
|
15
|
-
*/
|
|
16
|
-
async function getNetworkFailures(args) {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchAutomationScreenshots } from "./automate-utils/fetch-screenshots.js";
|
|
3
|
+
import { SessionType } from "../lib/constants.js";
|
|
4
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
5
|
+
import logger from "../logger.js";
|
|
6
|
+
// Tool function that fetches and processes screenshots from BrowserStack Automate session
|
|
7
|
+
export async function fetchAutomationScreenshotsTool(args) {
|
|
17
8
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
9
|
+
const screenshots = await fetchAutomationScreenshots(args.sessionId, args.sessionType);
|
|
10
|
+
if (screenshots.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: "No screenshots found in the session or some unexpected error occurred",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const results = screenshots.map((screenshot, index) => ({
|
|
22
|
+
type: "image",
|
|
23
|
+
text: `Screenshot ${index + 1}`,
|
|
24
|
+
data: screenshot.base64,
|
|
25
|
+
mimeType: "image/png",
|
|
26
|
+
metadata: { url: screenshot.url },
|
|
27
|
+
}));
|
|
25
28
|
return {
|
|
26
29
|
content: [
|
|
27
30
|
{
|
|
28
31
|
type: "text",
|
|
29
|
-
text
|
|
32
|
+
text: `Retrieved ${screenshots.length} screenshot(s) from the end of the session.`,
|
|
30
33
|
},
|
|
34
|
+
...results,
|
|
31
35
|
],
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
catch (error) {
|
|
35
|
-
|
|
36
|
-
throw
|
|
39
|
+
logger.error("Error during fetching screenshots", error);
|
|
40
|
+
throw error;
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
//Registers the fetchAutomationScreenshots tool with the MCP server
|
|
44
|
+
export default function addAutomationTools(server) {
|
|
45
|
+
server.tool("fetchAutomationScreenshots", "Fetch and process screenshots from a BrowserStack Automate session", {
|
|
46
|
+
sessionId: z
|
|
47
|
+
.string()
|
|
48
|
+
.describe("The BrowserStack session ID to fetch screenshots from"),
|
|
49
|
+
sessionType: z
|
|
50
|
+
.enum([SessionType.Automate, SessionType.AppAutomate])
|
|
51
|
+
.describe("Type of BrowserStack session"),
|
|
42
52
|
}, async (args) => {
|
|
43
53
|
try {
|
|
44
|
-
|
|
45
|
-
return await
|
|
54
|
+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion());
|
|
55
|
+
return await fetchAutomationScreenshotsTool(args);
|
|
46
56
|
}
|
|
47
57
|
catch (error) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
(0, instrumentation_1.trackMCP)("getNetworkFailures", server.server.getClientVersion(), error);
|
|
58
|
+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion(), error);
|
|
59
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
51
60
|
return {
|
|
52
61
|
content: [
|
|
53
62
|
{
|
|
54
63
|
type: "text",
|
|
55
|
-
text: `
|
|
56
|
-
isError: true,
|
|
64
|
+
text: `Error during fetching automate screenshots: ${errorMessage}`,
|
|
57
65
|
},
|
|
58
66
|
],
|
|
59
|
-
isError: true,
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
});
|
package/dist/tools/bstack-sdk.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.default = addSDKTools;
|
|
5
|
-
const zod_1 = require("zod");
|
|
6
|
-
const instructions_1 = require("./sdk-utils/instructions");
|
|
7
|
-
const instrumentation_1 = require("../lib/instrumentation");
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { generateBrowserStackYMLInstructions, getInstructionsForProjectConfiguration, } from "./sdk-utils/instructions.js";
|
|
3
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
8
4
|
/**
|
|
9
5
|
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
|
|
10
6
|
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
|
|
11
7
|
*/
|
|
12
|
-
async function bootstrapProjectWithSDK({ detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage, desiredPlatforms, }) {
|
|
13
|
-
const instructions =
|
|
14
|
-
const instructionsForProjectConfiguration =
|
|
8
|
+
export async function bootstrapProjectWithSDK({ detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage, desiredPlatforms, }) {
|
|
9
|
+
const instructions = generateBrowserStackYMLInstructions(desiredPlatforms);
|
|
10
|
+
const instructionsForProjectConfiguration = getInstructionsForProjectConfiguration(detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage);
|
|
15
11
|
return {
|
|
16
12
|
content: [
|
|
17
13
|
{
|
|
@@ -22,23 +18,23 @@ async function bootstrapProjectWithSDK({ detectedBrowserAutomationFramework, det
|
|
|
22
18
|
],
|
|
23
19
|
};
|
|
24
20
|
}
|
|
25
|
-
function addSDKTools(server) {
|
|
21
|
+
export default function addSDKTools(server) {
|
|
26
22
|
server.tool("runTestsOnBrowserStack", "Use this tool to get instructions for running tests on BrowserStack.", {
|
|
27
|
-
detectedBrowserAutomationFramework:
|
|
23
|
+
detectedBrowserAutomationFramework: z
|
|
28
24
|
.string()
|
|
29
25
|
.describe("The automation framework configured in the project. Example: 'playwright', 'selenium'"),
|
|
30
|
-
detectedTestingFramework:
|
|
26
|
+
detectedTestingFramework: z
|
|
31
27
|
.string()
|
|
32
28
|
.describe("The testing framework used in the project. Example: 'jest', 'pytest'"),
|
|
33
|
-
detectedLanguage:
|
|
29
|
+
detectedLanguage: z
|
|
34
30
|
.string()
|
|
35
31
|
.describe("The programming language used in the project. Example: 'nodejs', 'python'"),
|
|
36
|
-
desiredPlatforms:
|
|
37
|
-
.array(
|
|
32
|
+
desiredPlatforms: z
|
|
33
|
+
.array(z.enum(["windows", "macos", "android", "ios"]))
|
|
38
34
|
.describe("The platforms the user wants to test on. Always ask this to the user, do not try to infer this."),
|
|
39
35
|
}, async (args) => {
|
|
40
36
|
try {
|
|
41
|
-
|
|
37
|
+
trackMCP("runTestsOnBrowserStack", server.server.getClientVersion());
|
|
42
38
|
return await bootstrapProjectWithSDK({
|
|
43
39
|
detectedBrowserAutomationFramework: args.detectedBrowserAutomationFramework,
|
|
44
40
|
detectedTestingFramework: args.detectedTestingFramework,
|
|
@@ -47,7 +43,7 @@ function addSDKTools(server) {
|
|
|
47
43
|
});
|
|
48
44
|
}
|
|
49
45
|
catch (error) {
|
|
50
|
-
|
|
46
|
+
trackMCP("runTestsOnBrowserStack", server.server.getClientVersion(), error);
|
|
51
47
|
return {
|
|
52
48
|
content: [
|
|
53
49
|
{
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import config from "../../config.js";
|
|
2
|
+
import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
|
|
3
|
+
const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
|
|
4
|
+
// DEVICE LOGS
|
|
5
|
+
export async function retrieveDeviceLogs(sessionId, buildId) {
|
|
6
|
+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/deviceLogs`;
|
|
7
|
+
const response = await fetch(url, {
|
|
8
|
+
headers: {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
Authorization: `Basic ${auth}`,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
await assertOkResponse(response, "device logs");
|
|
14
|
+
const logText = await response.text();
|
|
15
|
+
return filterDeviceFailures(logText);
|
|
16
|
+
}
|
|
17
|
+
// APPIUM LOGS
|
|
18
|
+
export async function retrieveAppiumLogs(sessionId, buildId) {
|
|
19
|
+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/appiumlogs`;
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
Authorization: `Basic ${auth}`,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
await assertOkResponse(response, "Appium logs");
|
|
27
|
+
const logText = await response.text();
|
|
28
|
+
return filterAppiumFailures(logText);
|
|
29
|
+
}
|
|
30
|
+
// CRASH LOGS
|
|
31
|
+
export async function retrieveCrashLogs(sessionId, buildId) {
|
|
32
|
+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/crashlogs`;
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: `Basic ${auth}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
await assertOkResponse(response, "crash logs");
|
|
40
|
+
const logText = await response.text();
|
|
41
|
+
return filterCrashFailures(logText);
|
|
42
|
+
}
|
|
43
|
+
// FILTER HELPERS
|
|
44
|
+
export function filterDeviceFailures(logText) {
|
|
45
|
+
const keywords = [
|
|
46
|
+
"error",
|
|
47
|
+
"exception",
|
|
48
|
+
"fatal",
|
|
49
|
+
"anr",
|
|
50
|
+
"not responding",
|
|
51
|
+
"process crashed",
|
|
52
|
+
"crash",
|
|
53
|
+
"force close",
|
|
54
|
+
"signal",
|
|
55
|
+
"java.lang.",
|
|
56
|
+
"unable to",
|
|
57
|
+
];
|
|
58
|
+
return filterLinesByKeywords(logText, keywords);
|
|
59
|
+
}
|
|
60
|
+
export function filterAppiumFailures(logText) {
|
|
61
|
+
const keywords = [
|
|
62
|
+
"error",
|
|
63
|
+
"fail",
|
|
64
|
+
"exception",
|
|
65
|
+
"not found",
|
|
66
|
+
"no such element",
|
|
67
|
+
"unable to",
|
|
68
|
+
"stacktrace",
|
|
69
|
+
"appium exited",
|
|
70
|
+
"command failed",
|
|
71
|
+
"invalid selector",
|
|
72
|
+
];
|
|
73
|
+
return filterLinesByKeywords(logText, keywords);
|
|
74
|
+
}
|
|
75
|
+
export function filterCrashFailures(logText) {
|
|
76
|
+
const keywords = [
|
|
77
|
+
"fatal exception",
|
|
78
|
+
"crash",
|
|
79
|
+
"signal",
|
|
80
|
+
"java.lang.",
|
|
81
|
+
"caused by:",
|
|
82
|
+
"native crash",
|
|
83
|
+
"anr",
|
|
84
|
+
"abort message",
|
|
85
|
+
"application has stopped unexpectedly",
|
|
86
|
+
];
|
|
87
|
+
return filterLinesByKeywords(logText, keywords);
|
|
88
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import config from "../../config.js";
|
|
2
|
+
import { assertOkResponse, filterLinesByKeywords } from "../../lib/utils.js";
|
|
3
|
+
const auth = Buffer.from(`${config.browserstackUsername}:${config.browserstackAccessKey}`).toString("base64");
|
|
4
|
+
// NETWORK LOGS
|
|
5
|
+
export async function retrieveNetworkFailures(sessionId) {
|
|
6
|
+
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/networklogs`;
|
|
7
|
+
const response = await fetch(url, {
|
|
8
|
+
method: "GET",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
Authorization: `Basic ${auth}`,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
await assertOkResponse(response, "network logs");
|
|
15
|
+
const networklogs = await response.json();
|
|
16
|
+
// Filter for failure logs
|
|
17
|
+
const failureEntries = networklogs.log.entries.filter((entry) => {
|
|
18
|
+
return (entry.response.status === 0 ||
|
|
19
|
+
entry.response.status >= 400 ||
|
|
20
|
+
entry.response._error !== undefined);
|
|
21
|
+
});
|
|
22
|
+
// Return only the failure entries with some context
|
|
23
|
+
return failureEntries.map((entry) => ({
|
|
24
|
+
startedDateTime: entry.startedDateTime,
|
|
25
|
+
request: {
|
|
26
|
+
method: entry.request?.method,
|
|
27
|
+
url: entry.request?.url,
|
|
28
|
+
queryString: entry.request?.queryString,
|
|
29
|
+
},
|
|
30
|
+
response: {
|
|
31
|
+
status: entry.response?.status,
|
|
32
|
+
statusText: entry.response?.statusText,
|
|
33
|
+
_error: entry.response?._error,
|
|
34
|
+
},
|
|
35
|
+
serverIPAddress: entry.serverIPAddress,
|
|
36
|
+
time: entry.time,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
// SESSION LOGS
|
|
40
|
+
export async function retrieveSessionFailures(sessionId) {
|
|
41
|
+
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/logs`;
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
Authorization: `Basic ${auth}`,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
await assertOkResponse(response, "session logs");
|
|
49
|
+
const logText = await response.text();
|
|
50
|
+
return filterSessionFailures(logText);
|
|
51
|
+
}
|
|
52
|
+
// CONSOLE LOGS
|
|
53
|
+
export async function retrieveConsoleFailures(sessionId) {
|
|
54
|
+
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/consolelogs`;
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
Authorization: `Basic ${auth}`,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
await assertOkResponse(response, "console logs");
|
|
62
|
+
const logText = await response.text();
|
|
63
|
+
return filterConsoleFailures(logText);
|
|
64
|
+
}
|
|
65
|
+
// FILTER: session logs
|
|
66
|
+
export function filterSessionFailures(logText) {
|
|
67
|
+
const keywords = [
|
|
68
|
+
"error",
|
|
69
|
+
"fail",
|
|
70
|
+
"exception",
|
|
71
|
+
"fatal",
|
|
72
|
+
"unable to",
|
|
73
|
+
"not found",
|
|
74
|
+
'"success":false',
|
|
75
|
+
'"success": false',
|
|
76
|
+
'"msg":',
|
|
77
|
+
"console.error",
|
|
78
|
+
"stderr",
|
|
79
|
+
];
|
|
80
|
+
return filterLinesByKeywords(logText, keywords);
|
|
81
|
+
}
|
|
82
|
+
// FILTER: console logs
|
|
83
|
+
export function filterConsoleFailures(logText) {
|
|
84
|
+
const keywords = [
|
|
85
|
+
"failed to load resource",
|
|
86
|
+
"uncaught",
|
|
87
|
+
"typeerror",
|
|
88
|
+
"referenceerror",
|
|
89
|
+
"scanner is not ready",
|
|
90
|
+
"status of 4",
|
|
91
|
+
"status of 5",
|
|
92
|
+
"not found",
|
|
93
|
+
"undefined",
|
|
94
|
+
"error:",
|
|
95
|
+
];
|
|
96
|
+
return filterLinesByKeywords(logText, keywords);
|
|
97
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import logger from "../logger.js";
|
|
3
|
+
import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
|
|
4
|
+
import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
|
|
5
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
|
+
import { AppAutomateLogType, AutomateLogType, SessionType, } from "../lib/constants.js";
|
|
7
|
+
// Main log fetcher function
|
|
8
|
+
export async function getFailureLogs(args) {
|
|
9
|
+
const results = [];
|
|
10
|
+
const errors = [];
|
|
11
|
+
let validLogTypes = [];
|
|
12
|
+
if (!args.sessionId) {
|
|
13
|
+
throw new Error("Session ID is required");
|
|
14
|
+
}
|
|
15
|
+
if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
|
|
16
|
+
throw new Error("Build ID is required for app-automate sessions");
|
|
17
|
+
}
|
|
18
|
+
// Validate log types and collect errors
|
|
19
|
+
validLogTypes = args.logTypes.filter((logType) => {
|
|
20
|
+
const isAutomate = Object.values(AutomateLogType).includes(logType);
|
|
21
|
+
const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
|
|
22
|
+
if (!isAutomate && !isAppAutomate) {
|
|
23
|
+
errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
|
|
24
|
+
...Object.values(AutomateLogType),
|
|
25
|
+
...Object.values(AppAutomateLogType),
|
|
26
|
+
].join(", ")}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (args.sessionType === SessionType.Automate && !isAutomate) {
|
|
30
|
+
errors.push(`Log type '${logType}' is only available for app-automate sessions.`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (args.sessionType === SessionType.AppAutomate && !isAppAutomate) {
|
|
34
|
+
errors.push(`Log type '${logType}' is only available for automate sessions.`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
if (validLogTypes.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: `No valid log types found for ${args.sessionType} session.\nErrors encountered:\n${errors.join("\n")}`,
|
|
45
|
+
isError: true,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// eslint-disable-next-line no-useless-catch
|
|
51
|
+
try {
|
|
52
|
+
for (const logType of validLogTypes) {
|
|
53
|
+
switch (logType) {
|
|
54
|
+
case AutomateLogType.NetworkLogs: {
|
|
55
|
+
const logs = await retrieveNetworkFailures(args.sessionId);
|
|
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
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case AutomateLogType.SessionLogs: {
|
|
65
|
+
const logs = await retrieveSessionFailures(args.sessionId);
|
|
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
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case AutomateLogType.ConsoleLogs: {
|
|
75
|
+
const logs = await retrieveConsoleFailures(args.sessionId);
|
|
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
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case AppAutomateLogType.DeviceLogs: {
|
|
85
|
+
const logs = await retrieveDeviceLogs(args.sessionId, args.buildId);
|
|
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
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case AppAutomateLogType.AppiumLogs: {
|
|
95
|
+
const logs = await retrieveAppiumLogs(args.sessionId, args.buildId);
|
|
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
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case AppAutomateLogType.CrashLogs: {
|
|
105
|
+
const logs = await retrieveCrashLogs(args.sessionId, args.buildId);
|
|
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
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
results.push({
|
|
122
|
+
type: "text",
|
|
123
|
+
text: `Errors encountered:\n${errors.join("\n")}`,
|
|
124
|
+
isError: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { content: results };
|
|
128
|
+
}
|
|
129
|
+
// Register tool with the MCP server
|
|
130
|
+
export default function registerGetFailureLogs(server) {
|
|
131
|
+
server.tool("getFailureLogs", "Fetch various types of logs from a BrowserStack session. Supports both automate and app-automate sessions.", {
|
|
132
|
+
sessionType: z
|
|
133
|
+
.enum([SessionType.Automate, SessionType.AppAutomate])
|
|
134
|
+
.describe("Type of BrowserStack session. Must be explicitly provided by the user."),
|
|
135
|
+
sessionId: z
|
|
136
|
+
.string()
|
|
137
|
+
.describe("The BrowserStack session ID. Must be explicitly provided by the user."),
|
|
138
|
+
buildId: z
|
|
139
|
+
.string()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Required only when sessionType is 'app-automate'. If sessionType is 'app-automate', always ask the user to provide the build ID before proceeding."),
|
|
142
|
+
logTypes: z
|
|
143
|
+
.array(z.enum([
|
|
144
|
+
AutomateLogType.NetworkLogs,
|
|
145
|
+
AutomateLogType.SessionLogs,
|
|
146
|
+
AutomateLogType.ConsoleLogs,
|
|
147
|
+
AppAutomateLogType.DeviceLogs,
|
|
148
|
+
AppAutomateLogType.AppiumLogs,
|
|
149
|
+
AppAutomateLogType.CrashLogs,
|
|
150
|
+
]))
|
|
151
|
+
.describe("The types of logs to fetch."),
|
|
152
|
+
}, async (args) => {
|
|
153
|
+
try {
|
|
154
|
+
trackMCP("getFailureLogs", server.server.getClientVersion());
|
|
155
|
+
return await getFailureLogs(args);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
trackMCP("getFailureLogs", server.server.getClientVersion(), error);
|
|
160
|
+
logger.error("Failed to fetch logs: %s", message);
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `Failed to fetch logs: ${message}`,
|
|
166
|
+
isError: true,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
isError: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const fuzzy_1 = require("../../lib/fuzzy");
|
|
7
|
-
async function filterDesktop(args) {
|
|
8
|
-
const data = await (0, device_cache_1.getDevicesAndBrowsers)("live");
|
|
1
|
+
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
2
|
+
import { resolveVersion } from "./version-resolver.js";
|
|
3
|
+
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
4
|
+
export async function filterDesktop(args) {
|
|
5
|
+
const data = await getDevicesAndBrowsers(BrowserStackProducts.LIVE);
|
|
9
6
|
const allEntries = getAllDesktopEntries(data);
|
|
10
7
|
// Filter OS
|
|
11
8
|
const osList = filterByOS(allEntries, args.os);
|
|
@@ -18,7 +15,7 @@ async function filterDesktop(args) {
|
|
|
18
15
|
const entriesForOS = filterByOSVersion(browserList, chosenOS);
|
|
19
16
|
// Resolve browser version
|
|
20
17
|
const browserVersions = entriesForOS.map((e) => e.browser_version);
|
|
21
|
-
const chosenBrowserVersion =
|
|
18
|
+
const chosenBrowserVersion = resolveVersion(args.browserVersion, browserVersions);
|
|
22
19
|
// Find final entry
|
|
23
20
|
const finalEntry = entriesForOS.find((e) => e.browser_version === chosenBrowserVersion);
|
|
24
21
|
if (!finalEntry) {
|
|
@@ -57,7 +54,7 @@ function resolveOSVersion(os, requestedVersion, availableVersions) {
|
|
|
57
54
|
}
|
|
58
55
|
else {
|
|
59
56
|
// For Windows, use semantic versioning
|
|
60
|
-
return
|
|
57
|
+
return resolveVersion(requestedVersion, availableVersions);
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
function resolveMacOSVersion(requested, available) {
|
|
@@ -69,7 +66,7 @@ function resolveMacOSVersion(requested, available) {
|
|
|
69
66
|
}
|
|
70
67
|
else {
|
|
71
68
|
// Try fuzzy matching
|
|
72
|
-
const fuzzy =
|
|
69
|
+
const fuzzy = customFuzzySearch(available.map((v) => ({ os_version: v })), ["os_version"], requested, 1);
|
|
73
70
|
const matched = fuzzy.length ? fuzzy[0].os_version : requested;
|
|
74
71
|
// Fallback if not valid
|
|
75
72
|
return available.includes(matched) ? matched : available[0];
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const device_cache_1 = require("../../lib/device-cache");
|
|
5
|
-
const version_resolver_1 = require("./version-resolver");
|
|
6
|
-
const fuzzy_1 = require("../../lib/fuzzy");
|
|
1
|
+
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
2
|
+
import { resolveVersion } from "./version-resolver.js";
|
|
3
|
+
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
7
4
|
// Extract all mobile entries from the data
|
|
8
5
|
function getAllMobileEntries(data) {
|
|
9
6
|
return data.mobile.flatMap((grp) => grp.devices.map((d) => ({
|
|
@@ -22,7 +19,7 @@ function filterByOS(entries, os) {
|
|
|
22
19
|
}
|
|
23
20
|
// Find matching device with exact match validation
|
|
24
21
|
function findMatchingDevice(entries, deviceName, os) {
|
|
25
|
-
const matches =
|
|
22
|
+
const matches = customFuzzySearch(entries, ["display_name"], deviceName, 5);
|
|
26
23
|
if (!matches.length)
|
|
27
24
|
throw new Error(`No devices matching "${deviceName}" on ${os}.`);
|
|
28
25
|
const exact = matches.find((m) => m.display_name.toLowerCase() === deviceName.toLowerCase());
|
|
@@ -38,7 +35,7 @@ function findMatchingDevice(entries, deviceName, os) {
|
|
|
38
35
|
// Find the appropriate OS version
|
|
39
36
|
function findOSVersion(entries, requestedVersion) {
|
|
40
37
|
const versions = entries.map((d) => d.os_version);
|
|
41
|
-
const chosenVersion =
|
|
38
|
+
const chosenVersion = resolveVersion(requestedVersion, versions);
|
|
42
39
|
const result = entries.filter((d) => d.os_version === chosenVersion);
|
|
43
40
|
if (!result.length)
|
|
44
41
|
throw new Error(`No entry for OS version "${requestedVersion}".`);
|
|
@@ -53,8 +50,8 @@ function createVersionNote(requestedVersion, actualVersion) {
|
|
|
53
50
|
}
|
|
54
51
|
return "";
|
|
55
52
|
}
|
|
56
|
-
async function filterMobile(args) {
|
|
57
|
-
const data = await (
|
|
53
|
+
export async function filterMobile(args) {
|
|
54
|
+
const data = await getDevicesAndBrowsers(BrowserStackProducts.LIVE);
|
|
58
55
|
const allEntries = getAllMobileEntries(data);
|
|
59
56
|
const osCandidates = filterByOS(allEntries, args.os);
|
|
60
57
|
const deviceCandidates = findMatchingDevice(osCandidates, args.device, args.os);
|