Skip to main content
Glama

autonomous-frontend-browser-tools

shared.ts16.3 kB
/** * Refactor Temp: Top Scaffolding — env/config, connection bootstrap, shared utils * This module aggregates non-tool-specific code extracted from browser-connector.ts. * Exports are named to allow fine-grained re-use during the transition. * * NOTE: This file holds only stateless/shared utilities and configuration helpers. * Express bindings and WebSocket state remain in browser-connector.ts. */ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import os from "os"; // Recreate ESM helper constants and export them for passthrough usage export const __filename = fileURLToPath(import.meta.url); export const __dirname = path.dirname(__filename); /** * Returns the default downloads folder path for the current OS, used for screenshot storage fallback. */ export function getDefaultDownloadsFolder(): string { const homeDir = os.homedir(); // Downloads folder is typically the same path on Windows, macOS, and Linux const downloadsPath = path.join(homeDir, "Downloads", "mcp-screenshots"); return downloadsPath; } /** * Helper to recursively truncate strings in any data structure * (moved from browser-connector.ts) */ export function truncateStringsInData(data: any, maxLength: number): any { if (typeof data === "string") { return data.length > maxLength ? data.substring(0, maxLength) + "... (truncated)" : data; } if (Array.isArray(data)) { return data.map((item) => truncateStringsInData(item, maxLength)); } if (typeof data === "object" && data !== null) { const result: any = {}; for (const [key, value] of Object.entries(data)) { result[key] = truncateStringsInData(value as any, maxLength); } return result; } return data; } /** * Helper to safely parse and process JSON strings * (moved from browser-connector.ts) */ export function processJsonString( jsonString: string, maxLength: number ): string { try { // Try to parse the string as JSON const parsed = JSON.parse(jsonString); // Process any strings within the parsed JSON const processed = truncateStringsInData(parsed, maxLength); // Stringify the processed data return JSON.stringify(processed); } catch { // If it's not valid JSON, treat it as a regular string return truncateStringsInData(jsonString, maxLength); } } /** * Helper to calculate size of a log entry * (moved from browser-connector.ts) */ export function calculateLogSize(log: any): number { return JSON.stringify(log).length; } /** * Helper to process logs based on current settings * (moved from browser-connector.ts) */ export function processLogsWithSettings( logs: any[], currentSettings: { showRequestHeaders: boolean; showResponseHeaders: boolean; } ): any[] { return logs.map((log) => { const processedLog = { ...(log as any) }; if ((log as any).type === "network-request") { // Handle headers visibility if (!currentSettings.showRequestHeaders) { delete (processedLog as any).requestHeaders; } if (!currentSettings.showResponseHeaders) { delete (processedLog as any).responseHeaders; } } return processedLog; }); } /** * Helper to truncate logs based on character limit * (moved from browser-connector.ts) */ export interface TruncateSettings { queryLimit: number; showRequestHeaders: boolean; showResponseHeaders: boolean; } /** * Network logging types and limits (shared across tools) * (moved from browser-connector.ts) */ export interface NetworkLogEntry { url: string; method: string; status: number; requestHeaders: any; responseHeaders: any; requestBody?: string; responseBody?: string; timestamp: number; } /** Maximum in-memory cache size for detailed network logs */ export const MAX_DETAILED_NETWORK_LOG_CACHE = 50; /** * Keep a backward-compatible signature: if settings are omitted, defaults are used. */ export function truncateLogsToQueryLimit( logs: any[], currentSettings?: TruncateSettings ): any[] { // Overload support: original single-arg signature (logs only) // and new two-arg signature (logs, settings). If settings omitted, // return logs unchanged to preserve original behavior at call sites // that expect default processing in the caller. if (logs.length === 0) return logs; const effectiveSettings: TruncateSettings = currentSettings ?? { queryLimit: 30000, showRequestHeaders: false, showResponseHeaders: false, }; // First process logs according to current settings const processedLogs = processLogsWithSettings(logs, effectiveSettings); let currentSize = 0; const result: any[] = []; for (const log of processedLogs) { const logSize = calculateLogSize(log); // Check if adding this log would exceed the limit if (currentSize + logSize > effectiveSettings.queryLimit) { console.log( `Reached query limit (${currentSize}/${effectiveSettings.queryLimit}), truncating logs` ); break; } // Add log and update size result.push(log); currentSize += logSize; console.log(`Added log of size ${logSize}, total size now: ${currentSize}`); } return result; } /** * Converts a file path to the appropriate format for the current platform * Handles Windows, WSL, macOS and Linux path formats * (moved from browser-connector.ts) */ export function convertPathForCurrentPlatform(inputPath: string): string { const platform = os.platform(); // If no path provided, return as is if (!inputPath) return inputPath; console.log(`Converting path "${inputPath}" for platform: ${platform}`); // Windows-specific conversion if (platform === "win32") { // Convert forward slashes to backslashes return inputPath.replace(/\//g, "\\"); } // Linux/Mac-specific conversion if (platform === "linux" || platform === "darwin") { // Check if this is a Windows UNC path (starts with \\) if (inputPath.startsWith("\\\\") || inputPath.includes("\\")) { // Check if this is a WSL path (contains wsl.localhost or wsl$) if (inputPath.includes("wsl.localhost") || inputPath.includes("wsl$")) { // Extract the path after the distribution name // Handle both \\wsl.localhost\Ubuntu\path and \\wsl$\Ubuntu\path formats const parts = inputPath.split("\\").filter((part) => part.length > 0); console.log("Path parts:", parts); // Find the index after the distribution name const distNames = [ "Ubuntu", "Debian", "kali", "openSUSE", "SLES", "Fedora", ]; // Find the distribution name in the path let distIndex = -1; for (const dist of distNames) { const index = parts.findIndex( (part) => part === dist || part.toLowerCase() === dist.toLowerCase() ); if (index !== -1) { distIndex = index; break; } } if (distIndex !== -1 && distIndex + 1 < parts.length) { // Reconstruct the path as a native Linux path const linuxPath = "/" + parts.slice(distIndex + 1).join("/"); console.log( `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"` ); return linuxPath; } // If we couldn't find a distribution name but it's clearly a WSL path, // try to extract everything after wsl.localhost or wsl$ const wslIndex = parts.findIndex( (part) => part === "wsl.localhost" || part === "wsl$" || part.toLowerCase() === "wsl.localhost" || part.toLowerCase() === "wsl$" ); if (wslIndex !== -1 && wslIndex + 2 < parts.length) { // Skip the WSL prefix and distribution name const linuxPath = "/" + parts.slice(wslIndex + 2).join("/"); console.log( `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"` ); return linuxPath; } } // For non-WSL Windows paths, just normalize the slashes const normalizedPath = inputPath .replace(/\\\\/g, "/") .replace(/\\/g, "/"); console.log( `Converted Windows UNC path "${inputPath}" to "${normalizedPath}"` ); return normalizedPath; } // Handle Windows drive letters (e.g., C:\path\to\file) if (/^[A-Z]:\\/i.test(inputPath)) { // Convert Windows drive path to Linux/Mac compatible path const normalizedPath = inputPath .replace(/^[A-Z]:\\/i, "/") .replace(/\\/g, "/"); console.log( `Converted Windows drive path "${inputPath}" to "${normalizedPath}"` ); return normalizedPath; } } // Return the original path if no conversion was needed or possible return inputPath; } // ==== Project configuration management (moved) ==== export interface ProjectConfig { SWAGGER_URL?: string; AUTH_ORIGIN?: string; AUTH_STORAGE_TYPE?: string; AUTH_TOKEN_KEY?: string; API_BASE_URL?: string; SCREENSHOT_STORAGE_PATH?: string; BROWSER_TOOLS_HOST?: string; BROWSER_TOOLS_PORT?: string; ROUTES_FILE_PATH?: string; } export interface Project { config: ProjectConfig; } export interface ProjectsConfig { projects: Record<string, Project>; defaultProject: string; DEFAULT_SCREENSHOT_STORAGE_PATH?: string; } // Load project configuration // Lightweight in-process cache to avoid repeated filesystem reads and duplicate logs let cachedProjectsConfig: ProjectsConfig | null = null; let cachedProjectsConfigPath: string | null = null; let cachedProjectsConfigMtimeMs: number | null = null; let hasLoggedProjectsConfig: boolean = false; /** * Resolve the path to projects.json with priority: * 1) env AFBT_PROJECTS_JSON (absolute path) * 2) CWD/chrome-extension/projects.json (Setup UI writes here) * 3) Packaged fallback relative to this module (node_modules/...) */ function resolveProjectsJsonPath(): string { const candidates: string[] = []; if (process.env.AFBT_PROJECTS_JSON) { candidates.push(path.resolve(process.env.AFBT_PROJECTS_JSON)); } // 1) Current working directory (works when server is started from repo root) candidates.push(path.join(process.cwd(), "projects.json")); // 2) Parent of CWD (works when server CWD is browser-tools-server/) try { candidates.push(path.resolve(process.cwd(), "..", "projects.json")); } catch {} // 3) Module-relative repo root (dev/local builds) try { candidates.push(path.resolve(__dirname, "..", "..", "projects.json")); } catch {} try { const home = os.homedir(); if (home) candidates.push(path.resolve(home, ".afbt", "projects.json")); } catch {} const chosen = candidates.find((p) => fs.existsSync(p)); if (!chosen) { throw new Error( `projects.json not found. Checked: ${candidates.join(", ")}. Set AFBT_PROJECTS_JSON or use the Setup UI.` ); } return chosen; } export function loadProjectConfig(): ProjectsConfig | null { try { const configPath = resolveProjectsJsonPath(); // Only log once for discoverability (shows chosen path) if (!hasLoggedProjectsConfig) { console.log( `[INFO] Browser Connector: Loading projects.json from ${configPath}` ); } if (!fs.existsSync(configPath)) { if (!hasLoggedProjectsConfig) { console.log( `[WARN] Browser Connector: projects.json not found at: ${configPath}` ); } cachedProjectsConfig = null; cachedProjectsConfigPath = null; cachedProjectsConfigMtimeMs = null; hasLoggedProjectsConfig = true; return null; } // Reload when the file changes or path differs let stat: fs.Stats | null = null; try { stat = fs.statSync(configPath); } catch { stat = null; } const mtimeMs = stat ? stat.mtimeMs : null; const shouldReload = !cachedProjectsConfig || cachedProjectsConfigPath !== configPath || (mtimeMs !== null && cachedProjectsConfigMtimeMs !== mtimeMs); if (shouldReload) { const configData = fs.readFileSync(configPath, "utf8"); const parsed = JSON.parse(configData) as ProjectsConfig; cachedProjectsConfig = parsed; const previousPath = cachedProjectsConfigPath; cachedProjectsConfigPath = configPath; cachedProjectsConfigMtimeMs = mtimeMs; if (previousPath && previousPath !== configPath) { console.log( `[INFO] Browser Connector: Reloaded projects.json from ${configPath} (was ${previousPath})` ); } if (!hasLoggedProjectsConfig) { const projectCount = Object.keys(parsed.projects || {}).length; console.log( `[INFO] Browser Connector: Loaded projects.json (projects=${projectCount}, defaultProject=${parsed.defaultProject})` ); } } hasLoggedProjectsConfig = true; return cachedProjectsConfig; } catch (error) { console.error("Browser Connector: Error loading projects config:", error); return null; } } // Get configuration value with fallback priority: // 1. Environment variable (highest priority) // 2. Project config file // 3. Default value (lowest priority) export function getConfigValue( key: string, defaultValue?: string ): string | undefined { // First check environment variables if (process.env[key]) { return process.env[key]; } // Security: never read embedding API keys from projects.json if (key === "OPENAI_API_KEY" || key === "GEMINI_API_KEY") { return defaultValue; } // Then check project config const projectsConfig = loadProjectConfig(); if (projectsConfig) { const activeProject = process.env.ACTIVE_PROJECT || projectsConfig.defaultProject; const project = projectsConfig.projects[activeProject]; if (project && project.config[key as keyof ProjectConfig]) { return project.config[key as keyof ProjectConfig]; } } // Finally return default value return defaultValue; } // Get screenshot storage path from project config export function getScreenshotStoragePath(): string | undefined { const projectsConfig = loadProjectConfig(); if (projectsConfig && projectsConfig.DEFAULT_SCREENSHOT_STORAGE_PATH) { return projectsConfig.DEFAULT_SCREENSHOT_STORAGE_PATH; } return undefined; } // Get active project name export function getActiveProjectName(): string | undefined { const projectsConfig = loadProjectConfig(); if (projectsConfig) { const active = process.env.ACTIVE_PROJECT || projectsConfig.defaultProject; return active; } return undefined; } // ==== Port discovery and server utilities ==== /** * Check if a port is available */ export function isPortAvailable(port: number): Promise<boolean> { return new Promise((resolve) => { // Use dynamic import for ESM compatibility instead of require() import("net") .then((netMod: any) => { const server = netMod.createServer(); server.listen(port, () => { server.once("close", () => { resolve(true); }); server.close(); }); server.on("error", () => { resolve(false); }); }) .catch(() => { // If net import somehow fails, mark port as unavailable resolve(false); }); }); } /** * Find an available port starting from the requested port */ export async function getAvailablePort(requestedPort: number): Promise<number> { const maxAttempts = 10; for (let i = 0; i < maxAttempts; i++) { const portToTry = requestedPort + i; const available = await isPortAvailable(portToTry); if (available) { return portToTry; } } throw new Error( `No available port found in range ${requestedPort}-${ requestedPort + maxAttempts - 1 }` ); } // ==== Log management ==== // Global log storage (moved from browser-connector.ts) export const logs: any[] = []; export const networkLogs: any[] = []; export const detailedNetworkLogCache: NetworkLogEntry[] = []; /** * Clear all logs */ export function clearAllLogs(): void { logs.length = 0; networkLogs.length = 0; detailedNetworkLogCache.length = 0; console.log("All logs cleared"); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Winds-AI/Frontend-development-MCP-tools-public'

If you have feedback or need assistance with the MCP directory API, please join our Discord server