Skip to main content
Glama
snowfort-ai

Snowfort Circuit MCP

Official
by snowfort-ai
web-driver.ts18.4 kB
import { Browser, BrowserContext, Page, chromium, firefox, webkit, ConsoleMessage, Request, Response } from "playwright-core"; import { Driver, LaunchOpts, Session } from "@snowfort/circuit-core"; import { randomUUID } from "crypto"; export interface WebLaunchOpts extends LaunchOpts { browser?: "chromium" | "firefox" | "webkit"; headed?: boolean; userDataDir?: string; viewport?: { width: number; height: number }; compressScreenshots?: boolean; screenshotQuality?: number; } export interface ElementRef { ref: string; role: string; name?: string; selector?: string; level?: number; } export interface PageInfo { id: string; page: Page; title?: string; url?: string; elementRefs?: Map<string, ElementRef>; } export interface WebSession extends Session { browser: Browser; context: BrowserContext; pages: Map<string, PageInfo>; activePage: string; networkRequests: Request[]; consoleMessages: ConsoleMessage[]; dialogHandler?: { action: 'accept' | 'dismiss'; promptText?: string; }; recordedActions: Array<{ type: string; selector?: string; text?: string; timestamp: number; }>; options: WebLaunchOpts; } export class WebDriver implements Driver { async launch(opts: WebLaunchOpts): Promise<WebSession> { const browserType = opts.browser || "chromium"; let browserInstance; switch (browserType) { case "chromium": browserInstance = chromium; break; case "firefox": browserInstance = firefox; break; case "webkit": browserInstance = webkit; break; default: throw new Error(`Unsupported browser: ${browserType}`); } const browser = await browserInstance.launch({ headless: !opts.headed, timeout: opts.timeout, }); const context = await browser.newContext({ viewport: opts.viewport || { width: 1280, height: 720 }, }); const page = await context.newPage(); const pageId = randomUUID(); const session: WebSession = { id: randomUUID(), browser, context, pages: new Map(), activePage: pageId, networkRequests: [], consoleMessages: [], recordedActions: [], options: opts, }; // Add the initial page session.pages.set(pageId, { id: pageId, page, title: 'New Tab', url: 'about:blank', elementRefs: new Map(), }); // Set up event listeners for the initial page this.setupPageListeners(session, pageId); return session; } async navigate(session: Session, url: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.goto(url); pageInfo.url = url; pageInfo.title = await pageInfo.page.title(); this.recordAction(webSession, 'navigate', undefined, url); } async click(session: Session, selector: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.click(selector); this.recordAction(webSession, 'click', selector); } async type(session: Session, selector: string, text: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.fill(selector, text); this.recordAction(webSession, 'type', selector, text); } async screenshot(session: Session, path?: string): Promise<string> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } const compress = webSession.options.compressScreenshots !== false; // Default true const quality = webSession.options.screenshotQuality || 50; let screenshotPath: string; if (compress) { screenshotPath = path || `screenshot-${Date.now()}.jpeg`; await pageInfo.page.screenshot({ path: screenshotPath, type: 'jpeg', quality: quality }); } else { screenshotPath = path || `screenshot-${Date.now()}.png`; await pageInfo.page.screenshot({ path: screenshotPath, type: 'png' }); } return screenshotPath; } async evaluate(session: Session, script: string): Promise<any> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } try { // Check if the script contains a return statement outside of a function // If it does, wrap it in an IIFE (Immediately Invoked Function Expression) const trimmedScript = script.trim(); const hasReturnOutsideFunction = /^return\s+|[\s;]return\s+/.test(trimmedScript) && !trimmedScript.startsWith('function') && !trimmedScript.startsWith('(') && !trimmedScript.includes('=>'); let evalScript = script; if (hasReturnOutsideFunction) { // Wrap in IIFE to make return statement valid evalScript = `(() => { ${script} })()`; } // Execute the script with proper error handling return await pageInfo.page.evaluate(evalScript); } catch (error) { // Handle syntax errors and other evaluation errors gracefully const errorMessage = error instanceof Error ? error.message : String(error); // Log detailed error for debugging console.error(`[WEB-MCP] JavaScript evaluation error:`, errorMessage); console.error(`[WEB-MCP] Script that failed:`, script); // Re-throw with more context throw new Error(`JavaScript evaluation failed: ${errorMessage}`); } } async waitForSelector(session: Session, selector: string, timeout?: number): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.waitForSelector(selector, { timeout: timeout || 30000 }); } async snapshot(session: Session): Promise<string> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } const snapshot = await pageInfo.page.accessibility.snapshot(); const enhancedSnapshot = this.enhanceSnapshotWithRefs(snapshot, pageInfo); return JSON.stringify(enhancedSnapshot, null, 2); } private enhanceSnapshotWithRefs(snapshot: any, pageInfo: PageInfo): any { if (!snapshot) return snapshot; let refCounter = 1; const addRefs = (node: any): any => { if (!node) return node; const enhanced = { ...node }; // Add element reference if (node.role && node.role !== 'WebArea') { const ref = `e${refCounter++}`; enhanced.ref = ref; // Store reference for later use if (pageInfo.elementRefs) { pageInfo.elementRefs.set(ref, { ref, role: node.role, name: node.name, level: node.level, selector: this.generateSelector(node) }); } } // Process children recursively if (node.children && Array.isArray(node.children)) { enhanced.children = node.children.map(addRefs); } return enhanced; }; return addRefs(snapshot); } private generateSelector(node: any): string { // Generate basic CSS selector based on node properties if (node.role === 'link' && node.name) { return `a[href]:has-text("${node.name}")`; } if (node.role === 'button' && node.name) { return `button:has-text("${node.name}")`; } if (node.role === 'heading' && node.name) { return `h${node.level || '1'}:has-text("${node.name}")`; } if (node.role === 'textbox' && node.name) { return `input[placeholder*="${node.name}"]`; } // Fallback to role-based selector return `[role="${node.role}"]`; } async hover(session: Session, selector: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.hover(selector); this.recordAction(webSession, 'hover', selector); } async drag(session: Session, sourceSelector: string, targetSelector: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } const sourceElement = pageInfo.page.locator(sourceSelector); const targetElement = pageInfo.page.locator(targetSelector); await sourceElement.dragTo(targetElement); this.recordAction(webSession, 'drag', `${sourceSelector} to ${targetSelector}`); } async key(session: Session, key: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.keyboard.press(key); this.recordAction(webSession, 'key', undefined, key); } async select(session: Session, selector: string, value: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.selectOption(selector, value); this.recordAction(webSession, 'select', selector, value); } async upload(session: Session, selector: string, filePath: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.setInputFiles(selector, filePath); this.recordAction(webSession, 'upload', selector, filePath); } async back(session: Session): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.goBack(); this.recordAction(webSession, 'back'); } async forward(session: Session): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.goForward(); this.recordAction(webSession, 'forward'); } async refresh(session: Session): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.reload(); this.recordAction(webSession, 'refresh'); } async pdf(session: Session, path?: string): Promise<string> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } const pdfPath = path || `page-${Date.now()}.pdf`; await pageInfo.page.pdf({ path: pdfPath }); return pdfPath; } async content(session: Session): Promise<string> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } return await pageInfo.page.content(); } async textContent(session: Session): Promise<string> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } return await pageInfo.page.textContent('body') || ''; } async resize(session: Session, width: number, height: number): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(webSession.activePage); if (!pageInfo) { throw new Error('No active page found'); } await pageInfo.page.setViewportSize({ width, height }); this.recordAction(webSession, 'resize', undefined, `${width}x${height}`); } async handleDialog(session: Session, action: 'accept' | 'dismiss', promptText?: string): Promise<void> { const webSession = session as WebSession; webSession.dialogHandler = { action, promptText }; } async newTab(session: Session): Promise<string> { const webSession = session as WebSession; const page = await webSession.context.newPage(); const pageId = randomUUID(); webSession.pages.set(pageId, { id: pageId, page, title: 'New Tab', url: 'about:blank', elementRefs: new Map(), }); this.setupPageListeners(webSession, pageId); return pageId; } async listTabs(session: Session): Promise<Array<{id: string, title: string, url: string, active: boolean}>> { const webSession = session as WebSession; const tabs = []; for (const [pageId, pageInfo] of webSession.pages) { tabs.push({ id: pageId, title: pageInfo.title || 'Untitled', url: pageInfo.url || 'about:blank', active: pageId === webSession.activePage }); } return tabs; } async selectTab(session: Session, pageId: string): Promise<void> { const webSession = session as WebSession; if (!webSession.pages.has(pageId)) { throw new Error(`Tab not found: ${pageId}`); } webSession.activePage = pageId; } async closeTab(session: Session, pageId: string): Promise<void> { const webSession = session as WebSession; const pageInfo = webSession.pages.get(pageId); if (!pageInfo) { throw new Error(`Tab not found: ${pageId}`); } await pageInfo.page.close(); webSession.pages.delete(pageId); // If we closed the active page, switch to another one if (webSession.activePage === pageId) { const remainingPages = Array.from(webSession.pages.keys()); if (remainingPages.length > 0) { webSession.activePage = remainingPages[0]; } else { throw new Error('Cannot close the last tab'); } } } async getNetworkRequests(session: Session): Promise<Array<{url: string, method: string, status?: number, timestamp: number}>> { const webSession = session as WebSession; return webSession.networkRequests.map(req => ({ url: req.url(), method: req.method(), timestamp: Date.now() // Approximation, would need to track actual timestamps })); } async getConsoleMessages(session: Session): Promise<Array<{type: string, text: string, timestamp: number}>> { const webSession = session as WebSession; return webSession.consoleMessages.map(msg => ({ type: msg.type(), text: msg.text(), timestamp: Date.now() // Approximation, would need to track actual timestamps })); } async generatePlaywrightTest(session: Session): Promise<string> { const webSession = session as WebSession; let testCode = `const { test, expect } = require('@playwright/test'); test('Generated test', async ({ page }) => { `; for (const action of webSession.recordedActions) { switch (action.type) { case 'navigate': testCode += ` await page.goto('${action.text}'); `; break; case 'click': testCode += ` await page.click('${action.selector}'); `; break; case 'type': testCode += ` await page.fill('${action.selector}', '${action.text}'); `; break; case 'key': testCode += ` await page.keyboard.press('${action.text}'); `; break; case 'hover': testCode += ` await page.hover('${action.selector}'); `; break; case 'select': testCode += ` await page.selectOption('${action.selector}', '${action.text}'); `; break; } } testCode += `}); `; return testCode; } private setupPageListeners(session: WebSession, pageId: string): void { const pageInfo = session.pages.get(pageId); if (!pageInfo) return; const page = pageInfo.page; // Dialog handling page.on('dialog', async (dialog) => { if (session.dialogHandler) { const { action, promptText } = session.dialogHandler; if (action === 'accept') { await dialog.accept(promptText || ''); } else { await dialog.dismiss(); } } else { await dialog.dismiss(); // Default action } }); // Network monitoring page.on('request', (request) => { session.networkRequests.push(request); }); // Console monitoring page.on('console', (message) => { session.consoleMessages.push(message); }); // Update page info on navigation page.on('load', async () => { pageInfo.title = await page.title(); pageInfo.url = page.url(); // Clear element refs on navigation if (pageInfo.elementRefs) { pageInfo.elementRefs.clear(); } }); } private recordAction(session: WebSession, type: string, selector?: string, text?: string): void { session.recordedActions.push({ type, selector, text, timestamp: Date.now() }); } async close(session: Session): Promise<void> { const webSession = session as WebSession; // Close all pages for (const pageInfo of webSession.pages.values()) { await pageInfo.page.close(); } await webSession.context.close(); await webSession.browser.close(); } }

Latest Blog Posts

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/snowfort-ai/circuit-mcp'

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