Puppeteer MCP Server

import puppeteer, { Browser, Page } from "puppeteer"; import { logger } from "../config/logger.js"; import { dockerConfig, npxConfig, DEFAULT_NAVIGATION_TIMEOUT } from "../config/browser.js"; import { ActiveTab } from "../types/global.js"; // Global browser instance let browser: Browser | undefined; let page: Page | undefined; export async function ensureBrowser(): Promise<Page> { if (!browser) { logger.info('Launching browser with config:', process.env.DOCKER_CONTAINER ? 'docker' : 'npx'); browser = await puppeteer.launch(process.env.DOCKER_CONTAINER ? dockerConfig : npxConfig); const pages = await browser.pages(); page = pages[0]; // Set default navigation timeout await page.setDefaultNavigationTimeout(DEFAULT_NAVIGATION_TIMEOUT); // Enable JavaScript await page.setJavaScriptEnabled(true); logger.info('Browser launched successfully'); } return page!; } export async function getDebuggerWebSocketUrl(port: number = 9222): Promise<string> { try { const response = await fetch(`http://localhost:${port}/json/version`); if (!response.ok) { throw new Error(`Failed to fetch debugger info: ${response.statusText}`); } const data = await response.json(); if (!data.webSocketDebuggerUrl) { throw new Error("No WebSocket debugger URL found. Is Chrome running with --remote-debugging-port?"); } return data.webSocketDebuggerUrl; } catch (error) { throw new Error(`Failed to connect to Chrome debugging port ${port}: ${(error as Error).message}`); } } export async function connectToExistingBrowser( wsEndpoint: string, targetUrl?: string, onConsoleMessage?: (logEntry: string) => void ): Promise<Page> { logger.info('Connecting to existing browser', { wsEndpoint, targetUrl }); try { // If we have an existing Puppeteer-launched browser, close it if (browser && !browser.isConnected()) { logger.debug('Closing existing browser connection'); await browser.close(); browser = undefined; page = undefined; } // Connect to the browser instance with null viewport to maintain browser's viewport logger.debug('Establishing connection to browser'); browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, defaultViewport: null }); logger.info('Successfully connected to browser'); // Get all pages and find non-extension pages const pages = await browser.pages(); const activeTabs: ActiveTab[] = []; for (const p of pages) { const url = await p.url(); if (!url.startsWith('chrome-extension://')) { const title = await p.title(); logger.info('Found active webpage:', { url, title }); activeTabs.push({ page: p, url, title }); } } if (activeTabs.length === 0) { throw new Error("No active non-extension pages found in the browser"); } // Select appropriate page if (targetUrl) { // Find the page with matching URL const targetTab = activeTabs.find(tab => tab.url === targetUrl); page = targetTab ? targetTab.page : activeTabs[0].page; } else { // Use the first active non-extension page page = activeTabs[0].page; } // Configure page settings await page.setExtraHTTPHeaders({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }); // Set up console message handling if (onConsoleMessage) { page.on("console", (msg) => { const logEntry = `[${msg.type()}] ${msg.text()}`; onConsoleMessage(logEntry); }); } return page; } catch (error) { throw error; } } export async function closeBrowser(): Promise<void> { if (browser) { await browser.close(); browser = undefined; page = undefined; } } export function getCurrentPage(): Page | undefined { return page; }