Skip to content
1 change: 1 addition & 0 deletions src/lib/inmemory-store.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const signedUrlMap = new Map<string, object>();
export const testFilePathsMap = new Map<string, string[]>();
2 changes: 2 additions & 0 deletions src/server-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
import logger from "./logger.js";
import addSDKTools from "./tools/bstack-sdk.js";
import addPercyTools from "./tools/percy-sdk.js";
import addBrowserLiveTools from "./tools/live.js";
import addAccessibilityTools from "./tools/accessibility.js";
import addTestManagementTools from "./tools/testmanagement.js";
Expand Down Expand Up @@ -48,6 +49,7 @@ export class BrowserStackMcpServer {
const toolAdders = [
addAccessibilityTools,
addSDKTools,
addPercyTools,
addAppLiveTools,
addBrowserLiveTools,
addTestManagementTools,
Expand Down
32 changes: 32 additions & 0 deletions src/tools/add-percy-snapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { testFilePathsMap } from "../lib/inmemory-store.js";
import { updateFileAndStep } from "./percy-snapshot-utils/utils.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js";

export async function updateTestsWithPercyCommands(args: {
uuid: string;
index: number;
}): Promise<CallToolResult> {
const { uuid, index } = args;
const filePaths = testFilePathsMap.get(uuid);

if (!filePaths) {
throw new Error(`No test files found in memory for UUID: ${uuid}`);
}

if (index < 0 || index >= filePaths.length) {
throw new Error(
`Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`,
);
}
const result = await updateFileAndStep(
filePaths[index],
index,
filePaths.length,
percyWebSetupInstructions,
);

return {
content: result,
};
}
247 changes: 9 additions & 238 deletions src/tools/bstack-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,254 +1,25 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { trackMCP } from "../lib/instrumentation.js";
import { getSDKPrefixCommand } from "./sdk-utils/commands.js";

import {
SDKSupportedBrowserAutomationFramework,
SDKSupportedLanguage,
SDKSupportedTestingFramework,
SDKSupportedLanguageEnum,
SDKSupportedBrowserAutomationFrameworkEnum,
SDKSupportedTestingFrameworkEnum,
} from "./sdk-utils/types.js";

import {
generateBrowserStackYMLInstructions,
getInstructionsForProjectConfiguration,
formatInstructionsWithNumbers,
} from "./sdk-utils/instructions.js";

import {
formatPercyInstructions,
getPercyInstructions,
} from "./sdk-utils/percy/instructions.js";
import { getBrowserStackAuth } from "../lib/get-auth.js";
import { BrowserStackConfig } from "../lib/types.js";
import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js";
import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js";
import { RUN_ON_BROWSERSTACK_DESCRIPTION } from "./sdk-utils/common/constants.js";

/**
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
*/
export async function bootstrapProjectWithSDK({
detectedBrowserAutomationFramework,
detectedTestingFramework,
detectedLanguage,
desiredPlatforms,
enablePercy,
config,
}: {
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework;
detectedTestingFramework: SDKSupportedTestingFramework;
detectedLanguage: SDKSupportedLanguage;
desiredPlatforms: string[];
enablePercy: boolean;
config: BrowserStackConfig;
}): Promise<CallToolResult> {
// Get credentials from config
const authString = getBrowserStackAuth(config);
const [username, accessKey] = authString.split(":");

// Handle frameworks with unique setup instructions that don't use browserstack.yml
if (
detectedBrowserAutomationFramework === "cypress" ||
detectedTestingFramework === "webdriverio"
) {
let combinedInstructions = getInstructionsForProjectConfiguration(
detectedBrowserAutomationFramework,
detectedTestingFramework,
detectedLanguage,
username,
accessKey,
);

if (enablePercy) {
const percyInstructions = getPercyInstructions(
detectedLanguage,
detectedBrowserAutomationFramework,
detectedTestingFramework,
);

if (percyInstructions) {
combinedInstructions +=
"\n\n" + formatPercyInstructions(percyInstructions);
} else {
throw new Error(
`Percy is currently not supported through MCP for ${detectedLanguage} with ${detectedTestingFramework}. If you want to run the test cases without Percy, disable Percy and run it again.`,
);
}
}

// Apply consistent formatting for all configurations
return formatFinalInstructions(combinedInstructions);
}

// Handle default flow using browserstack.yml
const sdkSetupCommand = getSDKPrefixCommand(
detectedLanguage,
detectedTestingFramework,
username,
accessKey,
);

const ymlInstructions = generateBrowserStackYMLInstructions(
desiredPlatforms,
enablePercy,
);

const instructionsForProjectConfiguration =
getInstructionsForProjectConfiguration(
detectedBrowserAutomationFramework,
detectedTestingFramework,
detectedLanguage,
username,
accessKey,
);

let combinedInstructions = "";

// Step 1: Add SDK setup command
if (sdkSetupCommand) {
combinedInstructions += sdkSetupCommand;
}

// Step 2: Add browserstack.yml setup
if (ymlInstructions) {
combinedInstructions += "\n\n---STEP---\n" + ymlInstructions;
}

// Step 3: Add language/framework-specific setup
if (instructionsForProjectConfiguration) {
combinedInstructions += "\n\n" + instructionsForProjectConfiguration;
}

// Step 4: Add Percy setup if applicable
if (enablePercy) {
const percyInstructions = getPercyInstructions(
detectedLanguage,
detectedBrowserAutomationFramework,
detectedTestingFramework,
);

if (percyInstructions) {
combinedInstructions +=
"\n\n" + formatPercyInstructions(percyInstructions);
} else {
throw new Error(
`Percy is currently not supported through MCP for ${detectedLanguage} with ${detectedTestingFramework}. If you want to run the test cases without Percy, disable Percy and run it again.`,
);
}
}

// Apply consistent formatting for all configurations
return formatFinalInstructions(combinedInstructions);
}

// Helper function to apply consistent formatting to all instruction types
function formatFinalInstructions(combinedInstructions: string): CallToolResult {
const fullInstructions = `⚠️ IMPORTANT: DO NOT SKIP ANY STEP
All the setup steps described in this file MUST be executed regardless of any existing configuration or setup.
This ensures proper BrowserStack SDK setup.

${formatInstructionsWithNumbers(combinedInstructions)}`;

return {
content: [
{
type: "text",
text: fullInstructions,
isError: false,
},
],
};
}

export default function addSDKTools(
export function registerRunBrowserStackTestsTool(
server: McpServer,
config: BrowserStackConfig,
) {
const tools: Record<string, any> = {};

tools.setupBrowserStackAutomateTests = server.tool(
"setupBrowserStackAutomateTests",
"Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use for functional or integration tests on BrowserStack, with optional Percy visual testing for supported frameworks. Example prompts: run this test on browserstack; run this test on browserstack with Percy; set up this project for browserstack with Percy. Integrate BrowserStack SDK into your project",
{
detectedBrowserAutomationFramework: z
.nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum)
.describe(
"The automation framework configured in the project. Example: 'playwright', 'selenium'",
),

detectedTestingFramework: z
.nativeEnum(SDKSupportedTestingFrameworkEnum)
.describe(
"The testing framework used in the project. Be precise with framework selection Example: 'webdriverio', 'jest', 'pytest', 'junit4', 'junit5', 'mocha'",
),

detectedLanguage: z
.nativeEnum(SDKSupportedLanguageEnum)
.describe(
"The programming language used in the project. Example: 'nodejs', 'python', 'java', 'csharp'",
),

desiredPlatforms: z
.array(z.enum(["windows", "macos", "android", "ios"]))
.describe(
"The platforms the user wants to test on. Always ask this to the user, do not try to infer this.",
),

enablePercy: z
.boolean()
.optional()
.default(false)
.describe(
"Set to true if the user wants to enable Percy for visual testing. Defaults to false.",
),
},

RUN_ON_BROWSERSTACK_DESCRIPTION,
RunTestsOnBrowserStackParamsShape,
async (args) => {
try {
trackMCP(
"runTestsOnBrowserStack",
server.server.getClientVersion()!,
undefined,
config,
);

return await bootstrapProjectWithSDK({
detectedBrowserAutomationFramework:
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework,

detectedTestingFramework:
args.detectedTestingFramework as SDKSupportedTestingFramework,

detectedLanguage: args.detectedLanguage as SDKSupportedLanguage,

desiredPlatforms: args.desiredPlatforms,
enablePercy: args.enablePercy,
config,
});
} catch (error) {
trackMCP(
"runTestsOnBrowserStack",
server.server.getClientVersion()!,
error,
config,
);

return {
content: [
{
type: "text",
text: `Failed to bootstrap project with BrowserStack SDK. Error: ${error}. Please open an issue on GitHub if the problem persists`,
isError: true,
},
],
isError: true,
};
}
return runTestsOnBrowserStackHandler(args, config);
},
);

return tools;
}

export default registerRunBrowserStackTestsTool;
39 changes: 39 additions & 0 deletions src/tools/list-test-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js";
import { testFilePathsMap } from "../lib/inmemory-store.js";
import crypto from "crypto";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

export async function addListTestFiles(args: any): Promise<CallToolResult> {
const { dirs, language, framework } = args;
let testFiles: string[] = [];

for (const dir of dirs) {
const files = await listTestFiles({
language,
framework,
baseDir: dir,
});
testFiles = testFiles.concat(files);
}

if (testFiles.length === 0) {
throw new Error("No test files found");
}

// Generate a UUID and store the test files in memory
const uuid = crypto.randomUUID();
testFilePathsMap.set(uuid, testFiles);

return {
content: [
{
type: "text",
text: `The Test files are stored in memory with id ${uuid} and the total number of tests files found is ${testFiles.length}. You can use this UUID to retrieve the tests file paths later.`,
},
{
type: "text",
text: `You can now use the tool addPercySnapshotCommands to update the test file with Percy commands for visual testing with the UUID ${uuid}`,
},
],
};
}
Loading