Skip to main content
Glama

Webpage Screenshot MCP Server

index.ts39.1 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { execSync, spawn } from 'child_process'; import fs, { promises as fsPromises } from 'fs'; import os from 'os'; import path from 'path'; import puppeteer, { Browser, Cookie, Page } from 'puppeteer'; import { z } from 'zod'; // Create the MCP server const server = new McpServer({ name: "screenshot-page", version: "1.0.0", }); let browser: Browser | null = null; let persistentPage: Page | null = null; const cookiesDir = path.join(os.homedir(), '.mcp-screenshot-cookies'); // Ensure cookies directory exists async function ensureCookiesDir() { try { await fsPromises.mkdir(cookiesDir, { recursive: true }); } catch (error) { console.error('Error creating cookies directory:', error); } } // Get path to default Chrome/Edge installation function getDefaultBrowserPath(): string | null { try { if (process.platform === 'darwin') { // Check for Chrome first try { return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; } catch (e) { // Then check for Edge try { return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'; } catch (e) { // Then Safari (though Puppeteer doesn't work well with Safari) return '/Applications/Safari.app/Contents/MacOS/Safari'; } } } else if (process.platform === 'win32') { // On Windows, try to find Chrome or Edge try { const chromePath = execSync('where chrome').toString().trim(); if (chromePath) return chromePath; } catch (e) { try { const edgePath = execSync('where msedge').toString().trim(); if (edgePath) return edgePath; } catch (e) { // Fall back to default installation paths const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files'; const chromePath = `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`; const edgePath = `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`; try { if (fs.existsSync(chromePath)) return chromePath; if (fs.existsSync(edgePath)) return edgePath; } catch (e) { // Ignore filesystem errors } } } } else if (process.platform === 'linux') { // On Linux, try common browser paths try { const chromePath = execSync('which google-chrome').toString().trim(); if (chromePath) return chromePath; } catch (e) { try { const chromiumPath = execSync('which chromium-browser').toString().trim(); if (chromiumPath) return chromiumPath; } catch (e) { // No default browser found } } } } catch (e) { console.error('Error finding default browser:', e); } return null; } // Initialize browser instance async function initBrowser(headless: boolean = true, useDefaultBrowser: boolean = false): Promise<Browser> { if (browser) { // Check if we need to switch modes or browser type const isHeadless = browser.process()?.spawnargs?.includes('--headless') ?? true; const isUsingDefaultBrowser = browser.process()?.spawnargs?.includes('--remote-debugging-port') ?? false; if (isHeadless !== headless || isUsingDefaultBrowser !== useDefaultBrowser) { await browser.close(); browser = null; persistentPage = null; } } if (!browser) { if (useDefaultBrowser && !headless) { // Try to connect to default browser const defaultBrowserPath = getDefaultBrowserPath(); if (!defaultBrowserPath) { console.error('Could not find default browser. Falling back to bundled Chromium.'); browser = await puppeteer.launch({ executablePath: defaultBrowserPath ?? undefined, headless: headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-blink-features=AutomationControlled', '--disable-features=VizDisplayCompositor', '--disable-extensions-file-access-check', '--disable-extensions-http-throttling', '--disable-extensions-https-error-pages', '--disable-extensions', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-ipc-flooding-protection', '--disable-default-apps', '--disable-sync', '--disable-translate', '--hide-scrollbars', '--mute-audio', '--no-default-browser-check', '--no-pings', '--disable-web-security', '--disable-features=TranslateUI', '--disable-features=BlinkGenPropertyTrees', '--disable-client-side-phishing-detection', '--disable-component-extensions-with-background-pages', '--disable-default-apps', '--disable-hang-monitor', '--disable-prompt-on-repost', headless ? '--disable-gpu' : '' ].filter(Boolean) }); } else { // Use random debug port in allowed range (9222-9322) const debuggingPort = 9222 + Math.floor(Math.random() * 100); // Launch browser with debugging port const userDataDir = path.join(os.tmpdir(), `puppeteer_user_data_${Date.now()}`); // Launch browser process using spawn instead of execSync const browserProcess = spawn( defaultBrowserPath, [ `--remote-debugging-port=${debuggingPort}`, `--user-data-dir=${userDataDir}`, '--no-first-run', 'about:blank' ], { stdio: 'ignore', detached: true } ); // Detach the process so it continues running after our process exits browserProcess.unref(); // Wait for browser to start await new Promise(resolve => setTimeout(resolve, 1000)); // Connect to the browser try { browser = await puppeteer.connect({ browserURL: `http://localhost:${debuggingPort}`, defaultViewport: null }); // Store user data dir for cleanup (browser as any).__userDataDir = userDataDir; } catch (error) { console.error('Failed to connect to browser:', error); // Fall back to bundled browser browser = await puppeteer.launch({ executablePath: defaultBrowserPath ?? undefined, headless: headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-blink-features=AutomationControlled', '--disable-features=VizDisplayCompositor', '--disable-extensions', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-ipc-flooding-protection', '--disable-default-apps', '--disable-sync', '--disable-translate', '--hide-scrollbars', '--mute-audio', '--no-default-browser-check', '--no-pings', '--disable-web-security', '--disable-features=TranslateUI', '--disable-features=BlinkGenPropertyTrees', '--disable-client-side-phishing-detection' ].filter(Boolean) }); } } } else { // Use bundled browser browser = await puppeteer.launch({ headless: headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-blink-features=AutomationControlled', '--disable-features=VizDisplayCompositor', '--disable-extensions', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-ipc-flooding-protection', '--disable-default-apps', '--disable-sync', '--disable-translate', '--hide-scrollbars', '--mute-audio', '--no-default-browser-check', '--no-pings', '--disable-web-security', '--disable-features=TranslateUI', '--disable-features=BlinkGenPropertyTrees', '--disable-client-side-phishing-detection', headless ? '--disable-gpu' : '' ].filter(Boolean) }); } } return browser; } // Get domain from URL for cookie storage function getDomainFromUrl(url: string): string { try { const urlObj = new URL(url); return urlObj.hostname.replace(/\./g, '_'); } catch { return 'unknown'; } } // Save cookies for a domain async function saveCookies(url: string, cookies: Cookie[]) { await ensureCookiesDir(); const domain = getDomainFromUrl(url); const cookiesPath = path.join(cookiesDir, `${domain}.json`); await fsPromises.writeFile(cookiesPath, JSON.stringify(cookies, null, 2)); } // Load cookies for a domain async function loadCookies(url: string): Promise<Cookie[]> { try { const domain = getDomainFromUrl(url); const cookiesPath = path.join(cookiesDir, `${domain}.json`); const cookiesData = await fsPromises.readFile(cookiesPath, 'utf-8'); return JSON.parse(cookiesData); } catch { return []; } } // Function to clean up resources async function cleanupBrowser() { if (browser) { // Clean up user data directory if it exists (for default browser) const userDataDir = (browser as any).__userDataDir; try { await browser.close(); } catch (error) { console.error('Error closing browser:', error); } // Clean up user data directory if it exists if (userDataDir) { try { await fsPromises.rm(userDataDir, { recursive: true, force: true }); } catch (error) { console.error('Error cleaning up user data directory:', error); } } browser = null; persistentPage = null; } } // Cleanup browser on exit process.on('exit', () => { if (browser) { // Can't use async here, so just do a sync cleanup of what we can try { browser.close().catch(() => {}); } catch (e) { // Ignore errors on exit } } }); process.on('SIGINT', async () => { await cleanupBrowser(); process.exit(0); }); process.on('SIGTERM', async () => { await cleanupBrowser(); process.exit(0); }); // Register the login-and-wait tool server.tool( "login-and-wait", "Opens a webpage in a visible browser window for manual login, waits for user to complete login, then saves cookies", { url: z.string().url().describe("The URL of the login page"), waitMinutes: z.number().optional().default(3).describe("Maximum minutes to wait for login (default: 3)"), successIndicator: z.string().optional().describe("Optional CSS selector or URL pattern that indicates successful login"), useDefaultBrowser: z.boolean().optional().default(true).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium") }, async ({ url, waitMinutes, successIndicator, useDefaultBrowser }) => { let page: Page | null = null; try { // Initialize browser in non-headless mode with default browser option const browserInstance = await initBrowser(false, useDefaultBrowser); // Create or reuse persistent page if (!persistentPage || persistentPage.isClosed()) { persistentPage = await browserInstance.newPage(); } page = persistentPage; // Load existing cookies if available const existingCookies = await loadCookies(url); if (existingCookies.length > 0) { await page.setCookie(...existingCookies); } // Set user agent and anti-detection measures for login await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); // Additional anti-detection measures for Google login await page.evaluateOnNewDocument(() => { // Remove webdriver property delete (window.navigator as any).webdriver; // Override the plugins property to add fake plugins Object.defineProperty(window.navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); // Override the languages property Object.defineProperty(window.navigator, 'languages', { get: () => ['en-US', 'en'] }); // Override permissions Object.defineProperty(window.navigator, 'permissions', { get: () => ({ query: () => Promise.resolve({ state: 'granted' }) }) }); }); // Navigate to the URL await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); const startTime = Date.now(); const maxWaitTime = waitMinutes * 60 * 1000; // Wait for login console.error(`Waiting for manual login... (up to ${waitMinutes} minutes)`); console.error(`Please complete the login in the ${useDefaultBrowser ? 'default' : 'Puppeteer'} browser window.`); console.error(`To continue immediately after login, use the 'signal-login-complete' tool or navigate away from the login page.`); if (successIndicator) { try { // If it's a URL pattern if (successIndicator.startsWith('http') || successIndicator.includes('/')) { await page.waitForFunction( (pattern) => window.location.href.includes(pattern), { timeout: maxWaitTime }, successIndicator ); } else { // Otherwise treat as CSS selector await page.waitForSelector(successIndicator, { timeout: maxWaitTime }); } } catch (timeoutError) { // Continue even if indicator not found console.error('Success indicator not found, but continuing...'); } } else { // Wait for user confirmation via multiple methods await new Promise((resolve) => { const checkInterval = setInterval(() => { if (Date.now() - startTime > maxWaitTime) { clearInterval(checkInterval); resolve(null); } }, 1000); // Method 1: Page navigation detection page?.on('framenavigated', () => { const currentUrl = page?.url() || ''; // Check if we've navigated away from login pages if (!currentUrl.includes('accounts.google.com') && !currentUrl.includes('login') && !currentUrl.includes('signin') && !currentUrl.includes('auth')) { setTimeout(() => { clearInterval(checkInterval); resolve(null); }, 2000); } }); // Method 2: Check for a completion marker file const completionFile = path.join(os.tmpdir(), 'mcp-login-complete.txt'); const fileCheckInterval = setInterval(async () => { try { if (fs.existsSync(completionFile)) { await fsPromises.unlink(completionFile).catch(() => {}); clearInterval(checkInterval); clearInterval(fileCheckInterval); resolve(null); } } catch (e) { // Ignore file check errors } }, 1000); // Clean up file checker when main interval ends setTimeout(() => { clearInterval(fileCheckInterval); }, maxWaitTime); }); } // Save cookies after login const cookies = await page.cookies(); await saveCookies(url, cookies); const finalUrl = page.url(); const browserType = useDefaultBrowser ? 'default browser' : 'Puppeteer browser'; return { content: [ { type: "text", text: `Login session established and cookies saved!\n\nBrowser: ${browserType}\nInitial URL: ${url}\nFinal URL: ${finalUrl}\nCookies saved: ${cookies.length}\n\nLogin completed via: ${successIndicator ? 'success indicator detected' : 'automatic navigation detection or manual signal'}\n\nThe browser window will remain open for future screenshots.` } ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isError: true, content: [ { type: "text", text: `Error during login process: ${errorMessage}`, }, ], }; } // Don't close the page - keep it for future use } ); // Updated screenshot-page tool with authentication support server.tool( "screenshot-page", "Captures a screenshot of a given URL and returns it as base64 encoded image. Can use saved cookies from login-and-wait.", { url: z.string().url().describe("The URL of the webpage to screenshot"), fullPage: z.boolean().optional().default(true).describe("Whether to capture the full page or just the viewport"), width: z.number().optional().default(1920).describe("Viewport width in pixels"), height: z.number().optional().default(1080).describe("Viewport height in pixels"), format: z.enum(['png', 'jpeg', 'webp']).optional().default('png').describe("Image format for the screenshot"), quality: z.number().min(0).max(100).optional().describe("Quality of the image (0-100), only applicable for jpeg and webp"), waitFor: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().default('networkidle2').describe("When to consider the page loaded"), delay: z.number().optional().default(0).describe("Additional delay in milliseconds to wait after page load"), useSavedAuth: z.boolean().optional().default(true).describe("Whether to use saved cookies from previous login"), reuseAuthPage: z.boolean().optional().default(false).describe("Whether to use the existing authenticated page instead of creating a new one"), useDefaultBrowser: z.boolean().optional().default(false).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium"), visibleBrowser: z.boolean().optional().default(false).describe("Whether to show the browser window (non-headless mode)") }, async ({ url, fullPage, width, height, format, quality, waitFor, delay, useSavedAuth, reuseAuthPage, useDefaultBrowser, visibleBrowser }) => { let page: Page | null = null; let shouldClosePage = true; try { // Initialize browser with appropriate options const isHeadless = !visibleBrowser; const browserInstance = await initBrowser(isHeadless, useDefaultBrowser && visibleBrowser); // Check if we should reuse the authenticated page if (reuseAuthPage && persistentPage && !persistentPage.isClosed()) { page = persistentPage; shouldClosePage = false; // Navigate to the new URL if different const currentUrl = page.url(); if (currentUrl !== url) { await page.goto(url, { waitUntil: waitFor as any, timeout: 30000 }); } } else { // Create a new page page = await browserInstance.newPage(); // Load saved cookies if requested if (useSavedAuth) { const cookies = await loadCookies(url); if (cookies.length > 0) { await page.setCookie(...cookies); } } // Set viewport await page.setViewport({ width, height }); // Set user agent to avoid bot detection await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); // Additional anti-detection measures for Google await page.evaluateOnNewDocument(() => { // Remove webdriver property delete (window.navigator as any).webdriver; // Override the plugins property to add fake plugins Object.defineProperty(window.navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); // Override the languages property Object.defineProperty(window.navigator, 'languages', { get: () => ['en-US', 'en'] }); // Override permissions Object.defineProperty(window.navigator, 'permissions', { get: () => ({ query: () => Promise.resolve({ state: 'granted' }) }) }); }); // Navigate to the URL await page.goto(url, { waitUntil: waitFor as any, timeout: 30000 }); } // Optional delay if (delay > 0) { await new Promise(resolve => setTimeout(resolve, delay)); } // Prepare screenshot options const screenshotOptions: any = { encoding: 'base64', fullPage, type: format }; // Add quality option for jpeg and webp if ((format === 'jpeg' || format === 'webp') && quality !== undefined) { screenshotOptions.quality = quality; } // Take screenshot const screenshot = await page.screenshot(screenshotOptions) as string; // Get page title and final URL for context const pageTitle = await page.title(); const finalUrl = page.url(); // If using a new page, save any new cookies if (!reuseAuthPage && useSavedAuth) { const currentCookies = await page.cookies(); if (currentCookies.length > 0) { await saveCookies(url, currentCookies); } } // Determine browser type for response const browserType = useDefaultBrowser && visibleBrowser ? 'default browser' : 'Puppeteer browser'; const browserMode = visibleBrowser ? 'visible' : 'headless'; return { content: [ { type: "text", text: `Screenshot captured successfully!\n\nBrowser: ${browserType} (${browserMode})\nPage Title: ${pageTitle}\nFinal URL: ${finalUrl}\nFormat: ${format}\nDimensions: ${width}x${height}\nFull Page: ${fullPage}\nUsed saved auth: ${useSavedAuth}\nReused auth page: ${reuseAuthPage}` }, { type: "image", data: screenshot, mimeType: `image/${format}` } ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isError: true, content: [ { type: "text", text: `Error capturing screenshot: ${errorMessage}`, }, ], }; } finally { // Only close the page if it's not the persistent one or if we should close it if (page && shouldClosePage && page !== persistentPage) { await page.close().catch(() => {}); } } } ); // Tool to signal login completion server.tool( "signal-login-complete", "Signals that manual login is complete and the login-and-wait tool should continue", {}, async () => { try { const completionFile = path.join(os.tmpdir(), 'mcp-login-complete.txt'); await fsPromises.writeFile(completionFile, 'complete'); return { content: [ { type: "text", text: "Login completion signal sent! The login-and-wait tool should continue shortly." } ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isError: true, content: [ { type: "text", text: `Error signaling login completion: ${errorMessage}`, }, ], }; } } ); // Tool to clear saved cookies server.tool( "clear-auth-cookies", "Clears saved authentication cookies for a specific domain or all domains", { url: z.string().url().optional().describe("URL of the domain to clear cookies for. If not provided, clears all cookies."), }, async ({ url }) => { try { await ensureCookiesDir(); if (url) { // Clear cookies for specific domain const domain = getDomainFromUrl(url); const cookiesPath = path.join(cookiesDir, `${domain}.json`); try { await fsPromises.unlink(cookiesPath); return { content: [ { type: "text", text: `Cookies cleared for domain: ${domain}` } ], }; } catch { return { content: [ { type: "text", text: `No cookies found for domain: ${domain}` } ], }; } } else { // Clear all cookies const files = await fsPromises.readdir(cookiesDir); for (const file of files) { if (file.endsWith('.json')) { await fsPromises.unlink(path.join(cookiesDir, file)); } } return { content: [ { type: "text", text: `All saved cookies cleared (${files.length} domains)` } ], }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isError: true, content: [ { type: "text", text: `Error clearing cookies: ${errorMessage}`, }, ], }; } } ); // Keep the screenshot-element tool as before, but add default browser support server.tool( "screenshot-element", "Captures a screenshot of a specific element on a webpage using a CSS selector", { url: z.string().url().describe("The URL of the webpage"), selector: z.string().describe("CSS selector for the element to screenshot"), waitForSelector: z.boolean().optional().default(true).describe("Whether to wait for the selector to appear"), format: z.enum(['png', 'jpeg', 'webp']).optional().default('png').describe("Image format for the screenshot"), quality: z.number().min(0).max(100).optional().describe("Quality of the image (0-100), only applicable for jpeg and webp"), padding: z.number().optional().default(0).describe("Padding around the element in pixels"), useSavedAuth: z.boolean().optional().default(true).describe("Whether to use saved cookies from previous login"), useDefaultBrowser: z.boolean().optional().default(false).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium"), visibleBrowser: z.boolean().optional().default(false).describe("Whether to show the browser window (non-headless mode)") }, async ({ url, selector, waitForSelector, format, quality, padding, useSavedAuth, useDefaultBrowser, visibleBrowser }) => { let page: Page | null = null; try { // Initialize browser with appropriate options const isHeadless = !visibleBrowser; const browserInstance = await initBrowser(isHeadless, useDefaultBrowser && visibleBrowser); // Create a new page page = await browserInstance.newPage(); // Load saved cookies if requested if (useSavedAuth) { const cookies = await loadCookies(url); if (cookies.length > 0) { await page.setCookie(...cookies); } } // Set viewport (matching screenshot-page tool) await page.setViewport({ width: 1920, height: 1080 }); // Set user agent to avoid bot detection await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); // Additional anti-detection measures for Google await page.evaluateOnNewDocument(() => { // Remove webdriver property delete (window.navigator as any).webdriver; // Override the plugins property to add fake plugins Object.defineProperty(window.navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); // Override the languages property Object.defineProperty(window.navigator, 'languages', { get: () => ['en-US', 'en'] }); // Override permissions Object.defineProperty(window.navigator, 'permissions', { get: () => ({ query: () => Promise.resolve({ state: 'granted' }) }) }); }); // Navigate to the URL await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); // Wait for the selector if requested if (waitForSelector) { await page.waitForSelector(selector, { timeout: 10000 }); } // Get the element const element = await page.$(selector); if (!element) { return { isError: true, content: [ { type: "text", text: `Element not found with selector: ${selector}`, }, ], }; } // Add padding if requested if (padding > 0) { await page.evaluate((sel, pad) => { const el = document.querySelector(sel); if (el) { (el as HTMLElement).style.padding = `${pad}px`; } }, selector, padding); } // Prepare screenshot options const screenshotOptions: any = { encoding: 'base64', type: format }; // Add quality option for jpeg and webp if ((format === 'jpeg' || format === 'webp') && quality !== undefined) { screenshotOptions.quality = quality; } // Take screenshot of the element const screenshot = await element.screenshot(screenshotOptions) as string; // Determine browser type for response const browserType = useDefaultBrowser && visibleBrowser ? 'default browser' : 'Puppeteer browser'; const browserMode = visibleBrowser ? 'visible' : 'headless'; return { content: [ { type: "text", text: `Element screenshot captured successfully!\n\nBrowser: ${browserType} (${browserMode})\nURL: ${url}\nSelector: ${selector}\nFormat: ${format}` }, { type: "image", data: screenshot, mimeType: `image/${format}` } ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { isError: true, content: [ { type: "text", text: `Error capturing element screenshot: ${errorMessage}`, }, ], }; } finally { // Close the page if (page) { await page.close().catch(() => {}); } } } ); // Run the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Screenshot MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

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/ananddtyagi/webpage-screenshot-mcp'

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