Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "@browserstack/mcp-server",
"version": "1.2.3",
"version": "1.2.4",
"description": "BrowserStack's Official MCP Server",
"mcpName": "io.github.browserstack/mcp-server",
"main": "dist/index.js",
"repository": {
"type": "git",
Expand Down
27 changes: 27 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import sharp from "sharp";
import type { ApiResponse } from "./apiClient.js";
import { BrowserStackConfig } from "./types.js";
import { getBrowserStackAuth } from "./get-auth.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { trackMCP } from "../index.js";

export function sanitizeUrlParam(param: string): string {
// Remove any characters that could be used for command injection
Expand Down Expand Up @@ -62,3 +65,27 @@ export async function fetchFromBrowserStackAPI(

return res.json();
}

function errorContent(message: string): CallToolResult {
return {
content: [{ type: "text", text: message }],
isError: true,
};
}

export function handleMCPError(
toolName: string,
server: McpServer,
config: BrowserStackConfig,
error: unknown,
) {
trackMCP(toolName, server.server.getClientVersion()!, error, config);

const errorMessage = error instanceof Error ? error.message : "Unknown error";

const readableToolName = toolName.replace(/([A-Z])/g, " $1").toLowerCase();

return errorContent(
`Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`,
);
}
11 changes: 2 additions & 9 deletions src/tools/appautomate-utils/appium-sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,8 @@ export interface AppSDKInstruction {
export const SUPPORTED_CONFIGURATIONS = {
appium: {
ruby: ["cucumberRuby"],
java: [
"junit5",
"junit4",
"testng",
"cucumberTestng",
"selenide",
"jbehave",
],
csharp: ["nunit", "xunit", "mstest", "specflow", "reqnroll"],
java: [],
csharp: [],
python: ["pytest", "robot", "behave", "lettuce"],
nodejs: ["jest", "mocha", "cucumberJs", "webdriverio", "nightwatch"],
},
Expand Down
10 changes: 9 additions & 1 deletion src/tools/appautomate-utils/appium-sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ export function validateSupportforAppAutomate(
);
}

const testingFrameworks = SUPPORTED_CONFIGURATIONS[framework][language];
const testingFrameworks = SUPPORTED_CONFIGURATIONS[framework][
language
] as string[];

if (testingFrameworks.length === 0) {
throw new Error(
`No testing frameworks are supported for language '${language}' and framework '${framework}'.`,
);
}
if (!testingFrameworks.includes(testingFramework)) {
throw new Error(
`Unsupported testing framework '${testingFramework}' for language '${language}' and framework '${framework}'. Supported testing frameworks: ${testingFrameworks.join(", ")}`,
Expand Down
13 changes: 12 additions & 1 deletion src/tools/bstack-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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";
import { handleMCPError } from "../lib/utils.js";
import { trackMCP } from "../lib/instrumentation.js";

export function registerRunBrowserStackTestsTool(
server: McpServer,
Expand All @@ -15,7 +17,16 @@ export function registerRunBrowserStackTestsTool(
RUN_ON_BROWSERSTACK_DESCRIPTION,
RunTestsOnBrowserStackParamsShape,
async (args) => {
return runTestsOnBrowserStackHandler(args, config);
try {
trackMCP(
"runTestsOnBrowserStack",
server.server.getClientVersion()!,
config,
);
return await runTestsOnBrowserStackHandler(args, config);
} catch (error) {
return handleMCPError("runTestsOnBrowserStack", server, config, error);
}
},
);

Expand Down
19 changes: 8 additions & 11 deletions src/tools/build-insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import logger from "../logger.js";
import { BrowserStackConfig } from "../lib/types.js";
import { fetchFromBrowserStackAPI } from "../lib/utils.js";
import { fetchFromBrowserStackAPI, handleMCPError } from "../lib/utils.js";
import { trackMCP } from "../lib/instrumentation.js";

// Tool function that fetches build insights from two APIs
export async function fetchBuildInsightsTool(
Expand Down Expand Up @@ -78,18 +79,14 @@ export default function addBuildInsightsTools(
},
async (args) => {
try {
trackMCP(
"fetchBuildInsights",
server.server.getClientVersion()!,
config,
);
return await fetchBuildInsightsTool(args, config);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error during fetching build insights: ${errorMessage}`,
},
],
};
return handleMCPError("fetchBuildInsights", server, config, error);
}
},
);
Expand Down
6 changes: 6 additions & 0 deletions src/tools/list-test-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export async function addListTestFiles(args: any): Promise<CallToolResult> {
const { dirs, language, framework } = args;
let testFiles: string[] = [];

if (!dirs || dirs.length === 0) {
throw new Error(
"No directories provided to add the test files. Please provide test directories to add percy snapshot commands.",
);
}

for (const dir of dirs) {
const files = await listTestFiles({
language,
Expand Down
89 changes: 39 additions & 50 deletions src/tools/percy-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { trackMCP } from "../index.js";
import { BrowserStackConfig } from "../lib/types.js";
import { fetchPercyChanges } from "./percy-change.js";
import { fetchPercyChanges } from "./review-agent.js";
import { addListTestFiles } from "./list-test-files.js";
import { runPercyScan } from "./run-percy-scan.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
Expand All @@ -25,14 +25,14 @@ import {
FetchPercyChangesParamsShape,
ManagePercyBuildApprovalParamsShape,
} from "./sdk-utils/common/schema.js";
import { handleMCPError } from "../lib/utils.js";

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

// Register setupPercyVisualTesting
tools.setupPercyVisualTesting = server.tool(
"setupPercyVisualTesting",
SETUP_PERCY_DESCRIPTION,
Expand All @@ -46,26 +46,11 @@ export function registerPercyTools(
);
return setUpPercyHandler(args, config);
} catch (error) {
trackMCP(
"setupPercyVisualTesting",
server.server.getClientVersion()!,
error,
config,
);
return {
content: [
{
type: "text",
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
return handleMCPError("setupPercyVisualTesting", server, config, error);
}
},
);

// Register addPercySnapshotCommands
tools.addPercySnapshotCommands = server.tool(
"addPercySnapshotCommands",
PERCY_SNAPSHOT_COMMANDS_DESCRIPTION,
Expand All @@ -79,26 +64,16 @@ export function registerPercyTools(
);
return await updateTestsWithPercyCommands(args);
} catch (error) {
trackMCP(
return handleMCPError(
"addPercySnapshotCommands",
server.server.getClientVersion()!,
error,
server,
config,
error,
);
return {
content: [
{
type: "text",
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
}
},
);

// Register listTestFiles
tools.listTestFiles = server.tool(
"listTestFiles",
LIST_TEST_FILES_DESCRIPTION,
Expand All @@ -108,21 +83,7 @@ export function registerPercyTools(
trackMCP("listTestFiles", server.server.getClientVersion()!, config);
return addListTestFiles(args);
} catch (error) {
trackMCP(
"listTestFiles",
server.server.getClientVersion()!,
error,
config,
);
return {
content: [
{
type: "text",
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
return handleMCPError("listTestFiles", server, config, error);
}
},
);
Expand All @@ -132,16 +93,30 @@ export function registerPercyTools(
"Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool",
RunPercyScanParamsShape,
async (args) => {
return runPercyScan(args, config);
try {
trackMCP("runPercyScan", server.server.getClientVersion()!, config);
return runPercyScan(args, config);
} catch (error) {
return handleMCPError("runPercyScan", server, config, error);
}
},
);

tools.fetchPercyChanges = server.tool(
"fetchPercyChanges",
"Retrieves and summarizes all visual changes detected by Percy between the latest and previous builds, helping quickly review what has changed in your project.",
"Retrieves and summarizes all visual changes detected by Percy AI between the latest and previous builds, helping quickly review what has changed in your project.",
FetchPercyChangesParamsShape,
async (args) => {
return await fetchPercyChanges(args, config);
try {
trackMCP(
"fetchPercyChanges",
server.server.getClientVersion()!,
config,
);
return await fetchPercyChanges(args, config);
} catch (error) {
return handleMCPError("fetchPercyChanges", server, config, error);
}
},
);

Expand All @@ -150,7 +125,21 @@ export function registerPercyTools(
"Approve or reject a Percy build",
ManagePercyBuildApprovalParamsShape,
async (args) => {
return await approveOrDeclinePercyBuild(args, config);
try {
trackMCP(
"managePercyBuildApproval",
server.server.getClientVersion()!,
config,
);
return await approveOrDeclinePercyBuild(args, config);
} catch (error) {
return handleMCPError(
"managePercyBuildApproval",
server,
config,
error,
);
}
},
);

Expand Down
Loading