Skip to content

Feat: Integrate MCP Instrumentation across various tools #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from 14 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
81 changes: 81 additions & 0 deletions src/lib/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import logger from "../logger";
import config from "../config";
import packageJson from "../../package.json";
import axios from "axios";

interface MCPEventPayload {
event_type: string;
event_properties: {
mcp_version: string;
tool_name: string;
mcp_client: string;
success?: boolean;
error_message?: string;
error_type?: string;
};
}

export function trackMCP(
toolName: string,
clientInfo: { name?: string; version?: string },
error?: unknown,
): void {
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
const isSuccess = !error;
const mcpClient = clientInfo?.name || "unknown";

// Log client information
if (clientInfo?.name) {
logger.info(
`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`,
);
} else {
logger.info("Client connected: unknown client");
}

const event: MCPEventPayload = {
event_type: "MCPInstrumentation",
event_properties: {
mcp_version: packageJson.version,
tool_name: toolName,
mcp_client: mcpClient,
success: isSuccess,
},
};

// Add error details if applicable
if (error) {
event.event_properties.error_message =
error instanceof Error ? error.message : String(error);
event.event_properties.error_type =
error instanceof Error ? error.constructor.name : "Unknown";
}

axios
.post(instrumentationEndpoint, event, {
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${Buffer.from(
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
).toString("base64")}`,
},
timeout: 2000,
})
.then((response) => {
logger.info(
`MCP ${isSuccess ? "event" : "failure event"} tracked successfully`,
{
toolName,
response,
},
);
})
.catch((error: unknown) => {
logger.warn(
`Failed to track MCP ${isSuccess ? "event" : "failure event"}: ${error instanceof Error ? error.message : String(error)}`,
{
toolName,
},
);
});
}
73 changes: 40 additions & 33 deletions src/tools/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,33 @@ import {
startAccessibilityScan,
AccessibilityScanResponse,
} from "./accessiblity-utils/accessibility";
import { trackMCP } from "../lib/instrumentation";

async function runAccessibilityScan(
name: string,
pageURL: string,
): Promise<CallToolResult> {
try {
const response: AccessibilityScanResponse = await startAccessibilityScan(
name,
[pageURL],
);
const scanId = response.data?.id;
const scanRunId = response.data?.scanRunId;

if (!scanId || !scanRunId) {
throw new Error(
"Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists",
);
}
const response: AccessibilityScanResponse = await startAccessibilityScan(
name,
[pageURL],
);
const scanId = response.data?.id;
const scanRunId = response.data?.scanRunId;

return {
content: [
{
type: "text",
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
isError: true,
},
],
isError: true,
};
if (!scanId || !scanRunId) {
throw new Error(
"Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists",
);
}

return {
content: [
{
type: "text",
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
},
],
};
}

export default function addAccessibilityTools(server: McpServer) {
Expand All @@ -55,7 +43,26 @@ export default function addAccessibilityTools(server: McpServer) {
pageURL: z.string().describe("The URL to scan for accessibility issues"),
},
async (args) => {
return runAccessibilityScan(args.name, args.pageURL);
try {
trackMCP("startAccessibilityScan", server.server.getClientVersion()!);
return await runAccessibilityScan(args.name, args.pageURL);
} catch (error) {
trackMCP(
"startAccessibilityScan",
server.server.getClientVersion()!,
error,
);
return {
content: [
{
type: "text",
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
isError: true,
},
],
isError: true,
};
}
},
);
}
9 changes: 7 additions & 2 deletions src/tools/applive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import { startSession } from "./applive-utils/start-session";
import logger from "../logger";
import { trackMCP } from "../lib/instrumentation";

/**
* Launches an App Live Session on BrowserStack.
Expand Down Expand Up @@ -33,6 +34,7 @@ export async function startAppLiveSession(args: {
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
throw new Error("You must provide a valid iOS app path.");
}

// check if the app path exists && is readable
try {
if (!fs.existsSync(args.appPath)) {
Expand Down Expand Up @@ -89,13 +91,16 @@ export default function addAppLiveTools(server: McpServer) {
},
async (args) => {
try {
return startAppLiveSession(args);
trackMCP("runAppLiveSession", server.server.getClientVersion()!);
return await startAppLiveSession(args);
} catch (error) {
logger.error("App live session failed: %s", error);
trackMCP("runAppLiveSession", server.server.getClientVersion()!, error);
return {
content: [
{
type: "text",
text: `Failed to start an app live session. Error: ${error}. Please open an issue on GitHub if the problem persists`,
text: `Failed to start app live session: ${error instanceof Error ? error.message : String(error)}`,
isError: true,
},
],
Expand Down
45 changes: 30 additions & 15 deletions src/tools/automate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import logger from "../logger";
import { retrieveNetworkFailures } from "../lib/api";
import { trackMCP } from "../lib/instrumentation";

/**
* Fetches failed network requests from a BrowserStack Automate session.
Expand Down Expand Up @@ -33,20 +34,8 @@ export async function getNetworkFailures(args: {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred";
logger.error("Failed to fetch network logs: %s", errorMessage);

return {
content: [
{
type: "text",
text: `Failed to fetch network logs: ${errorMessage}`,
isError: true,
},
],
isError: true,
};
logger.error("Failed to fetch network logs: %s", error);
throw new Error(error instanceof Error ? error.message : String(error));
}
}

Expand All @@ -57,6 +46,32 @@ export default function addAutomateTools(server: McpServer) {
{
sessionId: z.string().describe("The Automate session ID."),
},
getNetworkFailures,
async (args) => {
try {
trackMCP("getNetworkFailures", server.server.getClientVersion()!);
return await getNetworkFailures(args);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error("Failed to fetch network logs: %s", errorMessage);

trackMCP(
"getNetworkFailures",
server.server.getClientVersion()!,
error,
);

return {
content: [
{
type: "text",
text: `Failed to fetch network logs: ${errorMessage}`,
isError: true,
},
],
isError: true,
};
}
},
);
}
27 changes: 15 additions & 12 deletions src/tools/bstack-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
generateBrowserStackYMLInstructions,
getInstructionsForProjectConfiguration,
} from "./sdk-utils/instructions";
import { trackMCP } from "../lib/instrumentation";

/**
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
Expand Down Expand Up @@ -72,21 +73,23 @@ export default function addSDKTools(server: McpServer) {
),
},
async (args) => {
const detectedBrowserAutomationFramework =
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework;
const detectedTestingFramework =
args.detectedTestingFramework as SDKSupportedTestingFramework;
const detectedLanguage = args.detectedLanguage as SDKSupportedLanguage;
const desiredPlatforms = args.desiredPlatforms;

try {
return bootstrapProjectWithSDK({
detectedBrowserAutomationFramework,
detectedTestingFramework,
detectedLanguage,
desiredPlatforms,
trackMCP("runTestsOnBrowserStack", server.server.getClientVersion()!);

return await bootstrapProjectWithSDK({
detectedBrowserAutomationFramework:
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework,
detectedTestingFramework:
args.detectedTestingFramework as SDKSupportedTestingFramework,
detectedLanguage: args.detectedLanguage as SDKSupportedLanguage,
desiredPlatforms: args.desiredPlatforms,
});
} catch (error) {
trackMCP(
"runTestsOnBrowserStack",
server.server.getClientVersion()!,
error,
);
return {
content: [
{
Expand Down
53 changes: 23 additions & 30 deletions src/tools/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from "zod";
import logger from "../logger";
import { startBrowserSession } from "./live-utils/start-session";
import { PlatformType } from "./live-utils/types";
import { trackMCP } from "../lib/instrumentation";

// Define the schema shape
const LiveArgsShape = {
Expand Down Expand Up @@ -81,34 +82,20 @@ async function runBrowserSession(rawArgs: any) {
// Validate and narrow
const args = LiveArgsSchema.parse(rawArgs);

try {
// Branch desktop vs mobile and delegate
const launchUrl =
args.platformType === PlatformType.DESKTOP
? await launchDesktopSession(args)
: await launchMobileSession(args);
// Branch desktop vs mobile and delegate
const launchUrl =
args.platformType === PlatformType.DESKTOP
? await launchDesktopSession(args)
: await launchMobileSession(args);

return {
content: [
{
type: "text" as const,
text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
},
],
};
} catch (err: any) {
logger.error("Live session failed: %s", err);
return {
content: [
{
type: "text" as const,
text: `❌ Failed to start session: ${err.message || err}`,
isError: true,
},
],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
},
],
};
}

export default function addBrowserLiveTools(server: McpServer) {
Expand All @@ -118,9 +105,15 @@ export default function addBrowserLiveTools(server: McpServer) {
LiveArgsShape,
async (args) => {
try {
const result = await runBrowserSession(args);
return result;
} catch (error: any) {
trackMCP("runBrowserLiveSession", server.server.getClientVersion()!);
return await runBrowserSession(args);
} catch (error) {
logger.error("Live session failed: %s", error);
trackMCP(
"runBrowserLiveSession",
server.server.getClientVersion()!,
error,
);
return {
content: [
{
Expand Down
Loading