Skip to content

Feat : Local MCP library #95

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b4da63a
Refactor authentication handling for BrowserStack API calls
ruturaj-browserstack Jul 1, 2025
897e888
Refactor test management utility functions to consistently include se…
ruturaj-browserstack Jul 1, 2025
788ac8c
Refactor local tunnel preparation to remove username and password par…
ruturaj-browserstack Jul 1, 2025
3062f7b
Refactor server-factory to remove unused import of addAppLiveTools
ruturaj-browserstack Jul 1, 2025
c693d6c
Refactor UUID generation to use randomUUID from node:crypto
ruturaj-browserstack Jul 1, 2025
047048d
Refactor AccessibilityScanner to remove local URL handling and associ…
ruturaj-browserstack Jul 1, 2025
7599979
Refactor Config and AccessibilityScanner to remove username and acces…
ruturaj-browserstack Jul 1, 2025
6815d92
Refactor start-session to comment out openBrowser function and its in…
ruturaj-browserstack Jul 1, 2025
e5c9c8a
Merge branch 'main-remote-mcp' into remote_mcp_without_oauth
ruturaj-browserstack Jul 1, 2025
e768467
Remove unused import of getSDKPrefixCommand from bstack-sdk.ts
ruturaj-browserstack Jul 1, 2025
7768493
Refactor instruction functions to remove unused parameters and clean …
ruturaj-browserstack Jul 1, 2025
3ad82e2
Refactor test management utilities to use BrowserStackConfig instead …
ruturaj-browserstack Jul 3, 2025
d7de713
Refactor openBrowser function to restore its implementation and ensur…
ruturaj-browserstack Jul 3, 2025
08f6bbc
Add child_process import to start-session.ts for process management
ruturaj-browserstack Jul 3, 2025
e07b67f
Refactor config parameter types to use BrowserStackConfig across mult…
ruturaj-browserstack Jul 3, 2025
672f4fc
Enhance local tunnel setup by adding username and password handling f…
ruturaj-browserstack Jul 3, 2025
1e8871b
Refactor openBrowser function to improve error handling and restore i…
ruturaj-browserstack Jul 3, 2025
062dc61
Refactor AccessibilityScanner to handle local URLs correctly and impr…
ruturaj-browserstack Jul 3, 2025
c163dad
Refactor server initialization and enhance BrowserStack integration w…
ruturaj-browserstack Jul 3, 2025
6eed850
Fix access key environment variable name and clean up code formatting
ruturaj-browserstack Jul 3, 2025
5bcde7e
Fix error message for missing BrowserStack access key environment var…
ruturaj-browserstack Jul 3, 2025
b5bcfc4
Add REMOTE_MCP configuration and restrict local URL usage in remote mode
ruturaj-browserstack Jul 3, 2025
8a25a11
Clean up whitespace in config and start-session files
ruturaj-browserstack Jul 3, 2025
831d162
Add exports field to package.json for module resolution
ruturaj-browserstack Jul 3, 2025
e911c7a
Export logger and createMcpServer functions from index.ts
ruturaj-browserstack Jul 3, 2025
5efbe54
Add declaration option to TypeScript configuration
ruturaj-browserstack Jul 3, 2025
17bfb4f
Remove isolatedModules option from TypeScript configuration
ruturaj-browserstack Jul 3, 2025
1b0b40a
Refactor main function execution to check module context before running
ruturaj-browserstack Jul 4, 2025
9d88768
Simplify main function execution by removing module context check
ruturaj-browserstack Jul 4, 2025
ed4b96a
Add remote MCP mode check in main function
ruturaj-browserstack Jul 4, 2025
5bfcf2e
Merge branch 'local-mcp-changes' into local-new-update
ruturaj-browserstack Jul 8, 2025
8e918dc
Merge pull request #8 from ruturaj-browserstack/local-new-update
ruturaj-browserstack Jul 8, 2025
eca0fa6
feat: refactor app automation functions to accept BrowserStack config…
ruturaj-browserstack Jul 8, 2025
f9aa128
Refactor API calls to use apiClient for improved consistency and erro…
ruturaj-browserstack Jul 8, 2025
ad61ad8
refactor: remove unused exports and dependencies from package.json
ruturaj-browserstack Jul 8, 2025
861444e
fix: add missing newline at end of package-lock.json
ruturaj-browserstack Jul 8, 2025
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
"vite": "^6.3.5",
"vitest": "^3.1.3"
}
}
}
6 changes: 2 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,18 @@ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
*/
export class Config {
constructor(
public readonly browserstackUsername: string,
public readonly browserstackAccessKey: string,
public readonly DEV_MODE: boolean,
public readonly browserstackLocalOptions: Record<string, any>,
public readonly USE_OWN_LOCAL_BINARY_PROCESS: boolean,
public readonly REMOTE_MCP: boolean,
) {}
}

const config = new Config(
process.env.BROWSERSTACK_USERNAME!,
process.env.BROWSERSTACK_ACCESS_KEY!,
process.env.DEV_MODE === "true",
browserstackLocalOptions,
process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true",
process.env.REMOTE_MCP === "true",
);

export default config;
61 changes: 27 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,43 @@
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
import "dotenv/config";
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";

function registerTools(server: McpServer) {
addAccessibilityTools(server);
addSDKTools(server);
addAppLiveTools(server);
addBrowserLiveTools(server);
addTestManagementTools(server);
addAppAutomationTools(server);
addFailureLogsTools(server);
addAutomateTools(server);
addSelfHealTools(server);
}

// Create an MCP server
const server: McpServer = new McpServer({
name: "BrowserStack MCP Server",
version: packageJson.version,
});

setupOnInitialized(server);

registerTools(server);
import { createMcpServer } from "./server-factory.js";

async function main() {
logger.info(
"Launching BrowserStack MCP server, version %s",
packageJson.version,
);

// Start receiving messages on stdin and sending messages on stdout
const remoteMCP = process.env.REMOTE_MCP === "true";
if (remoteMCP) {
logger.info("Running in remote MCP mode");
return;
}

const username = process.env.BROWSERSTACK_USERNAME;
const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;

if (!username) {
throw new Error("BROWSERSTACK_USERNAME environment variable is required");
}

if (!accessKey) {
throw new Error("BROWSERSTACK_ACCESS_KEY environment variable is required");
}

const transport = new StdioServerTransport();

const server = createMcpServer({
"browserstack-username": username,
"browserstack-access-key": accessKey,
});

await server.connect(transport);
}

Expand All @@ -57,3 +47,6 @@ main().catch(console.error);
process.on("exit", () => {
logger.flush();
});

export { default as logger } from "./logger.js";
export { createMcpServer } from "./server-factory.js";
17 changes: 11 additions & 6 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import config from "../config.js";
import { getBrowserStackAuth } from "./get-auth.js";
import { BrowserStackConfig } from "../lib/types.js";
import { apiClient } from "./apiClient.js";

export async function getLatestO11YBuildInfo(
buildName: string,
projectName: string,
config: BrowserStackConfig,
) {
const buildsUrl = `https://api-observability.browserstack.com/ext/v1/builds/latest?build_name=${encodeURIComponent(
buildName,
)}&project_name=${encodeURIComponent(projectName)}`;

const buildsResponse = await fetch(buildsUrl, {
const authString = getBrowserStackAuth(config);
const auth = Buffer.from(authString).toString("base64");

const buildsResponse = await apiClient.get({
url: buildsUrl,
headers: {
Authorization: `Basic ${Buffer.from(
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
).toString("base64")}`,
Authorization: `Basic ${auth}`,
},
});

Expand All @@ -25,5 +30,5 @@ export async function getLatestO11YBuildInfo(
throw new Error(`Failed to fetch builds: ${buildsResponse.statusText}`);
}

return buildsResponse.json();
return buildsResponse;
}
118 changes: 118 additions & 0 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";

type RequestOptions = {
url: string;
headers?: Record<string, string>;
params?: Record<string, string | number>;
body?: any;
};

class ApiResponse<T = any> {
private _response: AxiosResponse<T>;

constructor(response: AxiosResponse<T>) {
this._response = response;
}

get data(): T {
return this._response.data;
}

get status(): number {
return this._response.status;
}

get statusText(): string {
return this._response.statusText;
}

get headers(): Record<string, string> {
const raw = this._response.headers;
const sanitized: Record<string, string> = {};

for (const key in raw) {
const value = raw[key];
if (typeof value === "string") {
sanitized[key] = value;
}
}

return sanitized;
}

get config(): AxiosRequestConfig {
return this._response.config;
}

get url(): string | undefined {
return this._response.config.url;
}

get ok(): boolean {
return this._response.status >= 200 && this._response.status < 300;
}
}

class ApiClient {
private instance = axios.create();

async get<T = any>({
url,
headers,
params,
}: RequestOptions): Promise<ApiResponse<T>> {
const res = await this.instance.get<T>(url, {
headers,
params,
});
return new ApiResponse<T>(res);
}

async post<T = any>({
url,
headers,
body,
}: RequestOptions): Promise<ApiResponse<T>> {
const res = await this.instance.post<T>(url, body, {
headers,
});
return new ApiResponse<T>(res);
}

async put<T = any>({
url,
headers,
body,
}: RequestOptions): Promise<ApiResponse<T>> {
const res = await this.instance.put<T>(url, body, {
headers,
});
return new ApiResponse<T>(res);
}

async patch<T = any>({
url,
headers,
body,
}: RequestOptions): Promise<ApiResponse<T>> {
const res = await this.instance.patch<T>(url, body, {
headers,
});
return new ApiResponse<T>(res);
}

async delete<T = any>({
url,
headers,
params,
}: RequestOptions): Promise<ApiResponse<T>> {
const res = await this.instance.delete<T>(url, {
headers,
params,
});
return new ApiResponse<T>(res);
}
}

export const apiClient = new ApiClient();
export type { ApiResponse, RequestOptions };
7 changes: 3 additions & 4 deletions src/lib/device-cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs";
import os from "os";
import path from "path";
import { apiClient } from "./apiClient.js";

const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
const CACHE_FILE = path.join(CACHE_DIR, "data.json");
Expand Down Expand Up @@ -48,18 +49,16 @@ export async function getDevicesAndBrowsers(
}
}

const liveRes = await fetch(URLS[type]);
const liveRes = await apiClient.get({ url: URLS[type] });

if (!liveRes.ok) {
throw new Error(
`Failed to fetch configuration from BrowserStack : ${type}=${liveRes.statusText}`,
);
}

const data = await liveRes.json();

cache = {
[type]: data,
[type]: liveRes,
};
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");

Expand Down
10 changes: 10 additions & 0 deletions src/lib/get-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BrowserStackConfig } from "../lib/types.js";

export function getBrowserStackAuth(config: BrowserStackConfig): string {
const username = config["browserstack-username"];
const accessKey = config["browserstack-access-key"];
if (!username || !accessKey) {
throw new Error("BrowserStack credentials not set on server.authHeaders");
}
return `${username}:${accessKey}`;
}
18 changes: 9 additions & 9 deletions src/lib/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logger from "../logger.js";
import config from "../config.js";
import { getBrowserStackAuth } from "./get-auth.js";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const packageJson = require("../../package.json");
Expand All @@ -21,12 +21,8 @@ export function trackMCP(
toolName: string,
clientInfo: { name?: string; version?: string },
error?: unknown,
config?: any,
): void {
if (config.DEV_MODE) {
logger.info("Tracking MCP is disabled in dev mode");
return;
}

const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
const isSuccess = !error;
const mcpClient = clientInfo?.name || "unknown";
Expand Down Expand Up @@ -58,13 +54,17 @@ export function trackMCP(
error instanceof Error ? error.constructor.name : "Unknown";
}

let authHeader = undefined;
if (config) {
const authString = getBrowserStackAuth(config);
authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
}

axios
.post(instrumentationEndpoint, event, {
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${Buffer.from(
`${config.browserstackUsername}:${config.browserstackAccessKey}`,
).toString("base64")}`,
...(authHeader ? { Authorization: authHeader } : {}),
},
timeout: 2000,
})
Expand Down
6 changes: 4 additions & 2 deletions src/lib/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export async function killExistingBrowserStackLocalProcesses() {
}

export async function ensureLocalBinarySetup(
username: string,
password: string,
localIdentifier?: string,
): Promise<void> {
logger.info(
Expand Down Expand Up @@ -104,8 +106,8 @@ export async function ensureLocalBinarySetup(
// Use a single options object from config and extend with required fields
const bsLocalArgs: Record<string, any> = {
...(config.browserstackLocalOptions || {}),
key: config.browserstackAccessKey,
username: config.browserstackUsername,
key: password,
username,
};

if (localIdentifier) {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type BrowserStackConfig = {
"browserstack-username": string;
"browserstack-access-key": string;
};
6 changes: 5 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sharp from "sharp";
import type { ApiResponse } from "./apiClient.js";

export function sanitizeUrlParam(param: string): string {
// Remove any characters that could be used for command injection
Expand All @@ -24,7 +25,10 @@ export async function maybeCompressBase64(base64: string): Promise<string> {
return compressedBuffer.toString("base64");
}

export async function assertOkResponse(response: Response, action: string) {
export async function assertOkResponse(
response: Response | ApiResponse,
action: string,
) {
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Invalid session ID for ${action}`);
Expand Down
Loading