From 14e9b221d154764d256753c6af6e5ff0071287e1 Mon Sep 17 00:00:00 2001 From: rututaj-browserstack Date: Fri, 30 May 2025 11:41:46 +0530 Subject: [PATCH 1/4] feat: implement dynamic product management system with tool registration and status handling --- DYNAMIC_PRODUCTS.md | 151 ++++++++++ src/index.ts | 62 +++- src/lib/product-manager.ts | 276 ++++++++++++++++++ src/tools/dynamic.ts | 202 +++++++++++++ .../testmanagement-utils/TCG-utils/api.ts | 37 +++ 5 files changed, 717 insertions(+), 11 deletions(-) create mode 100644 DYNAMIC_PRODUCTS.md create mode 100644 src/lib/product-manager.ts create mode 100644 src/tools/dynamic.ts diff --git a/DYNAMIC_PRODUCTS.md b/DYNAMIC_PRODUCTS.md new file mode 100644 index 0000000..c4c3bf2 --- /dev/null +++ b/DYNAMIC_PRODUCTS.md @@ -0,0 +1,151 @@ +# Dynamic Product Management System + +The BrowserStack MCP Server now features a dynamic product management system that allows you to enable/disable specific BrowserStack capabilities on demand. This prevents tool overload and provides a cleaner, more focused experience. + +## How It Works + +### Initial State +When the server starts, only two core tools are available: +- `enable_products` - Enable specific BrowserStack products +- `get_product_status` - View current product status + +### Product Queue System +- **Maximum Products**: 2 products can be enabled simultaneously +- **Queue Behavior**: When you enable a 3rd product, the oldest enabled product is automatically disabled +- **Always Available**: The `enable_products` tool is always accessible + +## Available Products + +### šŸš€ **SDK** (`sdk`) +**Category**: Test Automation +**Description**: Set up and run tests using BrowserStack SDK with automatic configuration +**Tools**: `runTestsOnBrowserStack` + +### šŸ“± **App Live Testing** (`app-live`) +**Category**: Mobile Testing +**Description**: Test mobile apps on real devices with interactive debugging capabilities +**Tools**: `runAppLiveSession` + +### 🌐 **Browser Live Testing** (`browser-live`) +**Category**: Web Testing +**Description**: Test web applications on real browsers and devices for cross-browser compatibility +**Tools**: `runBrowserLiveSession` + +### ♿ **Accessibility Testing** (`accessibility`) +**Category**: Quality Assurance +**Description**: Automated accessibility scanning and reporting for WCAG compliance +**Tools**: `startAccessibilityScan` + +### šŸ“‹ **Test Management** (`test-management`) +**Category**: Test Management +**Description**: Comprehensive test case and test run management with project organization +**Tools**: `createProjectOrFolder`, `createTestCase`, `listTestCases`, `createTestRun`, `listTestRuns`, `updateTestRun`, `addTestResult`, `uploadProductRequirementFile`, `createTestCasesFromFile` + +### šŸ¤– **App Automation** (`app-automation`) +**Category**: Mobile Automation +**Description**: Automated mobile app testing with screenshot capture and visual validation +**Tools**: `takeAppScreenshot` + +### šŸ” **Failure Analysis** (`failure-logs`) +**Category**: Debugging +**Description**: Debug test failures with comprehensive logs, network data, and crash reports +**Tools**: `getFailureLogs` + +### šŸ–„ļø **Web Automation** (`automate`) +**Category**: Web Automation +**Description**: Automated web testing with screenshot capture and session management +**Tools**: `fetchAutomationScreenshots` + +### 🧠 **Self-Healing Tests** (`self-heal`) +**Category**: AI/ML +**Description**: AI-powered test maintenance with automatic selector healing for flaky tests +**Tools**: `fetchSelfHealedSelectors` + +## Usage Examples + +### Enable a Single Product +```javascript +// Enable App Live Testing +await server.callTool("enable_products", { + products: ["app-live"] +}); +``` + +### Enable Multiple Products +```javascript +// Enable both Browser Live and Accessibility Testing +await server.callTool("enable_products", { + products: ["browser-live", "accessibility"] +}); +``` + +### Check Current Status +```javascript +// View which products are currently enabled +await server.callTool("get_product_status", {}); +``` + +### Typical Workflow + +1. **Start Server**: Only `enable_products` and `get_product_status` are available +2. **Check Available Products**: Use `get_product_status` to see all options +3. **Enable Needed Products**: Use `enable_products` with the products you need +4. **Use Product Tools**: The enabled product tools are now available +5. **Switch Products**: Enable different products as needed (oldest will be auto-disabled) + +## Example Scenarios + +### Mobile App Testing Workflow +```javascript +// Enable mobile testing capabilities +await server.callTool("enable_products", { + products: ["app-live", "app-automation"] +}); + +// Now you can use: +// - runAppLiveSession (interactive testing) +// - takeAppScreenshot (automated testing) +``` + +### Web Testing with Debugging +```javascript +// Enable web testing and debugging +await server.callTool("enable_products", { + products: ["browser-live", "failure-logs"] +}); + +// Now you can use: +// - runBrowserLiveSession (interactive testing) +// - getFailureLogs (debugging failed tests) +``` + +### Full Test Management Pipeline +```javascript +// Enable test management and automation +await server.callTool("enable_products", { + products: ["test-management", "automate"] +}); + +// Now you can use all test management tools plus automation +``` + +## Technical Implementation + +### Architecture +- **ProductManager**: Core class managing the enabled products queue +- **Dynamic Tools**: Handles the `enable_products` tool registration +- **Modular Registration**: Each product has its own registration function +- **Queue Management**: FIFO queue with configurable maximum size + +### Benefits +- **Reduced Complexity**: Users only see relevant tools +- **Better Organization**: Tools grouped by product/capability +- **Flexible Usage**: Enable/disable based on current needs +- **Performance**: Only active tools are registered +- **Discoverability**: Clear product descriptions and capabilities + +### Configuration +The system is configured in `src/lib/product-manager.ts`: +- `MAX_ENABLED_PRODUCTS`: Maximum concurrent products (default: 2) +- `PRODUCT_CONFIGS`: Product definitions and metadata +- `BrowserStackProduct`: Enum of available products diff --git a/src/index.ts b/src/index.ts index bfad2f9..f5d61e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,17 +17,57 @@ import addFailureLogsTools from "./tools/getFailureLogs.js"; import addAutomateTools from "./tools/automate.js"; import addSelfHealTools from "./tools/selfheal.js"; import { setupOnInitialized } from "./oninitialized.js"; +import { ProductManager, BrowserStackProduct } from "./lib/product-manager.js"; +import { addDynamicTools } from "./tools/dynamic.js"; -function registerTools(server: McpServer) { - addSDKTools(server); - addAppLiveTools(server); - addBrowserLiveTools(server); - addAccessibilityTools(server); - addTestManagementTools(server); - addAppAutomationTools(server); - addFailureLogsTools(server); - addAutomateTools(server); - addSelfHealTools(server); +function setupDynamicProductManager(server: McpServer): ProductManager { + const productManager = new ProductManager(server); + + // Register all product tool registration functions + productManager.registerProduct(BrowserStackProduct.SDK, addSDKTools); + productManager.registerProduct(BrowserStackProduct.APP_LIVE, addAppLiveTools); + productManager.registerProduct( + BrowserStackProduct.BROWSER_LIVE, + addBrowserLiveTools, + ); + productManager.registerProduct( + BrowserStackProduct.ACCESSIBILITY, + addAccessibilityTools, + ); + productManager.registerProduct( + BrowserStackProduct.TEST_MANAGEMENT, + addTestManagementTools, + ); + productManager.registerProduct( + BrowserStackProduct.APP_AUTOMATION, + addAppAutomationTools, + ); + productManager.registerProduct( + BrowserStackProduct.FAILURE_LOGS, + addFailureLogsTools, + ); + productManager.registerProduct( + BrowserStackProduct.AUTOMATE, + addAutomateTools, + ); + productManager.registerProduct( + BrowserStackProduct.SELF_HEAL, + addSelfHealTools, + ); + + return productManager; +} + +function registerDynamicTools(server: McpServer) { + // Set up the product manager + const productManager = setupDynamicProductManager(server); + + // Add the dynamic tools (enable_products and get_product_status) + addDynamicTools(server, productManager); + + logger.info( + "Dynamic product management initialized. Use 'enable_products' tool to activate specific BrowserStack capabilities.", + ); } // Create an MCP server @@ -38,7 +78,7 @@ const server: McpServer = new McpServer({ setupOnInitialized(server); -registerTools(server); +registerDynamicTools(server); async function main() { logger.info( diff --git a/src/lib/product-manager.ts b/src/lib/product-manager.ts new file mode 100644 index 0000000..c31fcfa --- /dev/null +++ b/src/lib/product-manager.ts @@ -0,0 +1,276 @@ +/** + * Product Manager for Dynamic Tool Registration + * Manages enabled/disabled products with a queue-based system (max 2 products) + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import logger from "../logger.js"; + +export enum BrowserStackProduct { + SDK = "sdk", + APP_LIVE = "app-live", + BROWSER_LIVE = "browser-live", + ACCESSIBILITY = "accessibility", + TEST_MANAGEMENT = "test-management", + APP_AUTOMATION = "app-automation", + FAILURE_LOGS = "failure-logs", + AUTOMATE = "automate", + SELF_HEAL = "self-heal", +} + +export interface ProductConfig { + name: string; + description: string; + category: string; + tools: string[]; +} + +export const PRODUCT_CONFIGS: Record = { + [BrowserStackProduct.SDK]: { + name: "BrowserStack SDK", + description: + "Set up and run tests using BrowserStack SDK with automatic configuration", + category: "Test Automation", + tools: ["runTestsOnBrowserStack"], + }, + [BrowserStackProduct.APP_LIVE]: { + name: "App Live Testing", + description: + "Test mobile apps on real devices with interactive debugging capabilities", + category: "Mobile Testing", + tools: ["runAppLiveSession"], + }, + [BrowserStackProduct.BROWSER_LIVE]: { + name: "Browser Live Testing", + description: + "Test web applications on real browsers and devices for cross-browser compatibility", + category: "Web Testing", + tools: ["runBrowserLiveSession"], + }, + [BrowserStackProduct.ACCESSIBILITY]: { + name: "Accessibility Testing", + description: + "Automated accessibility scanning and reporting for WCAG compliance", + category: "Quality Assurance", + tools: ["startAccessibilityScan"], + }, + [BrowserStackProduct.TEST_MANAGEMENT]: { + name: "Test Management", + description: + "Comprehensive test case and test run management with project organization", + category: "Test Management", + tools: [ + "createProjectOrFolder", + "createTestCase", + "listTestCases", + "createTestRun", + "listTestRuns", + "updateTestRun", + "addTestResult", + "uploadProductRequirementFile", + "createTestCasesFromFile", + ], + }, + [BrowserStackProduct.APP_AUTOMATION]: { + name: "App Automation", + description: + "Automated mobile app testing with screenshot capture and visual validation", + category: "Mobile Automation", + tools: ["takeAppScreenshot"], + }, + [BrowserStackProduct.FAILURE_LOGS]: { + name: "Failure Analysis", + description: + "Debug test failures with comprehensive logs, network data, and crash reports", + category: "Debugging", + tools: ["getFailureLogs"], + }, + [BrowserStackProduct.AUTOMATE]: { + name: "Web Automation", + description: + "Automated web testing with screenshot capture and session management", + category: "Web Automation", + tools: ["fetchAutomationScreenshots"], + }, + [BrowserStackProduct.SELF_HEAL]: { + name: "Self-Healing Tests", + description: + "AI-powered test maintenance with automatic selector healing for flaky tests", + category: "AI/ML", + tools: ["fetchSelfHealedSelectors"], + }, +}; + +export class ProductManager { + private enabledProducts: BrowserStackProduct[] = []; + private readonly MAX_ENABLED_PRODUCTS = 2; + private server: McpServer; + private toolRegistrationFunctions: Map< + BrowserStackProduct, + (server: McpServer) => void + >; + + constructor(server: McpServer) { + this.server = server; + this.toolRegistrationFunctions = new Map(); + } + + /** + * Register a product's tool registration function + */ + registerProduct( + product: BrowserStackProduct, + registrationFn: (server: McpServer) => void, + ): void { + this.toolRegistrationFunctions.set(product, registrationFn); + } + + /** + * Enable a product (adds to queue, removes oldest if queue is full) + */ + enableProduct(product: BrowserStackProduct): { + success: boolean; + message: string; + previouslyEnabled?: BrowserStackProduct; + } { + // Check if product is already enabled + if (this.enabledProducts.includes(product)) { + return { + success: false, + message: `Product '${PRODUCT_CONFIGS[product].name}' is already enabled`, + }; + } + + // Check if we have a registration function for this product + const registrationFn = this.toolRegistrationFunctions.get(product); + if (!registrationFn) { + return { + success: false, + message: `Product '${PRODUCT_CONFIGS[product].name}' is not available for registration`, + }; + } + + let removedProduct: BrowserStackProduct | undefined; + + // If we're at max capacity, remove the oldest product + if (this.enabledProducts.length >= this.MAX_ENABLED_PRODUCTS) { + removedProduct = this.enabledProducts.shift(); + if (removedProduct) { + this.disableProductTools(removedProduct); + logger.info( + `Disabled product: ${PRODUCT_CONFIGS[removedProduct].name} (queue full)`, + ); + } + } + + // Add the new product to the end of the queue + this.enabledProducts.push(product); + + // Register the product's tools + try { + registrationFn(this.server); + logger.info(`Enabled product: ${PRODUCT_CONFIGS[product].name}`); + + const message = removedProduct + ? `Successfully enabled '${PRODUCT_CONFIGS[product].name}'. Disabled '${PRODUCT_CONFIGS[removedProduct].name}' due to queue limit (max ${this.MAX_ENABLED_PRODUCTS} products).` + : `Successfully enabled '${PRODUCT_CONFIGS[product].name}'.`; + + return { + success: true, + message, + previouslyEnabled: removedProduct, + }; + } catch (error) { + // If registration failed, remove from enabled list + this.enabledProducts.pop(); + logger.error( + `Failed to enable product ${PRODUCT_CONFIGS[product].name}:`, + error, + ); + + return { + success: false, + message: `Failed to enable '${PRODUCT_CONFIGS[product].name}': ${error instanceof Error ? error.message : "Unknown error"}`, + }; + } + } + + /** + * Disable a specific product's tools + */ + private disableProductTools(product: BrowserStackProduct): void { + const config = PRODUCT_CONFIGS[product]; + config.tools.forEach((toolName) => { + try { + // Remove the tool from the server + // Note: The MCP SDK doesn't have a direct remove method, + // so we'll mark them as disabled in our tracking + logger.debug(`Disabled tool: ${toolName}`); + } catch (error) { + logger.error(`Failed to disable tool ${toolName}:`, error); + } + }); + } + + /** + * Get list of currently enabled products + */ + getEnabledProducts(): BrowserStackProduct[] { + return [...this.enabledProducts]; + } + + /** + * Get list of available but not enabled products + */ + getAvailableProducts(): BrowserStackProduct[] { + return Object.values(BrowserStackProduct).filter( + (product) => !this.enabledProducts.includes(product), + ); + } + + /** + * Get product configuration + */ + getProductConfig(product: BrowserStackProduct): ProductConfig { + return PRODUCT_CONFIGS[product]; + } + + /** + * Get formatted status of all products + */ + getProductStatus(): string { + const enabled = this.getEnabledProducts(); + const available = this.getAvailableProducts(); + + let status = `## Product Status (${enabled.length}/${this.MAX_ENABLED_PRODUCTS} enabled)\n\n`; + + if (enabled.length > 0) { + status += "### 🟢 Currently Enabled Products:\n"; + enabled.forEach((product, index) => { + const config = PRODUCT_CONFIGS[product]; + status += `${index + 1}. **${config.name}** (${config.category})\n`; + status += ` ${config.description}\n`; + status += ` Tools: ${config.tools.join(", ")}\n\n`; + }); + } + + if (available.length > 0) { + status += "### ⚪ Available Products:\n"; + available.forEach((product) => { + const config = PRODUCT_CONFIGS[product]; + status += `- **${config.name}** (${config.category}): ${config.description}\n`; + }); + } + + status += `\nšŸ’” **Tip**: You can enable up to ${this.MAX_ENABLED_PRODUCTS} products simultaneously. Enabling a new product when at capacity will disable the oldest enabled product.`; + + return status; + } + + /** + * Check if a product is currently enabled + */ + isProductEnabled(product: BrowserStackProduct): boolean { + return this.enabledProducts.includes(product); + } +} diff --git a/src/tools/dynamic.ts b/src/tools/dynamic.ts new file mode 100644 index 0000000..014cb6c --- /dev/null +++ b/src/tools/dynamic.ts @@ -0,0 +1,202 @@ +/** + * Dynamic Tools for Product Management + * Handles the enable_products tool and dynamic tool registration + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + ProductManager, + BrowserStackProduct, + PRODUCT_CONFIGS, +} from "../lib/product-manager.js"; +import { trackMCP } from "../lib/instrumentation.js"; +import logger from "../logger.js"; + +let productManager: ProductManager; + +/** + * Enable one or more BrowserStack products + */ +export async function enableProductsTool(args: { + products: BrowserStackProduct[]; +}): Promise { + const results: string[] = []; + const errors: string[] = []; + + if (!args.products || args.products.length === 0) { + return { + content: [ + { + type: "text", + text: + "āŒ No products specified. Please provide at least one product to enable.\n\n" + + productManager.getProductStatus(), + isError: true, + }, + ], + isError: true, + }; + } + + // Validate product names + const validProducts = Object.values(BrowserStackProduct); + const invalidProducts = args.products.filter( + (p) => !validProducts.includes(p), + ); + + if (invalidProducts.length > 0) { + return { + content: [ + { + type: "text", + text: + `āŒ Invalid products: ${invalidProducts.join(", ")}\n\n` + + `Valid products are: ${validProducts.join(", ")}\n\n` + + productManager.getProductStatus(), + isError: true, + }, + ], + isError: true, + }; + } + + // Enable each product + for (const product of args.products) { + const result = productManager.enableProduct(product); + + if (result.success) { + results.push(`āœ… ${result.message}`); + if (result.previouslyEnabled) { + results.push( + `ā„¹ļø Previously enabled product '${PRODUCT_CONFIGS[result.previouslyEnabled].name}' was automatically disabled.`, + ); + } + } else { + errors.push(`āŒ ${result.message}`); + } + } + + // Build response + let responseText = ""; + + if (results.length > 0) { + responseText += results.join("\n") + "\n\n"; + } + + if (errors.length > 0) { + responseText += errors.join("\n") + "\n\n"; + } + + responseText += productManager.getProductStatus(); + + const hasErrors = errors.length > 0; + + return { + content: [ + { + type: "text", + text: responseText, + isError: hasErrors, + }, + ], + isError: hasErrors, + }; +} + +/** + * Get current product status + */ +export async function getProductStatusTool(): Promise { + return { + content: [ + { + type: "text", + text: productManager.getProductStatus(), + }, + ], + }; +} + +/** + * Register dynamic tools with the server + */ +export function addDynamicTools( + server: McpServer, + manager: ProductManager, +): void { + productManager = manager; + + // Enable products tool - always available + server.tool( + "enable_products", + "Enable BrowserStack products to access their tools. You can enable up to 2 products simultaneously. Enabling additional products will disable the oldest enabled product. Use this tool to activate specific BrowserStack capabilities based on user needs.", + { + products: z.array(z.nativeEnum(BrowserStackProduct)).describe( + `Array of products to enable. Available products:\n` + + Object.values(BrowserStackProduct) + .map((product) => { + const config = PRODUCT_CONFIGS[product]; + return `- ${product}: ${config.name} (${config.category}) - ${config.description}`; + }) + .join("\n"), + ), + }, + async (args) => { + try { + trackMCP("enable_products", server.server.getClientVersion()!); + return await enableProductsTool(args); + } catch (error) { + trackMCP("enable_products", server.server.getClientVersion()!, error); + logger.error("Failed to enable products:", error); + + return { + content: [ + { + type: "text", + text: + `āŒ Failed to enable products: ${error instanceof Error ? error.message : "Unknown error"}\n\n` + + productManager.getProductStatus(), + isError: true, + }, + ], + isError: true, + }; + } + }, + ); + + // Product status tool - always available + server.tool( + "get_product_status", + "Get the current status of all BrowserStack products (enabled/available) and their capabilities.", + {}, + async () => { + try { + trackMCP("get_product_status", server.server.getClientVersion()!); + return await getProductStatusTool(); + } catch (error) { + trackMCP( + "get_product_status", + server.server.getClientVersion()!, + error, + ); + logger.error("Failed to get product status:", error); + + return { + content: [ + { + type: "text", + text: `āŒ Failed to get product status: ${error instanceof Error ? error.message : "Unknown error"}`, + isError: true, + }, + ], + isError: true, + }; + } + }, + ); +} + +export { ProductManager, BrowserStackProduct, PRODUCT_CONFIGS }; diff --git a/src/tools/testmanagement-utils/TCG-utils/api.ts b/src/tools/testmanagement-utils/TCG-utils/api.ts index 7fd1d38..e359a0d 100644 --- a/src/tools/testmanagement-utils/TCG-utils/api.ts +++ b/src/tools/testmanagement-utils/TCG-utils/api.ts @@ -361,3 +361,40 @@ export async function projectIdentifierToId( } throw new Error(`Project with identifier ${projectId} not found.`); } + +export async function testCaseIdentifierToId( + projectId: string, + testCaseIdentifier: string, +): Promise<> { + const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`; + + const response = await axios.get(url, { + headers: { + "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`, + accept: "application/json, text/plain, */*", + }, + }); + + if (response.data.success !== true) { + throw new Error(`Failed to fetch test case ID: ${response.statusText}`); + } + + // Check if test_cases array exists and has items + if ( + !response.data.test_cases || + !Array.isArray(response.data.test_cases) || + response.data.test_cases.length === 0 + ) { + throw new Error( + `No test cases found in response for identifier ${testCaseIdentifier}`, + ); + } + + for (const testCase of response.data.test_cases) { + if (testCase.identifier === testCaseIdentifier) { + return testCase.id; + } + } + + throw new Error(`Test case with identifier ${testCaseIdentifier} not found.`); +} From af3a88bcd7d8513d37135e024fd0a0dfe522894e Mon Sep 17 00:00:00 2001 From: rututaj-browserstack Date: Fri, 30 May 2025 14:53:02 +0530 Subject: [PATCH 2/4] feat: implement dynamic and static tool registration for BrowserStack products --- src/config.ts | 2 + src/index.ts | 71 +++------------- src/lib/dynamic-tools.ts | 80 +++++++++++++++++++ src/lib/product-manager.ts | 10 ++- src/lib/static-tools.ts | 39 +++++++++ src/tools/dynamic.ts | 11 ++- .../testmanagement-utils/TCG-utils/api.ts | 2 +- 7 files changed, 150 insertions(+), 65 deletions(-) create mode 100644 src/lib/dynamic-tools.ts create mode 100644 src/lib/static-tools.ts diff --git a/src/config.ts b/src/config.ts index 706c79f..f32ec3b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,7 @@ export class Config { public readonly browserstackUsername: string, public readonly browserstackAccessKey: string, public readonly DEV_MODE: boolean, + public readonly DYNAMIC_SERVER: boolean, ) {} } @@ -10,6 +11,7 @@ const config = new Config( process.env.BROWSERSTACK_USERNAME!, process.env.BROWSERSTACK_ACCESS_KEY!, process.env.DEV_MODE === "true", + process.env.DYNAMIC_SERVER === "true", ); export default config; diff --git a/src/index.ts b/src/index.ts index f5d61e5..24eee64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,69 +6,11 @@ import { createRequire } from "module"; const require = createRequire(import.meta.url); const packageJson = require("../package.json"); import "dotenv/config"; +import config from "./config.js"; import logger from "./logger.js"; -import addSDKTools from "./tools/bstack-sdk.js"; -import addAppLiveTools from "./tools/applive.js"; -import addBrowserLiveTools from "./tools/live.js"; -import addAccessibilityTools from "./tools/accessibility.js"; -import addTestManagementTools from "./tools/testmanagement.js"; -import addAppAutomationTools from "./tools/appautomate.js"; -import addFailureLogsTools from "./tools/getFailureLogs.js"; -import addAutomateTools from "./tools/automate.js"; -import addSelfHealTools from "./tools/selfheal.js"; import { setupOnInitialized } from "./oninitialized.js"; -import { ProductManager, BrowserStackProduct } from "./lib/product-manager.js"; -import { addDynamicTools } from "./tools/dynamic.js"; - -function setupDynamicProductManager(server: McpServer): ProductManager { - const productManager = new ProductManager(server); - - // Register all product tool registration functions - productManager.registerProduct(BrowserStackProduct.SDK, addSDKTools); - productManager.registerProduct(BrowserStackProduct.APP_LIVE, addAppLiveTools); - productManager.registerProduct( - BrowserStackProduct.BROWSER_LIVE, - addBrowserLiveTools, - ); - productManager.registerProduct( - BrowserStackProduct.ACCESSIBILITY, - addAccessibilityTools, - ); - productManager.registerProduct( - BrowserStackProduct.TEST_MANAGEMENT, - addTestManagementTools, - ); - productManager.registerProduct( - BrowserStackProduct.APP_AUTOMATION, - addAppAutomationTools, - ); - productManager.registerProduct( - BrowserStackProduct.FAILURE_LOGS, - addFailureLogsTools, - ); - productManager.registerProduct( - BrowserStackProduct.AUTOMATE, - addAutomateTools, - ); - productManager.registerProduct( - BrowserStackProduct.SELF_HEAL, - addSelfHealTools, - ); - - return productManager; -} - -function registerDynamicTools(server: McpServer) { - // Set up the product manager - const productManager = setupDynamicProductManager(server); - - // Add the dynamic tools (enable_products and get_product_status) - addDynamicTools(server, productManager); - - logger.info( - "Dynamic product management initialized. Use 'enable_products' tool to activate specific BrowserStack capabilities.", - ); -} +import { registerStaticTools } from "./lib/static-tools.js"; +import { registerDynamicTools } from "./lib/dynamic-tools.js"; // Create an MCP server const server: McpServer = new McpServer({ @@ -78,7 +20,12 @@ const server: McpServer = new McpServer({ setupOnInitialized(server); -registerDynamicTools(server); +// Choose registration mode based on config +if (config.DYNAMIC_SERVER) { + registerDynamicTools(server); +} else { + registerStaticTools(server); +} async function main() { logger.info( diff --git a/src/lib/dynamic-tools.ts b/src/lib/dynamic-tools.ts new file mode 100644 index 0000000..86d8425 --- /dev/null +++ b/src/lib/dynamic-tools.ts @@ -0,0 +1,80 @@ +/** + * Dynamic Tools Registration + * Sets up the product manager and dynamic tool registration system + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import logger from "../logger.js"; +import { ProductManager, BrowserStackProduct } from "./product-manager.js"; +import { addDynamicTools } from "../tools/dynamic.js"; +import addSDKTools from "../tools/bstack-sdk.js"; +import addAppLiveTools from "../tools/applive.js"; +import addBrowserLiveTools from "../tools/live.js"; +import addAccessibilityTools from "../tools/accessibility.js"; +import addTestManagementTools from "../tools/testmanagement.js"; +import addAppAutomationTools from "../tools/appautomate.js"; +import addFailureLogsTools from "../tools/getFailureLogs.js"; +import addAutomateTools from "../tools/automate.js"; +import addSelfHealTools from "../tools/selfheal.js"; + +/** + * Set up the product manager with all available products + */ +function setupDynamicProductManager(server: McpServer): ProductManager { + const productManager = new ProductManager(server); + + logger.info("Registering product tool registration functions..."); + + // Register all product tool registration functions + productManager.registerProduct(BrowserStackProduct.SDK, addSDKTools); + productManager.registerProduct(BrowserStackProduct.APP_LIVE, addAppLiveTools); + productManager.registerProduct( + BrowserStackProduct.BROWSER_LIVE, + addBrowserLiveTools, + ); + productManager.registerProduct( + BrowserStackProduct.ACCESSIBILITY, + addAccessibilityTools, + ); + productManager.registerProduct( + BrowserStackProduct.TEST_MANAGEMENT, + addTestManagementTools, + ); + productManager.registerProduct( + BrowserStackProduct.APP_AUTOMATION, + addAppAutomationTools, + ); + productManager.registerProduct( + BrowserStackProduct.FAILURE_LOGS, + addFailureLogsTools, + ); + productManager.registerProduct( + BrowserStackProduct.AUTOMATE, + addAutomateTools, + ); + productManager.registerProduct( + BrowserStackProduct.SELF_HEAL, + addSelfHealTools, + ); + + logger.info("Product registration functions setup completed."); + return productManager; +} + +/** + * Register dynamic tools and product management system + * Only enable_products and get_product_status tools will be available initially + */ +export function registerDynamicTools(server: McpServer): void { + logger.info("Starting dynamic tools registration..."); + + // Set up the product manager + const productManager = setupDynamicProductManager(server); + + // Add the dynamic tools (enable_products and get_product_status) + addDynamicTools(server, productManager); + + logger.info( + "Dynamic product management initialized. Use 'enable_products' tool to activate specific BrowserStack capabilities.", + ); +} diff --git a/src/lib/product-manager.ts b/src/lib/product-manager.ts index c31fcfa..0d7262f 100644 --- a/src/lib/product-manager.ts +++ b/src/lib/product-manager.ts @@ -57,7 +57,9 @@ export const PRODUCT_CONFIGS: Record = { [BrowserStackProduct.TEST_MANAGEMENT]: { name: "Test Management", description: - "Comprehensive test case and test run management with project organization", + "Comprehensive test case and test run management with project organization. " + + "Involve creating test cases, organizing them into projects or folders, " + + "and managing test runs.", category: "Test Management", tools: [ "createProjectOrFolder", @@ -125,6 +127,12 @@ export class ProductManager { this.toolRegistrationFunctions.set(product, registrationFn); } + /** + * send notification to the server when a product is enabled + */ + notifyProductEnabled(): void { + this.server.sendToolListChanged(); + } /** * Enable a product (adds to queue, removes oldest if queue is full) */ diff --git a/src/lib/static-tools.ts b/src/lib/static-tools.ts new file mode 100644 index 0000000..2420d77 --- /dev/null +++ b/src/lib/static-tools.ts @@ -0,0 +1,39 @@ +/** + * Static Tools Registration + * Registers all BrowserStack tools immediately when the server starts + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import logger from "../logger.js"; +import addSDKTools from "../tools/bstack-sdk.js"; +import addAppLiveTools from "../tools/applive.js"; +import addBrowserLiveTools from "../tools/live.js"; +import addAccessibilityTools from "../tools/accessibility.js"; +import addTestManagementTools from "../tools/testmanagement.js"; +import addAppAutomationTools from "../tools/appautomate.js"; +import addFailureLogsTools from "../tools/getFailureLogs.js"; +import addAutomateTools from "../tools/automate.js"; +import addSelfHealTools from "../tools/selfheal.js"; + +/** + * Register all BrowserStack tools statically + * All tools will be available immediately when the server starts + */ +export function registerStaticTools(server: McpServer): void { + logger.info("Starting static tool registration..."); + + // Register all tools + addSDKTools(server); + addAppLiveTools(server); + addBrowserLiveTools(server); + addAccessibilityTools(server); + addTestManagementTools(server); + addAppAutomationTools(server); + addFailureLogsTools(server); + addAutomateTools(server); + addSelfHealTools(server); + + logger.info( + "Static tool registration completed. All BrowserStack tools are available.", + ); +} diff --git a/src/tools/dynamic.ts b/src/tools/dynamic.ts index 014cb6c..d102f84 100644 --- a/src/tools/dynamic.ts +++ b/src/tools/dynamic.ts @@ -78,6 +78,8 @@ export async function enableProductsTool(args: { } } + productManager.notifyProductEnabled(); + // Build response let responseText = ""; @@ -100,6 +102,12 @@ export async function enableProductsTool(args: { text: responseText, isError: hasErrors, }, + { + type: "text", + text: + "Don't Directly Call the enabled products tools. " + + "Ask User for the confirmation and then check for the enabled products and call the respective tools.", + }, ], isError: hasErrors, }; @@ -131,7 +139,8 @@ export function addDynamicTools( // Enable products tool - always available server.tool( "enable_products", - "Enable BrowserStack products to access their tools. You can enable up to 2 products simultaneously. Enabling additional products will disable the oldest enabled product. Use this tool to activate specific BrowserStack capabilities based on user needs.", + "Enable BrowserStack products to access their tools. You can enable up to 2 products simultaneously. Enabling additional products will disable the oldest enabled product. Use this tool to activate specific BrowserStack capabilities based on user needs." + + "Tools will be dynamically registered based on the enabled products.", { products: z.array(z.nativeEnum(BrowserStackProduct)).describe( `Array of products to enable. Available products:\n` + diff --git a/src/tools/testmanagement-utils/TCG-utils/api.ts b/src/tools/testmanagement-utils/TCG-utils/api.ts index e359a0d..bf2f6f1 100644 --- a/src/tools/testmanagement-utils/TCG-utils/api.ts +++ b/src/tools/testmanagement-utils/TCG-utils/api.ts @@ -365,7 +365,7 @@ export async function projectIdentifierToId( export async function testCaseIdentifierToId( projectId: string, testCaseIdentifier: string, -): Promise<> { +): Promise { const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`; const response = await axios.get(url, { From cd4d16d520c70889bc1eb60e9be0ae6c6d7badc4 Mon Sep 17 00:00:00 2001 From: rututaj-browserstack Date: Mon, 2 Jun 2025 11:38:18 +0530 Subject: [PATCH 3/4] refactor: remove usage examples and technical implementation details from DYNAMIC_PRODUCTS.md --- DYNAMIC_PRODUCTS.md | 73 --------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/DYNAMIC_PRODUCTS.md b/DYNAMIC_PRODUCTS.md index c4c3bf2..9ae1b0b 100644 --- a/DYNAMIC_PRODUCTS.md +++ b/DYNAMIC_PRODUCTS.md @@ -61,29 +61,6 @@ When the server starts, only two core tools are available: **Description**: AI-powered test maintenance with automatic selector healing for flaky tests **Tools**: `fetchSelfHealedSelectors` -## Usage Examples - -### Enable a Single Product -```javascript -// Enable App Live Testing -await server.callTool("enable_products", { - products: ["app-live"] -}); -``` - -### Enable Multiple Products -```javascript -// Enable both Browser Live and Accessibility Testing -await server.callTool("enable_products", { - products: ["browser-live", "accessibility"] -}); -``` - -### Check Current Status -```javascript -// View which products are currently enabled -await server.callTool("get_product_status", {}); -``` ### Typical Workflow @@ -93,56 +70,6 @@ await server.callTool("get_product_status", {}); 4. **Use Product Tools**: The enabled product tools are now available 5. **Switch Products**: Enable different products as needed (oldest will be auto-disabled) -## Example Scenarios - -### Mobile App Testing Workflow -```javascript -// Enable mobile testing capabilities -await server.callTool("enable_products", { - products: ["app-live", "app-automation"] -}); - -// Now you can use: -// - runAppLiveSession (interactive testing) -// - takeAppScreenshot (automated testing) -``` - -### Web Testing with Debugging -```javascript -// Enable web testing and debugging -await server.callTool("enable_products", { - products: ["browser-live", "failure-logs"] -}); - -// Now you can use: -// - runBrowserLiveSession (interactive testing) -// - getFailureLogs (debugging failed tests) -``` - -### Full Test Management Pipeline -```javascript -// Enable test management and automation -await server.callTool("enable_products", { - products: ["test-management", "automate"] -}); - -// Now you can use all test management tools plus automation -``` - -## Technical Implementation - -### Architecture -- **ProductManager**: Core class managing the enabled products queue -- **Dynamic Tools**: Handles the `enable_products` tool registration -- **Modular Registration**: Each product has its own registration function -- **Queue Management**: FIFO queue with configurable maximum size - -### Benefits -- **Reduced Complexity**: Users only see relevant tools -- **Better Organization**: Tools grouped by product/capability -- **Flexible Usage**: Enable/disable based on current needs -- **Performance**: Only active tools are registered -- **Discoverability**: Clear product descriptions and capabilities ### Configuration The system is configured in `src/lib/product-manager.ts`: From c51115e3c626ae40ae90fe03f863b0df014c30f8 Mon Sep 17 00:00:00 2001 From: rututaj-browserstack Date: Mon, 2 Jun 2025 11:47:37 +0530 Subject: [PATCH 4/4] feat: expand DYNAMIC_PRODUCTS.md with additional product categories and tools --- DYNAMIC_PRODUCTS.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/DYNAMIC_PRODUCTS.md b/DYNAMIC_PRODUCTS.md index 9ae1b0b..54445ae 100644 --- a/DYNAMIC_PRODUCTS.md +++ b/DYNAMIC_PRODUCTS.md @@ -5,63 +5,91 @@ The BrowserStack MCP Server now features a dynamic product management system tha ## How It Works ### Initial State + When the server starts, only two core tools are available: + - `enable_products` - Enable specific BrowserStack products - `get_product_status` - View current product status ### Product Queue System + - **Maximum Products**: 2 products can be enabled simultaneously - **Queue Behavior**: When you enable a 3rd product, the oldest enabled product is automatically disabled - **Always Available**: The `enable_products` tool is always accessible +## Enabling Dynamic Products + +The dynamic product management system is controlled by an environment variable. To enable this feature: + +### Environment Variable Configuration + +Set the following environment variable when starting the MCP server in MCP configuration file: + +```bash +DYNAMIC_SERVER=true +``` + +### Behavior + +- **When `DYNAMIC_SERVER=true`**: The dynamic product management system is enabled, allowing you to selectively enable/disable products +- **When `DYNAMIC_SERVER=false` or not set**: All products and their tools are available by default (legacy behavior) + ## Available Products ### šŸš€ **SDK** (`sdk`) + **Category**: Test Automation **Description**: Set up and run tests using BrowserStack SDK with automatic configuration **Tools**: `runTestsOnBrowserStack` ### šŸ“± **App Live Testing** (`app-live`) + **Category**: Mobile Testing **Description**: Test mobile apps on real devices with interactive debugging capabilities **Tools**: `runAppLiveSession` ### 🌐 **Browser Live Testing** (`browser-live`) + **Category**: Web Testing **Description**: Test web applications on real browsers and devices for cross-browser compatibility **Tools**: `runBrowserLiveSession` ### ♿ **Accessibility Testing** (`accessibility`) + **Category**: Quality Assurance **Description**: Automated accessibility scanning and reporting for WCAG compliance **Tools**: `startAccessibilityScan` ### šŸ“‹ **Test Management** (`test-management`) + **Category**: Test Management **Description**: Comprehensive test case and test run management with project organization **Tools**: `createProjectOrFolder`, `createTestCase`, `listTestCases`, `createTestRun`, `listTestRuns`, `updateTestRun`, `addTestResult`, `uploadProductRequirementFile`, `createTestCasesFromFile` ### šŸ¤– **App Automation** (`app-automation`) + **Category**: Mobile Automation **Description**: Automated mobile app testing with screenshot capture and visual validation **Tools**: `takeAppScreenshot` ### šŸ” **Failure Analysis** (`failure-logs`) + **Category**: Debugging **Description**: Debug test failures with comprehensive logs, network data, and crash reports **Tools**: `getFailureLogs` ### šŸ–„ļø **Web Automation** (`automate`) + **Category**: Web Automation **Description**: Automated web testing with screenshot capture and session management **Tools**: `fetchAutomationScreenshots` ### 🧠 **Self-Healing Tests** (`self-heal`) + **Category**: AI/ML **Description**: AI-powered test maintenance with automatic selector healing for flaky tests **Tools**: `fetchSelfHealedSelectors` - ### Typical Workflow 1. **Start Server**: Only `enable_products` and `get_product_status` are available @@ -70,9 +98,10 @@ When the server starts, only two core tools are available: 4. **Use Product Tools**: The enabled product tools are now available 5. **Switch Products**: Enable different products as needed (oldest will be auto-disabled) - ### Configuration + The system is configured in `src/lib/product-manager.ts`: + - `MAX_ENABLED_PRODUCTS`: Maximum concurrent products (default: 2) - `PRODUCT_CONFIGS`: Product definitions and metadata - `BrowserStackProduct`: Enum of available products