Skip to content

Commit 73c7030

Browse files
authored
Network Log Retrieval for BrowserStack Automate Sessions (#4)
* Add automate tool for fetching and storing network logs from BrowserStack sessions. * Refactor network logs handling: update directory paths and error handling in API and automate tools * Refactor network logs handling: simplify return value and error handling * Focus on retrieving only failure logs, update related types and tests accordingly. * Initial Contribution Guidelines * Update contributing.md * Update contributing.md * Update contributing guidelines and enhance MCP Inspector documentation with some code refactoring * Remove unnecessary lines from contributing.md to streamline the document * Add contributing guidelines and link in README.md
1 parent 6187c1c commit 73c7030

File tree

8 files changed

+324
-0
lines changed

8 files changed

+324
-0
lines changed

CONTRIBUTING.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# 🚀 Contributing to the Browserstack MCP Server
2+
3+
This guide will help you set up your environment and contribute effectively to the MCP (Model Context Protocol) Server.
4+
5+
## ✅ Prerequisites
6+
7+
Make sure you have the following installed:
8+
9+
- 🟢 [Node.js](https://nodejs.org/) (Recommended: LTS v22.15.0)
10+
- 🤖 GitHub Copilot (for VS Code or Cursor)
11+
- 🧠 Optionally, [Claude desktop app](https://www.anthropic.com/index/claude-desktop) for additional AI assistance
12+
13+
## 🛠 Getting Started
14+
15+
1. **Clone the repository:**
16+
17+
```bash
18+
git clone https://github.com/browserstack/mcp-server.git
19+
cd mcp-server
20+
```
21+
22+
2. **Build the project:**
23+
24+
```bash
25+
npm run build
26+
```
27+
28+
This compiles the TypeScript source code and generates `dist/index.js`.
29+
30+
3. **Configure MCP for your editor:**
31+
32+
### 💻 VS Code: `.vscode/mcp.json`
33+
34+
```json
35+
{
36+
"servers": {
37+
"browserstack": {
38+
"command": "node",
39+
"args": ["FULL PATH TO dist/index.js"],
40+
"env": {
41+
"BROWSERSTACK_USERNAME": "",
42+
"BROWSERSTACK_ACCESS_KEY": ""
43+
}
44+
}
45+
}
46+
}
47+
```
48+
49+
### 🖱 Cursor: `.cursor/mcp.json`
50+
51+
```json
52+
{
53+
"mcpServers": {
54+
"browserstack": {
55+
"command": "node",
56+
"args": ["FULL PATH TO dist/index.js"],
57+
"env": {
58+
"BROWSERSTACK_USERNAME": "",
59+
"BROWSERSTACK_ACCESS_KEY": ""
60+
}
61+
}
62+
}
63+
}
64+
```
65+
66+
### 🔨 Quick Start from VS Code or Cursor
67+
68+
When you open your `.vscode/mcp.json` or `.cursor/mcp.json` file,
69+
you'll see a **"play" icon** (Start ▶️) next to the server configuration.
70+
**Click it to instantly start your MCP server!**
71+
72+
73+
## 🧪 How to Test with MCP Inspector
74+
75+
**MCP Inspector** is a lightweight tool for launching, testing, and validating MCP server implementations easily.
76+
77+
### 🔹 Run with Config
78+
79+
If you've configured `.cursor/mcp.json` or `.vscode/mcp.json`, you can start testing by running:
80+
81+
```bash
82+
npx @modelcontextprotocol/inspector --config /PATH_TO_CONFIG/.cursor/mcp.json --server browserstack
83+
```
84+
85+
This will spin up your MCP server and open the Inspector at:
86+
[http://127.0.0.1:6274](http://127.0.0.1:6274)
87+
88+
<div align="center">
89+
<img src="assets/mcp-inspector.png" alt="MCP Inspector UI" height="300">
90+
div>
91+
92+
Inside the Inspector:
93+
94+
- View and manage your server connection (restart, disconnect, etc.)
95+
- Validate your server credentials and environment variables
96+
- Access available tools under the **"Middle Tab"**, and run tests to see results in the **Right Panel**
97+
- Review past interactions easily via the **History Panel**
98+
99+
Additionally, for every MCP server session, a log file is automatically generated at:
100+
`~/Library/Logs/Claude/` — you can check detailed logs there if needed.
101+
102+
---
103+
104+
## ✨ Next Steps
105+
106+
🌀 Fork the repository to your GitHub account
107+
108+
🧩 Add tests to verify your contributions
109+
110+
🤖 Explore and interact with the server using Copilot, Cursor, or Claude
111+
112+
📬 Raise a pull request from your fork once you're ready!

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Use the following prompts to run/debug/fix your **automated tests** on BrowserSt
167167
## 📝 Contributing
168168

169169
We welcome contributions! Please open an issue to discuss any changes you'd like to make.
170+
👉 [**Click here to view our Contributing Guidelines**](https://github.com/browserstack/mcp-server/blob/main/CONTRIBUTING.md)
170171

171172
## 📞 Support
172173

assets/mcp-inspector.png

411 KB
Loading

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import addAppLiveTools from "./tools/applive";
1010
import addObservabilityTools from "./tools/observability";
1111
import addBrowserLiveTools from "./tools/live";
1212
import addAccessibilityTools from "./tools/accessibility";
13+
import addAutomateTools from "./tools/automate";
1314

1415
function registerTools(server: McpServer) {
1516
addSDKTools(server);
1617
addAppLiveTools(server);
1718
addBrowserLiveTools(server);
1819
addObservabilityTools(server);
1920
addAccessibilityTools(server);
21+
addAutomateTools(server);
2022
}
2123

2224
// Create an MCP server

src/lib/api.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import config from "../config";
2+
import { HarEntry, HarFile } from "./utils";
23

34
export async function getLatestO11YBuildInfo(
45
buildName: string,
@@ -27,3 +28,62 @@ export async function getLatestO11YBuildInfo(
2728

2829
return buildsResponse.json();
2930
}
31+
32+
// Fetches network logs for a given session ID and returns only failure logs
33+
export async function retrieveNetworkFailures(sessionId: string): Promise<any> {
34+
if (!sessionId) {
35+
throw new Error("Session ID is required");
36+
}
37+
const url = `https://api.browserstack.com/automate/sessions/${sessionId}/networklogs`;
38+
const auth = Buffer.from(
39+
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
40+
).toString("base64");
41+
42+
const response = await fetch(url, {
43+
method: "GET",
44+
headers: {
45+
"Content-Type": "application/json",
46+
Authorization: `Basic ${auth}`,
47+
},
48+
});
49+
50+
if (!response.ok) {
51+
if (response.status === 404) {
52+
throw new Error("Invalid session ID");
53+
}
54+
throw new Error(`Failed to fetch network logs: ${response.statusText}`);
55+
}
56+
57+
const networklogs: HarFile = await response.json();
58+
59+
// Filter for failure logs
60+
const failureEntries: HarEntry[] = networklogs.log.entries.filter(
61+
(entry: HarEntry) => {
62+
return (
63+
entry.response.status === 0 ||
64+
entry.response.status >= 400 ||
65+
entry.response._error !== undefined
66+
);
67+
},
68+
);
69+
70+
// Return only the failure entries with some context
71+
return {
72+
failures: failureEntries.map((entry: any) => ({
73+
startedDateTime: entry.startedDateTime,
74+
request: {
75+
method: entry.request?.method,
76+
url: entry.request?.url,
77+
queryString: entry.request?.queryString,
78+
},
79+
response: {
80+
status: entry.response?.status,
81+
statusText: entry.response?.statusText,
82+
_error: entry.response?._error,
83+
},
84+
serverIPAddress: entry.serverIPAddress,
85+
time: entry.time,
86+
})),
87+
totalFailures: failureEntries.length,
88+
};
89+
}

src/lib/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,25 @@ export function sanitizeUrlParam(param: string): string {
22
// Remove any characters that could be used for command injection
33
return param.replace(/[;&|`$(){}[\]<>]/g, "");
44
}
5+
6+
export interface HarFile {
7+
log: {
8+
entries: HarEntry[];
9+
};
10+
}
11+
12+
export interface HarEntry {
13+
startedDateTime: string;
14+
request: {
15+
method: string;
16+
url: string;
17+
queryString?: { name: string; value: string }[];
18+
};
19+
response: {
20+
status: number;
21+
statusText?: string;
22+
_error?: string;
23+
};
24+
serverIPAddress?: string;
25+
time?: number;
26+
}

src/tools/automate.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import logger from "../logger";
5+
import { retrieveNetworkFailures } from "../lib/api";
6+
7+
/**
8+
* Fetches failed network requests from a BrowserStack Automate session.
9+
* Returns network requests that resulted in errors or failed to complete.
10+
*/
11+
export async function getNetworkFailures(args: {
12+
sessionId: string;
13+
}): Promise<CallToolResult> {
14+
try {
15+
const failureLogs = await retrieveNetworkFailures(args.sessionId);
16+
logger.info(
17+
"Successfully fetched failure network logs for session: %s",
18+
args.sessionId,
19+
);
20+
21+
// Check if there are any failures
22+
const hasFailures = failureLogs.totalFailures > 0;
23+
const text = hasFailures
24+
? `${failureLogs.totalFailures} network failure(s) found for session :\n\n${JSON.stringify(failureLogs.failures, null, 2)}`
25+
: `No network failures found for session`;
26+
27+
return {
28+
content: [
29+
{
30+
type: "text",
31+
text,
32+
},
33+
],
34+
};
35+
} 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+
};
50+
}
51+
}
52+
53+
export default function addAutomateTools(server: McpServer) {
54+
server.tool(
55+
"getNetworkFailures",
56+
"Use this tool to fetch failed network requests from a BrowserStack Automate session.",
57+
{
58+
sessionId: z.string().describe("The Automate session ID."),
59+
},
60+
getNetworkFailures,
61+
);
62+
}

tests/tools/automate.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { getNetworkFailures } from '../../src/tools/automate';
2+
import { retrieveNetworkFailures } from '../../src/lib/api';
3+
4+
jest.mock('../../src/lib/api', () => ({
5+
retrieveNetworkFailures: jest.fn()
6+
}));
7+
jest.mock('../../src/logger', () => ({
8+
error: jest.fn(),
9+
info: jest.fn()
10+
}));
11+
12+
describe('getNetworkFailures', () => {
13+
const validSessionId = 'valid-session-123';
14+
const mockFailures = {
15+
failures: [
16+
{
17+
startedDateTime: '2024-01-01T00:00:00Z',
18+
request: { method: 'GET', url: 'https://example.com' },
19+
response: { status: 404, statusText: 'Not Found' },
20+
serverIPAddress: '1.2.3.4',
21+
time: 123
22+
}
23+
],
24+
totalFailures: 1
25+
};
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
(retrieveNetworkFailures as jest.Mock).mockResolvedValue(mockFailures);
30+
});
31+
32+
it('should return failure logs when present', async () => {
33+
const result = await getNetworkFailures({ sessionId: validSessionId });
34+
expect(retrieveNetworkFailures).toHaveBeenCalledWith(validSessionId);
35+
expect(result.content[0].text).toContain('network failure(s) found for session');
36+
expect(result.content[0].text).toContain('"status": 404');
37+
expect(result.isError).toBeFalsy();
38+
});
39+
40+
it('should return message when no failure logs are found', async () => {
41+
(retrieveNetworkFailures as jest.Mock).mockResolvedValue({ failures: [], totalFailures: 0 });
42+
const result = await getNetworkFailures({ sessionId: validSessionId });
43+
expect(retrieveNetworkFailures).toHaveBeenCalledWith(validSessionId);
44+
expect(result.content[0].text).toContain('No network failures found for sessio');
45+
expect(result.isError).toBeFalsy();
46+
});
47+
48+
it('should handle errors from the API', async () => {
49+
(retrieveNetworkFailures as jest.Mock).mockRejectedValue(new Error('Invalid session ID'));
50+
const result = await getNetworkFailures({ sessionId: 'invalid-id' });
51+
expect(retrieveNetworkFailures).toHaveBeenCalledWith('invalid-id');
52+
expect(result.content[0].text).toBe('Failed to fetch network logs: Invalid session ID');
53+
expect(result.content[0].isError).toBe(true);
54+
expect(result.isError).toBe(true);
55+
});
56+
57+
it('should handle empty session ID', async () => {
58+
(retrieveNetworkFailures as jest.Mock).mockRejectedValue(new Error('Session ID is required'));
59+
const result = await getNetworkFailures({ sessionId: '' });
60+
expect(retrieveNetworkFailures).toHaveBeenCalledWith('');
61+
expect(result.content[0].text).toBe('Failed to fetch network logs: Session ID is required');
62+
expect(result.content[0].isError).toBe(true);
63+
expect(result.isError).toBe(true);
64+
});
65+
});

0 commit comments

Comments
 (0)