Skip to content

Commit cc5cb2b

Browse files
authored
Feat: Integrate MCP Instrumentation across various tools (#22)
* Feat: Integrate MCP Instrumentation tracking across various tools * Modified TC to mock trackMCPEvent * Feat: Enhance MCP event tracking * Update index.ts * Update utils.ts * Mocking tests for server * Feat: Add failure tracking for MCP events across various tools * Fix: Adding MCPFailure tracking mock * Minor Fix * Code formatting * Refactor: Consolidate MCP event tracking functions * Refactor: Improve consistency in MCP event * Refactor: Add TODO for moving traceMCP and catch block to parent function * Introduce DEV_MODE configuration to control MCP tracking behavior * Remove unnecessary logging
1 parent 185c011 commit cc5cb2b

14 files changed

+276
-97
lines changed

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ export class Config {
1111
constructor(
1212
public readonly browserstackUsername: string,
1313
public readonly browserstackAccessKey: string,
14+
public readonly DEV_MODE: boolean,
1415
) {}
1516
}
1617

1718
const config = new Config(
1819
process.env.BROWSERSTACK_USERNAME!,
1920
process.env.BROWSERSTACK_ACCESS_KEY!,
21+
process.env.DEV_MODE === "true"
2022
);
2123

2224
export default config;

src/lib/instrumentation.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import logger from "../logger";
2+
import config from "../config";
3+
import packageJson from "../../package.json";
4+
import axios from "axios";
5+
6+
interface MCPEventPayload {
7+
event_type: string;
8+
event_properties: {
9+
mcp_version: string;
10+
tool_name: string;
11+
mcp_client: string;
12+
success?: boolean;
13+
error_message?: string;
14+
error_type?: string;
15+
};
16+
}
17+
18+
export function trackMCP(
19+
toolName: string,
20+
clientInfo: { name?: string; version?: string },
21+
error?: unknown,
22+
): void {
23+
24+
if (config.DEV_MODE) {
25+
logger.info("Tracking MCP is disabled in dev mode");
26+
return;
27+
}
28+
29+
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
30+
const isSuccess = !error;
31+
const mcpClient = clientInfo?.name || "unknown";
32+
33+
// Log client information
34+
if (clientInfo?.name) {
35+
logger.info(
36+
`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`,
37+
);
38+
} else {
39+
logger.info("Client connected: unknown client");
40+
}
41+
42+
const event: MCPEventPayload = {
43+
event_type: "MCPInstrumentation",
44+
event_properties: {
45+
mcp_version: packageJson.version,
46+
tool_name: toolName,
47+
mcp_client: mcpClient,
48+
success: isSuccess,
49+
},
50+
};
51+
52+
// Add error details if applicable
53+
if (error) {
54+
event.event_properties.error_message =
55+
error instanceof Error ? error.message : String(error);
56+
event.event_properties.error_type =
57+
error instanceof Error ? error.constructor.name : "Unknown";
58+
}
59+
60+
axios
61+
.post(instrumentationEndpoint, event, {
62+
headers: {
63+
"Content-Type": "application/json",
64+
Authorization: `Basic ${Buffer.from(
65+
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
66+
).toString("base64")}`,
67+
},
68+
timeout: 2000,
69+
})
70+
.catch(() => {});
71+
}

src/tools/accessibility.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,33 @@ import {
55
startAccessibilityScan,
66
AccessibilityScanResponse,
77
} from "./accessiblity-utils/accessibility";
8+
import { trackMCP } from "../lib/instrumentation";
89

910
async function runAccessibilityScan(
1011
name: string,
1112
pageURL: string,
1213
): Promise<CallToolResult> {
13-
try {
14-
const response: AccessibilityScanResponse = await startAccessibilityScan(
15-
name,
16-
[pageURL],
17-
);
18-
const scanId = response.data?.id;
19-
const scanRunId = response.data?.scanRunId;
20-
21-
if (!scanId || !scanRunId) {
22-
throw new Error(
23-
"Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists",
24-
);
25-
}
14+
const response: AccessibilityScanResponse = await startAccessibilityScan(
15+
name,
16+
[pageURL],
17+
);
18+
const scanId = response.data?.id;
19+
const scanRunId = response.data?.scanRunId;
2620

27-
return {
28-
content: [
29-
{
30-
type: "text",
31-
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
32-
},
33-
],
34-
};
35-
} catch (error) {
36-
return {
37-
content: [
38-
{
39-
type: "text",
40-
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
41-
isError: true,
42-
},
43-
],
44-
isError: true,
45-
};
21+
if (!scanId || !scanRunId) {
22+
throw new Error(
23+
"Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists",
24+
);
4625
}
26+
27+
return {
28+
content: [
29+
{
30+
type: "text",
31+
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
32+
},
33+
],
34+
};
4735
}
4836

4937
export default function addAccessibilityTools(server: McpServer) {
@@ -55,7 +43,26 @@ export default function addAccessibilityTools(server: McpServer) {
5543
pageURL: z.string().describe("The URL to scan for accessibility issues"),
5644
},
5745
async (args) => {
58-
return runAccessibilityScan(args.name, args.pageURL);
46+
try {
47+
trackMCP("startAccessibilityScan", server.server.getClientVersion()!);
48+
return await runAccessibilityScan(args.name, args.pageURL);
49+
} catch (error) {
50+
trackMCP(
51+
"startAccessibilityScan",
52+
server.server.getClientVersion()!,
53+
error,
54+
);
55+
return {
56+
content: [
57+
{
58+
type: "text",
59+
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
60+
isError: true,
61+
},
62+
],
63+
isError: true,
64+
};
65+
}
5966
},
6067
);
6168
}

src/tools/applive.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import fs from "fs";
55
import { startSession } from "./applive-utils/start-session";
66
import logger from "../logger";
7+
import { trackMCP } from "../lib/instrumentation";
78

89
/**
910
* Launches an App Live Session on BrowserStack.
@@ -33,6 +34,7 @@ export async function startAppLiveSession(args: {
3334
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
3435
throw new Error("You must provide a valid iOS app path.");
3536
}
37+
3638
// check if the app path exists && is readable
3739
try {
3840
if (!fs.existsSync(args.appPath)) {
@@ -89,13 +91,16 @@ export default function addAppLiveTools(server: McpServer) {
8991
},
9092
async (args) => {
9193
try {
92-
return startAppLiveSession(args);
94+
trackMCP("runAppLiveSession", server.server.getClientVersion()!);
95+
return await startAppLiveSession(args);
9396
} catch (error) {
97+
logger.error("App live session failed: %s", error);
98+
trackMCP("runAppLiveSession", server.server.getClientVersion()!, error);
9499
return {
95100
content: [
96101
{
97102
type: "text",
98-
text: `Failed to start an app live session. Error: ${error}. Please open an issue on GitHub if the problem persists`,
103+
text: `Failed to start app live session: ${error instanceof Error ? error.message : String(error)}`,
99104
isError: true,
100105
},
101106
],

src/tools/automate.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import logger from "../logger";
55
import { retrieveNetworkFailures } from "../lib/api";
6+
import { trackMCP } from "../lib/instrumentation";
67

78
/**
89
* Fetches failed network requests from a BrowserStack Automate session.
@@ -33,20 +34,8 @@ export async function getNetworkFailures(args: {
3334
],
3435
};
3536
} catch (error) {
36-
const errorMessage =
37-
error instanceof Error ? error.message : "An unknown error occurred";
38-
logger.error("Failed to fetch network logs: %s", errorMessage);
39-
40-
return {
41-
content: [
42-
{
43-
type: "text",
44-
text: `Failed to fetch network logs: ${errorMessage}`,
45-
isError: true,
46-
},
47-
],
48-
isError: true,
49-
};
37+
logger.error("Failed to fetch network logs: %s", error);
38+
throw new Error(error instanceof Error ? error.message : String(error));
5039
}
5140
}
5241

@@ -57,6 +46,32 @@ export default function addAutomateTools(server: McpServer) {
5746
{
5847
sessionId: z.string().describe("The Automate session ID."),
5948
},
60-
getNetworkFailures,
49+
async (args) => {
50+
try {
51+
trackMCP("getNetworkFailures", server.server.getClientVersion()!);
52+
return await getNetworkFailures(args);
53+
} catch (error) {
54+
const errorMessage =
55+
error instanceof Error ? error.message : String(error);
56+
logger.error("Failed to fetch network logs: %s", errorMessage);
57+
58+
trackMCP(
59+
"getNetworkFailures",
60+
server.server.getClientVersion()!,
61+
error,
62+
);
63+
64+
return {
65+
content: [
66+
{
67+
type: "text",
68+
text: `Failed to fetch network logs: ${errorMessage}`,
69+
isError: true,
70+
},
71+
],
72+
isError: true,
73+
};
74+
}
75+
},
6176
);
6277
}

src/tools/bstack-sdk.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
generateBrowserStackYMLInstructions,
1111
getInstructionsForProjectConfiguration,
1212
} from "./sdk-utils/instructions";
13+
import { trackMCP } from "../lib/instrumentation";
1314

1415
/**
1516
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
@@ -72,21 +73,23 @@ export default function addSDKTools(server: McpServer) {
7273
),
7374
},
7475
async (args) => {
75-
const detectedBrowserAutomationFramework =
76-
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework;
77-
const detectedTestingFramework =
78-
args.detectedTestingFramework as SDKSupportedTestingFramework;
79-
const detectedLanguage = args.detectedLanguage as SDKSupportedLanguage;
80-
const desiredPlatforms = args.desiredPlatforms;
81-
8276
try {
83-
return bootstrapProjectWithSDK({
84-
detectedBrowserAutomationFramework,
85-
detectedTestingFramework,
86-
detectedLanguage,
87-
desiredPlatforms,
77+
trackMCP("runTestsOnBrowserStack", server.server.getClientVersion()!);
78+
79+
return await bootstrapProjectWithSDK({
80+
detectedBrowserAutomationFramework:
81+
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework,
82+
detectedTestingFramework:
83+
args.detectedTestingFramework as SDKSupportedTestingFramework,
84+
detectedLanguage: args.detectedLanguage as SDKSupportedLanguage,
85+
desiredPlatforms: args.desiredPlatforms,
8886
});
8987
} catch (error) {
88+
trackMCP(
89+
"runTestsOnBrowserStack",
90+
server.server.getClientVersion()!,
91+
error,
92+
);
9093
return {
9194
content: [
9295
{

src/tools/live.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import logger from "../logger";
55
import { startBrowserSession } from "./live-utils/start-session";
66
import { PlatformType } from "./live-utils/types";
7+
import { trackMCP } from "../lib/instrumentation";
78

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

84-
try {
85-
// Branch desktop vs mobile and delegate
86-
const launchUrl =
87-
args.platformType === PlatformType.DESKTOP
88-
? await launchDesktopSession(args)
89-
: await launchMobileSession(args);
85+
// Branch desktop vs mobile and delegate
86+
const launchUrl =
87+
args.platformType === PlatformType.DESKTOP
88+
? await launchDesktopSession(args)
89+
: await launchMobileSession(args);
9090

91-
return {
92-
content: [
93-
{
94-
type: "text" as const,
95-
text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
96-
},
97-
],
98-
};
99-
} catch (err: any) {
100-
logger.error("Live session failed: %s", err);
101-
return {
102-
content: [
103-
{
104-
type: "text" as const,
105-
text: `❌ Failed to start session: ${err.message || err}`,
106-
isError: true,
107-
},
108-
],
109-
isError: true,
110-
};
111-
}
91+
return {
92+
content: [
93+
{
94+
type: "text" as const,
95+
text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
96+
},
97+
],
98+
};
11299
}
113100

114101
export default function addBrowserLiveTools(server: McpServer) {
@@ -118,9 +105,15 @@ export default function addBrowserLiveTools(server: McpServer) {
118105
LiveArgsShape,
119106
async (args) => {
120107
try {
121-
const result = await runBrowserSession(args);
122-
return result;
123-
} catch (error: any) {
108+
trackMCP("runBrowserLiveSession", server.server.getClientVersion()!);
109+
return await runBrowserSession(args);
110+
} catch (error) {
111+
logger.error("Live session failed: %s", error);
112+
trackMCP(
113+
"runBrowserLiveSession",
114+
server.server.getClientVersion()!,
115+
error,
116+
);
124117
return {
125118
content: [
126119
{

0 commit comments

Comments
 (0)