Skip to main content
Glama

Browser Agent MCP

executor.ts19.1 kB
import { chromium, firefox, webkit, Browser, Page, request, APIRequestContext, BrowserType } from "playwright"; import { CallToolResult, TextContent, ImageContent } from "@modelcontextprotocol/sdk/types.js"; import { BROWSER_TOOLS, API_TOOLS } from "./tools.js"; import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; let browser: Browser | null = null; let page: Page | null = null; const browserLogs: string[] = []; const screenshotRegistry = new Map<string, string>(); const defaultDownloadsPath = path.join(os.homedir(), 'Downloads'); const getConfig = () => { const config = { browserType: 'chrome', viewportWidth: 1280, viewportHeight: 800, deviceScaleFactor: 1.25 }; if (process.env.MCP_BROWSER_TYPE) { config.browserType = process.env.MCP_BROWSER_TYPE.toLowerCase(); } if (process.env.MCP_VIEWPORT_WIDTH) { config.viewportWidth = parseInt(process.env.MCP_VIEWPORT_WIDTH, 10); } if (process.env.MCP_VIEWPORT_HEIGHT) { config.viewportHeight = parseInt(process.env.MCP_VIEWPORT_HEIGHT, 10); } if (process.env.MCP_DEVICE_SCALE_FACTOR) { config.deviceScaleFactor = parseFloat(process.env.MCP_DEVICE_SCALE_FACTOR); } try { const configPath = path.join(os.homedir(), '.mcp_browser_agent_config.json'); if (fs.existsSync(configPath)) { const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (fileConfig.browserType && !process.env.MCP_BROWSER_TYPE) { config.browserType = fileConfig.browserType.toLowerCase(); } if (fileConfig.viewportWidth && !process.env.MCP_VIEWPORT_WIDTH) { config.viewportWidth = fileConfig.viewportWidth; } if (fileConfig.viewportHeight && !process.env.MCP_VIEWPORT_HEIGHT) { config.viewportHeight = fileConfig.viewportHeight; } if (fileConfig.deviceScaleFactor && !process.env.MCP_DEVICE_SCALE_FACTOR) { config.deviceScaleFactor = fileConfig.deviceScaleFactor; } } } catch (error) { console.error('Error reading config file:', error); } return config; }; export function getBrowserLogs(): string[] { return browserLogs; } export function getScreenshotRegistry(): Map<string, string> { return screenshotRegistry; } process.on('SIGINT', async () => { await cleanupBrowser(); process.exit(0); }); process.on('SIGTERM', async () => { await cleanupBrowser(); process.exit(0); }); async function cleanupBrowser() { if (browser) { try { await browser.close(); browser = null; page = null; console.log('Browser instance closed successfully'); } catch (error) { console.error('Error closing browser:', error); } } } async function initBrowser(): Promise<Page> { if (!browser) { const config = getConfig(); let browserInstance: BrowserType; switch (config.browserType) { case 'firefox': browserInstance = firefox; break; case 'webkit': case 'safari': browserInstance = webkit; break; case 'chrome': case 'chromium': default: browserInstance = chromium; break; } // Determine if we're running in a Docker container const isDocker = fs.existsSync('/.dockerenv') || fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker'); browser = await browserInstance.launch({ headless: isDocker ? true : false, channel: config.browserType === 'chrome' && !isDocker ? 'chrome' : undefined }); const context = await browser.newContext({ viewport: { width: config.viewportWidth, height: config.viewportHeight }, deviceScaleFactor: config.deviceScaleFactor, }); page = await context.newPage(); page.on("console", (msg) => { const logEntry = `[${msg.type()}] ${msg.text()}`; browserLogs.push(logEntry); }); } return page!; } async function initApiClient(baseUrl: string): Promise<APIRequestContext> { return await request.newContext({ baseURL: baseUrl, }); } async function getResponseData(response: any): Promise<TextContent[]> { const contentType = response.headers()['content-type'] || ''; let responseText: string; if (contentType.includes('application/json')) { try { const json = await response.json(); responseText = JSON.stringify(json, null, 2); } catch (e) { responseText = await response.text(); } } else { responseText = await response.text(); } return [{ type: "text", text: `Response body:\n${responseText}`, } as TextContent]; } export async function executeToolCall( toolName: string, args: any, server: any ): Promise<{ toolResult: CallToolResult }> { try { const isBrowserTool = BROWSER_TOOLS.includes(toolName); const isApiTool = API_TOOLS.includes(toolName); let activePage: Page | null = null; let apiClient: APIRequestContext | null = null; if (isBrowserTool) { activePage = await initBrowser(); } if (isApiTool) { apiClient = await initApiClient(args.url); } switch (toolName) { case "browser_set_viewport": return await handleBrowserSetViewport(activePage!, args); case "browser_navigate": return await handleBrowserNavigate(activePage!, args); case "browser_screenshot": return await handleBrowserScreenshot(activePage!, args, server); case "browser_click": return await handleBrowserClick(activePage!, args); case "browser_fill": return await handleBrowserFill(activePage!, args); case "browser_select": return await handleBrowserSelect(activePage!, args); case "browser_hover": return await handleBrowserHover(activePage!, args); case "browser_evaluate": return await handleBrowserEvaluate(activePage!, args); case "api_get": return await handleApiGet(apiClient!, args); case "api_post": return await handleApiPost(apiClient!, args); case "api_put": return await handleApiPut(apiClient!, args); case "api_patch": return await handleApiPatch(apiClient!, args); case "api_delete": return await handleApiDelete(apiClient!, args); default: return { toolResult: { content: [{ type: "text", text: `Unknown tool: ${toolName}`, }], isError: true, }, }; } } catch (error) { return { toolResult: { content: [{ type: "text", text: `Tool execution error: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserNavigate(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { await page.goto(args.url, { timeout: args.timeout || 30000, waitUntil: args.waitUntil || "load" }); return { toolResult: { content: [{ type: "text", text: `Navigated to ${args.url}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Navigation failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserScreenshot(page: Page, args: any, server: any): Promise<{ toolResult: CallToolResult }> { try { const options: any = { type: "png", fullPage: !!args.fullPage }; if (args.selector) { const element = await page.$(args.selector); if (!element) { return { toolResult: { content: [{ type: "text", text: `Element not found: ${args.selector}`, }], isError: true, }, }; } options.element = element; } if (args.mask && Array.isArray(args.mask)) { options.mask = await Promise.all( args.mask.map(async (selector: string) => await page.$(selector)) ); } const screenshot = await page.screenshot(options); const base64Screenshot = screenshot.toString('base64'); const responseContent: (TextContent | ImageContent)[] = []; const savePath = args.savePath || defaultDownloadsPath; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${args.name}-${timestamp}.png`; const filePath = path.join(savePath, filename); const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, screenshot); responseContent.push({ type: "text", text: `Screenshot saved to: ${filePath}`, } as TextContent); screenshotRegistry.set(args.name, base64Screenshot); server.notification({ method: "notifications/resources/list_changed", }); responseContent.push({ type: "image", data: base64Screenshot, mimeType: "image/png", } as ImageContent); return { toolResult: { content: responseContent, isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Screenshot failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserClick(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { await page.click(args.selector); return { toolResult: { content: [{ type: "text", text: `Clicked element: ${args.selector}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Click failed on ${args.selector}: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserFill(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { await page.waitForSelector(args.selector); await page.fill(args.selector, args.value); return { toolResult: { content: [{ type: "text", text: `Filled ${args.selector} with: ${args.value}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Fill operation failed on ${args.selector}: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserSelect(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { await page.waitForSelector(args.selector); await page.selectOption(args.selector, args.value); return { toolResult: { content: [{ type: "text", text: `Selected option ${args.value} in ${args.selector}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Selection failed on ${args.selector}: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserHover(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { await page.waitForSelector(args.selector); await page.hover(args.selector); return { toolResult: { content: [{ type: "text", text: `Hovered over element: ${args.selector}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Hover failed on ${args.selector}: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserEvaluate(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { const result = await page.evaluate((script) => { const logs: string[] = []; const originalConsole = { ...console }; ['log', 'info', 'warn', 'error'].forEach(method => { (console as any)[method] = (...args: any[]) => { logs.push(`[${method}] ${args.join(' ')}`); (originalConsole as any)[method](...args); }; }); try { const result = eval(script); Object.assign(console, originalConsole); return { result, logs }; } catch (error) { Object.assign(console, originalConsole); throw error; } }, args.script); return { toolResult: { content: [ { type: "text", text: `Script result: ${JSON.stringify(result.result, null, 2)}`, }, { type: "text", text: `Console output:\n${result.logs.join('\n')}`, } ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Script execution failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleApiGet(client: APIRequestContext, args: any): Promise<{ toolResult: CallToolResult }> { try { const options = args.headers ? { headers: args.headers } : undefined; const response = await client.get(args.url, options); const responseData = await getResponseData(response); return { toolResult: { content: [ { type: "text", text: `GET ${args.url} - Status: ${response.status()}`, }, ...responseData ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `GET request failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleApiPost(client: APIRequestContext, args: any): Promise<{ toolResult: CallToolResult }> { try { const options = { data: args.data, headers: args.headers || { 'Content-Type': 'application/json' } }; const response = await client.post(args.url, options); const responseData = await getResponseData(response); return { toolResult: { content: [ { type: "text", text: `POST ${args.url} - Status: ${response.status()}`, }, ...responseData ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `POST request failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleApiPut(client: APIRequestContext, args: any): Promise<{ toolResult: CallToolResult }> { try { const options = { data: args.data, headers: args.headers || { 'Content-Type': 'application/json' } }; const response = await client.put(args.url, options); const responseData = await getResponseData(response); return { toolResult: { content: [ { type: "text", text: `PUT ${args.url} - Status: ${response.status()}`, }, ...responseData ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `PUT request failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleApiPatch(client: APIRequestContext, args: any): Promise<{ toolResult: CallToolResult }> { try { const options = { data: args.data, headers: args.headers || { 'Content-Type': 'application/json' } }; const response = await client.patch(args.url, options); const responseData = await getResponseData(response); return { toolResult: { content: [ { type: "text", text: `PATCH ${args.url} - Status: ${response.status()}`, }, ...responseData ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `PATCH request failed: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleBrowserSetViewport(page: Page, args: any): Promise<{ toolResult: CallToolResult }> { try { const config = getConfig(); // Get current values or use defaults from config const width = args.width || config.viewportWidth; const height = args.height || config.viewportHeight; const deviceScaleFactor = args.deviceScaleFactor || config.deviceScaleFactor; // Set the new viewport size await page.setViewportSize({ width, height }); // Save the configuration for future sessions try { const configPath = path.join(os.homedir(), '.mcp_browser_agent_config.json'); const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {}; if (args.width) { config.viewportWidth = width; process.env.MCP_VIEWPORT_WIDTH = width.toString(); } if (args.height) { config.viewportHeight = height; process.env.MCP_VIEWPORT_HEIGHT = height.toString(); } if (args.deviceScaleFactor) { config.deviceScaleFactor = deviceScaleFactor; process.env.MCP_DEVICE_SCALE_FACTOR = deviceScaleFactor.toString(); } fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } catch (error) { console.error('Error saving viewport config:', error); } return { toolResult: { content: [{ type: "text", text: `Set viewport to width: ${width}, height: ${height}, scale factor: ${deviceScaleFactor}`, }], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `Failed to set viewport: ${(error as Error).message}`, }], isError: true, }, }; } } async function handleApiDelete(client: APIRequestContext, args: any): Promise<{ toolResult: CallToolResult }> { try { const options = args.headers ? { headers: args.headers } : undefined; const response = await client.delete(args.url, options); return { toolResult: { content: [ { type: "text", text: `DELETE ${args.url} - Status: ${response.status()}`, } ], isError: false, }, }; } catch (error) { return { toolResult: { content: [{ type: "text", text: `DELETE request failed: ${(error as Error).message}`, }], isError: true, }, }; } }

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/imprvhub/mcp-browser-agent'

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