# Directory Structure ``` ├── .claude │ └── commands │ └── claude-desktop-extension.md ├── .gitignore ├── LICENSE ├── manifest.json ├── package-lock.json ├── package.json ├── README.md ├── screenshot-webpage-mcp.dxt ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | build/ 7 | *.tsbuildinfo 8 | 9 | # Logs 10 | logs/ 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids/ 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov/ 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage/ 27 | 28 | # nyc test coverage 29 | .nyc_output/ 30 | 31 | # Dependency directories 32 | .npm/ 33 | .eslintcache 34 | 35 | # Optional npm cache directory 36 | .npm/ 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | # Output of 'npm pack' 45 | *.tgz 46 | 47 | # Yarn Integrity file 48 | .yarn-integrity 49 | 50 | # dotenv environment variable files 51 | .env 52 | .env.local 53 | .env.development.local 54 | .env.test.local 55 | .env.production.local 56 | 57 | # IDE and editor files 58 | .idea/ 59 | .vscode/ 60 | *.swp 61 | *.swo 62 | .DS_Store 63 | .AppleDouble 64 | .LSOverride 65 | 66 | # User-specific files 67 | npm-debug.log 68 | yarn-error.log 69 | .DS_Store 70 | 71 | # Project specific 72 | .mcp-screenshot-cookies/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Webpage Screenshot MCP Server 2 | 3 | An MCP (Model Context Protocol) server that captures screenshots of web pages using Puppeteer. This server allows AI agents to visually verify web applications and see their progress when generating web apps. 4 | 5 |  6 | 7 | 8 | ## Features 9 | 10 | - **Full page screenshots**: Capture entire web pages or just the viewport 11 | - **Element screenshots**: Target specific elements using CSS selectors 12 | - **Multiple formats**: Support for PNG, JPEG, and WebP formats 13 | - **Customizable options**: Set viewport size, image quality, wait conditions, and delays 14 | - **Base64 encoding**: Returns screenshots as base64 encoded images for easy integration 15 | - **Authentication support**: Manual login and cookie persistence 16 | - **Default browser integration**: Use your system's default browser for a more natural experience 17 | - **Session persistence**: Keep browser sessions open for multi-step workflows 18 | 19 | ## Installation 20 | 21 | ### Quick Start (Claude Desktop Extension) 22 | 23 | Drag and drop the generated `screenshot-webpage-mcp.dxt` file into Claude Desktop for automatic installation! 24 | 25 | ### Manual Installation 26 | 27 | To install and build the MCP from source: 28 | 29 | ```bash 30 | # Clone the repository (if you haven't already) 31 | git clone https://github.com/ananddtyagi/webpage-screenshot-mcp.git 32 | cd webpage-screenshot-mcp 33 | 34 | # Install dependencies 35 | npm install 36 | 37 | # Build the project 38 | npm run build 39 | ``` 40 | 41 | The MCP server is built using TypeScript and compiled to JavaScript. The `dist` folder contains the compiled JavaScript files. 42 | 43 | ### Adding to Claude or Cursor 44 | 45 | To add this MCP to Claude Desktop or Cursor: 46 | 47 | 1. **Claude Desktop**: 48 | - Go to Settings > Developer 49 | - Click "Edit Config" 50 | - Add the following: 51 | 52 | ```json 53 | "webpage-screenshot": { 54 | "command": "node", 55 | "args": [ 56 | "~/path/to/webpage-screenshot-mcp/dist/index.js" 57 | ] 58 | } 59 | ``` 60 | - Save and reload Claude 61 | 62 | 2. **Cursor**: 63 | - Open Cursor and go to Cursor Settings > MCP 64 | - Click "Add new global MCP server" 65 | - Add the following: 66 | 67 | ```json 68 | "webpage-screenshot": { 69 | "command": "node", 70 | "args": ["~/path/to/webpage-screenshot-mcp/dist/index.js"] 71 | } 72 | ``` 73 | 74 | - Save and reload Cursor 75 | 76 | ## Usage 77 | 78 | ### Tools 79 | 80 | This MCP server provides several tools: 81 | 82 | #### 1. login-and-wait 83 | 84 | Opens a webpage in a visible browser window for manual login, waits for user to complete login, then saves cookies. 85 | 86 | ```json 87 | { 88 | "url": "https://example.com/login", 89 | "waitMinutes": 5, 90 | "successIndicator": ".dashboard-welcome", 91 | "useDefaultBrowser": true 92 | } 93 | ``` 94 | 95 | - `url` (required): The URL of the login page 96 | - `waitMinutes` (optional): Maximum minutes to wait for login (default: 5) 97 | - `successIndicator` (optional): CSS selector or URL pattern that indicates successful login 98 | - `useDefaultBrowser` (optional): Whether to use the system's default browser (default: true) 99 | 100 | #### 2. screenshot-page 101 | 102 | Captures a screenshot of a given URL and returns it as base64 encoded image. 103 | 104 | ```json 105 | { 106 | "url": "https://example.com/dashboard", 107 | "fullPage": true, 108 | "width": 1920, 109 | "height": 1080, 110 | "format": "png", 111 | "quality": 80, 112 | "waitFor": "networkidle2", 113 | "delay": 500, 114 | "useSavedAuth": true, 115 | "reuseAuthPage": true, 116 | "useDefaultBrowser": true, 117 | "visibleBrowser": true 118 | } 119 | ``` 120 | 121 | - `url` (required): The URL of the webpage to screenshot 122 | - `fullPage` (optional): Whether to capture the full page or just the viewport (default: true) 123 | - `width` (optional): Viewport width in pixels (default: 1920) 124 | - `height` (optional): Viewport height in pixels (default: 1080) 125 | - `format` (optional): Image format - "png", "jpeg", or "webp" (default: "png") 126 | - `quality` (optional): Quality of the image (0-100), only applicable for jpeg and webp 127 | - `waitFor` (optional): When to consider page loaded - "load", "domcontentloaded", "networkidle0", or "networkidle2" (default: "networkidle2") 128 | - `delay` (optional): Additional delay in milliseconds after page load (default: 0) 129 | - `useSavedAuth` (optional): Whether to use saved cookies from previous login (default: true) 130 | - `reuseAuthPage` (optional): Whether to use the existing authenticated page (default: false) 131 | - `useDefaultBrowser` (optional): Whether to use the system's default browser (default: false) 132 | - `visibleBrowser` (optional): Whether to show the browser window (default: false) 133 | 134 | #### 3. screenshot-element 135 | 136 | Captures a screenshot of a specific element on a webpage using a CSS selector. 137 | 138 | ```json 139 | { 140 | "url": "https://example.com/dashboard", 141 | "selector": ".user-profile", 142 | "waitForSelector": true, 143 | "format": "png", 144 | "quality": 80, 145 | "padding": 10, 146 | "useSavedAuth": true, 147 | "useDefaultBrowser": true, 148 | "visibleBrowser": true 149 | } 150 | ``` 151 | 152 | - `url` (required): The URL of the webpage 153 | - `selector` (required): CSS selector for the element to screenshot 154 | - `waitForSelector` (optional): Whether to wait for the selector to appear (default: true) 155 | - `format` (optional): Image format - "png", "jpeg", or "webp" (default: "png") 156 | - `quality` (optional): Quality of the image (0-100), only applicable for jpeg and webp 157 | - `padding` (optional): Padding around the element in pixels (default: 0) 158 | - `useSavedAuth` (optional): Whether to use saved cookies from previous login (default: true) 159 | - `useDefaultBrowser` (optional): Whether to use the system's default browser (default: false) 160 | - `visibleBrowser` (optional): Whether to show the browser window (default: false) 161 | 162 | #### 4. clear-auth-cookies 163 | 164 | Clears saved authentication cookies for a specific domain or all domains. 165 | 166 | ```json 167 | { 168 | "url": "https://example.com" 169 | } 170 | ``` 171 | 172 | - `url` (optional): URL of the domain to clear cookies for. If not provided, clears all cookies. 173 | 174 | ## Default Browser Mode 175 | 176 | The default browser mode allows you to use your system's regular browser (Chrome, Edge, etc.) instead of Puppeteer's bundled Chromium. This is useful for: 177 | 178 | 1. Using your existing browser sessions and extensions 179 | 2. Manually logging in to websites with your saved credentials 180 | 3. Having a more natural browsing experience for multi-step workflows 181 | 4. Testing with the same browser environment as your users 182 | 183 | To enable default browser mode, set `useDefaultBrowser: true` and `visibleBrowser: true` in your tool parameters. 184 | 185 | ### How Default Browser Mode Works 186 | 187 | When you enable default browser mode: 188 | 189 | 1. The tool will attempt to locate your system's default browser (Chrome, Edge, etc.) 190 | 2. It launches your browser with remote debugging enabled on a random port 191 | 3. Puppeteer connects to this browser instance instead of launching its own 192 | 4. Your existing profiles, extensions, and cookies are available during the session 193 | 5. The browser window remains visible so you can interact with it manually 194 | 195 | This mode is particularly useful for workflows that require authentication or complex user interactions. 196 | 197 | ## Browser Persistence 198 | 199 | The MCP server can maintain a persistent browser session across multiple tool calls: 200 | 201 | 1. When you use `login-and-wait`, the browser session is kept open 202 | 2. Subsequent calls to `screenshot-page` or `screenshot-element` with `reuseAuthPage: true` will use the same page 203 | 3. This allows for multi-step workflows without having to re-authenticate 204 | 205 | ## Cookie Management 206 | 207 | Cookies are automatically saved for each domain you visit: 208 | 209 | 1. After using `login-and-wait`, cookies are saved to the `.mcp-screenshot-cookies` directory in your home folder 210 | 2. These cookies are automatically loaded when visiting the same domain again with `useSavedAuth: true` 211 | 3. You can clear cookies using the `clear-auth-cookies` tool 212 | 213 | ## Example Workflow: Protected Page Screenshots 214 | 215 | Here's an example workflow for taking screenshots of pages that require authentication: 216 | 217 | 1. **Manual Login Phase** 218 | 219 | ```json 220 | { 221 | "name": "login-and-wait", 222 | "parameters": { 223 | "url": "https://example.com/login", 224 | "waitMinutes": 3, 225 | "successIndicator": ".dashboard-welcome", 226 | "useDefaultBrowser": true 227 | } 228 | } 229 | ``` 230 | 231 | This will open your default browser with the login page. You can manually log in, and once complete (either by detecting the success indicator or after navigating away from the login page), the session cookies will be saved. 232 | 233 | 2. **Take Screenshots Using Saved Session** 234 | 235 | ```json 236 | { 237 | "name": "screenshot-page", 238 | "parameters": { 239 | "url": "https://example.com/account", 240 | "fullPage": true, 241 | "useSavedAuth": true, 242 | "reuseAuthPage": true, 243 | "useDefaultBrowser": true, 244 | "visibleBrowser": true 245 | } 246 | } 247 | ``` 248 | 249 | This will take a screenshot of the account page using your saved authentication cookies in the same browser window. 250 | 251 | 3. **Take Screenshots of Specific Elements** 252 | 253 | ```json 254 | { 255 | "name": "screenshot-element", 256 | "parameters": { 257 | "url": "https://example.com/dashboard", 258 | "selector": ".user-profile-section", 259 | "useSavedAuth": true, 260 | "useDefaultBrowser": true, 261 | "visibleBrowser": true 262 | } 263 | } 264 | ``` 265 | 266 | 4. **Clear Cookies When Done** 267 | 268 | ```json 269 | { 270 | "name": "clear-auth-cookies", 271 | "parameters": { 272 | "url": "https://example.com" 273 | } 274 | } 275 | ``` 276 | 277 | This workflow allows you to interact with protected pages as if you were a regular user, completing the full authentication flow in your default browser. 278 | 279 | ## Headless vs. Visible Mode 280 | 281 | - **Headless mode** (`visibleBrowser: false`): Faster and more suitable for automated workflows where no user interaction is needed. 282 | - **Visible mode** (`visibleBrowser: true`): Shows the browser window, allowing for user interaction and manual verification. Required for `useDefaultBrowser: true`. 283 | 284 | ## Platform Support 285 | 286 | The default browser detection works on: 287 | 288 | - **macOS**: Detects Chrome, Edge, and Safari 289 | - **Windows**: Detects Chrome and Edge via registry or common installation paths 290 | - **Linux**: Detects Chrome and Chromium via system commands 291 | 292 | ## Troubleshooting 293 | 294 | ### Common Issues 295 | 296 | 1. **Default browser not found**: If the system can't find your default browser, it will fall back to Puppeteer's bundled Chromium. 297 | 2. **Connection issues**: If there are problems connecting to the browser's debugging port, check if another instance is already using that port. 298 | 3. **Cookie issues**: If authentication isn't working, try clearing cookies with the `clear-auth-cookies` tool. 299 | 300 | ### Debugging 301 | 302 | The MCP server logs helpful error messages to the console when issues occur. Check these messages for troubleshooting information. 303 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "dist"] 18 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "webpage-screenshot-mcp", 3 | "version": "1.0.0", 4 | "description": "MCP server for capturing screenshots of web pages", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js" 10 | }, 11 | "keywords": [ 12 | "mcp", 13 | "screenshot", 14 | "puppeteer", 15 | "web-scraping" 16 | ], 17 | "author": "Anand Tyagi", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "^1.0.0", 21 | "puppeteer": "24.9.0", 22 | "zod": "^3.22.0" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.0.0", 26 | "typescript": "^5.0.0" 27 | }, 28 | "files": [ 29 | "dist/**/*", 30 | "README.md", 31 | "package.json" 32 | ] 33 | } ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "0.1", 3 | "name": "webpage-screenshot-mcp", 4 | "display_name": "Webpage Screenshot MCP Server", 5 | "version": "1.0.0", 6 | "description": "MCP server for capturing screenshots of web pages using Puppeteer", 7 | "long_description": "This MCP server allows AI agents to capture screenshots of web pages, with support for full page captures, element screenshots, authentication handling, and multiple image formats.", 8 | "author": { 9 | "name": "Anand Tyagi" 10 | }, 11 | "license": "MIT", 12 | "server": { 13 | "type": "node", 14 | "entry_point": "dist/index.js", 15 | "mcp_config": { 16 | "command": "node", 17 | "args": ["${__dirname}/dist/index.js"] 18 | } 19 | }, 20 | "tools": [ 21 | { 22 | "name": "screenshot-page", 23 | "description": "Captures a screenshot of a given URL" 24 | }, 25 | { 26 | "name": "screenshot-element", 27 | "description": "Captures a screenshot of a specific element on a webpage" 28 | }, 29 | { 30 | "name": "login-and-wait", 31 | "description": "Opens a webpage for manual login and saves cookies" 32 | }, 33 | { 34 | "name": "clear-auth-cookies", 35 | "description": "Clears saved authentication cookies" 36 | } 37 | ], 38 | "homepage": "https://github.com/ananddtyagi/webpage-screenshot-mcp", 39 | "keywords": ["mcp", "screenshot", "puppeteer", "web-scraping"] 40 | } ``` -------------------------------------------------------------------------------- /.claude/commands/claude-desktop-extension.md: -------------------------------------------------------------------------------- ```markdown 1 | I want to build this as a Desktop Extension, abbreviated as "DXT". Please follow these steps: 2 | 3 | 1. **Read the specifications thoroughly:** 4 | - https://github.com/anthropics/dxt/blob/main/README.md - DXT architecture overview, capabilities, and integration patterns 5 | - https://github.com/anthropics/dxt/blob/main/MANIFEST.md - Complete extension manifest structure and field definitions 6 | - https://github.com/anthropics/dxt/tree/main/examples - Reference implementations including a "Hello World" example 7 | 8 | 2. **Create a proper extension structure:** 9 | - Generate a valid manifest.json following the MANIFEST.md spec 10 | - Implement an MCP server using @modelcontextprotocol/sdk with proper tool definitions 11 | - Include proper error handling and timeout management 12 | 13 | 3. **Follow best development practices:** 14 | - Implement proper MCP protocol communication via stdio transport 15 | - Structure tools with clear schemas, validation, and consistent JSON responses 16 | - Make use of the fact that this extension will be running locally 17 | - Add appropriate logging and debugging capabilities 18 | - Include proper documentation and setup instructions 19 | 20 | 4. **Test considerations:** 21 | - Validate that all tool calls return properly structured responses 22 | - Verify manifest loads correctly and host integration works 23 | 24 | Generate complete, production-ready code that can be immediately tested. Focus on defensive programming, clear error messages, and following the exact 25 | DXT specifications to ensure compatibility with the ecosystem. ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { execSync, spawn } from 'child_process'; 5 | import fs, { promises as fsPromises } from 'fs'; 6 | import os from 'os'; 7 | import path from 'path'; 8 | import puppeteer, { Browser, Cookie, Page } from 'puppeteer'; 9 | import { z } from 'zod'; 10 | 11 | // Create the MCP server 12 | const server = new McpServer({ 13 | name: "screenshot-page", 14 | version: "1.0.0", 15 | }); 16 | 17 | let browser: Browser | null = null; 18 | let persistentPage: Page | null = null; 19 | const cookiesDir = path.join(os.homedir(), '.mcp-screenshot-cookies'); 20 | 21 | // Ensure cookies directory exists 22 | async function ensureCookiesDir() { 23 | try { 24 | await fsPromises.mkdir(cookiesDir, { recursive: true }); 25 | } catch (error) { 26 | console.error('Error creating cookies directory:', error); 27 | } 28 | } 29 | 30 | // Get path to default Chrome/Edge installation 31 | function getDefaultBrowserPath(): string | null { 32 | try { 33 | if (process.platform === 'darwin') { 34 | // Check for Chrome first 35 | try { 36 | return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; 37 | } catch (e) { 38 | // Then check for Edge 39 | try { 40 | return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'; 41 | } catch (e) { 42 | // Then Safari (though Puppeteer doesn't work well with Safari) 43 | return '/Applications/Safari.app/Contents/MacOS/Safari'; 44 | } 45 | } 46 | } else if (process.platform === 'win32') { 47 | // On Windows, try to find Chrome or Edge 48 | try { 49 | const chromePath = execSync('where chrome').toString().trim(); 50 | if (chromePath) return chromePath; 51 | } catch (e) { 52 | try { 53 | const edgePath = execSync('where msedge').toString().trim(); 54 | if (edgePath) return edgePath; 55 | } catch (e) { 56 | // Fall back to default installation paths 57 | const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files'; 58 | const chromePath = `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`; 59 | const edgePath = `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`; 60 | 61 | try { 62 | if (fs.existsSync(chromePath)) return chromePath; 63 | if (fs.existsSync(edgePath)) return edgePath; 64 | } catch (e) { 65 | // Ignore filesystem errors 66 | } 67 | } 68 | } 69 | } else if (process.platform === 'linux') { 70 | // On Linux, try common browser paths 71 | try { 72 | const chromePath = execSync('which google-chrome').toString().trim(); 73 | if (chromePath) return chromePath; 74 | } catch (e) { 75 | try { 76 | const chromiumPath = execSync('which chromium-browser').toString().trim(); 77 | if (chromiumPath) return chromiumPath; 78 | } catch (e) { 79 | // No default browser found 80 | } 81 | } 82 | } 83 | } catch (e) { 84 | console.error('Error finding default browser:', e); 85 | } 86 | return null; 87 | } 88 | 89 | // Initialize browser instance 90 | async function initBrowser(headless: boolean = true, useDefaultBrowser: boolean = false): Promise<Browser> { 91 | if (browser) { 92 | // Check if we need to switch modes or browser type 93 | const isHeadless = browser.process()?.spawnargs?.includes('--headless') ?? true; 94 | const isUsingDefaultBrowser = browser.process()?.spawnargs?.includes('--remote-debugging-port') ?? false; 95 | 96 | if (isHeadless !== headless || isUsingDefaultBrowser !== useDefaultBrowser) { 97 | await browser.close(); 98 | browser = null; 99 | persistentPage = null; 100 | } 101 | } 102 | 103 | if (!browser) { 104 | if (useDefaultBrowser && !headless) { 105 | // Try to connect to default browser 106 | const defaultBrowserPath = getDefaultBrowserPath(); 107 | 108 | if (!defaultBrowserPath) { 109 | console.error('Could not find default browser. Falling back to bundled Chromium.'); 110 | browser = await puppeteer.launch({ 111 | executablePath: defaultBrowserPath ?? undefined, 112 | headless: headless, 113 | args: [ 114 | '--no-sandbox', 115 | '--disable-setuid-sandbox', 116 | '--disable-dev-shm-usage', 117 | '--disable-accelerated-2d-canvas', 118 | '--no-first-run', 119 | '--no-zygote', 120 | '--disable-blink-features=AutomationControlled', 121 | '--disable-features=VizDisplayCompositor', 122 | '--disable-extensions-file-access-check', 123 | '--disable-extensions-http-throttling', 124 | '--disable-extensions-https-error-pages', 125 | '--disable-extensions', 126 | '--disable-background-timer-throttling', 127 | '--disable-renderer-backgrounding', 128 | '--disable-backgrounding-occluded-windows', 129 | '--disable-ipc-flooding-protection', 130 | '--disable-default-apps', 131 | '--disable-sync', 132 | '--disable-translate', 133 | '--hide-scrollbars', 134 | '--mute-audio', 135 | '--no-default-browser-check', 136 | '--no-pings', 137 | '--disable-web-security', 138 | '--disable-features=TranslateUI', 139 | '--disable-features=BlinkGenPropertyTrees', 140 | '--disable-client-side-phishing-detection', 141 | '--disable-component-extensions-with-background-pages', 142 | '--disable-default-apps', 143 | '--disable-hang-monitor', 144 | '--disable-prompt-on-repost', 145 | headless ? '--disable-gpu' : '' 146 | ].filter(Boolean) 147 | }); 148 | } else { 149 | // Use random debug port in allowed range (9222-9322) 150 | const debuggingPort = 9222 + Math.floor(Math.random() * 100); 151 | 152 | // Launch browser with debugging port 153 | const userDataDir = path.join(os.tmpdir(), `puppeteer_user_data_${Date.now()}`); 154 | 155 | // Launch browser process using spawn instead of execSync 156 | const browserProcess = spawn( 157 | defaultBrowserPath, 158 | [ 159 | `--remote-debugging-port=${debuggingPort}`, 160 | `--user-data-dir=${userDataDir}`, 161 | '--no-first-run', 162 | 'about:blank' 163 | ], 164 | { stdio: 'ignore', detached: true } 165 | ); 166 | 167 | // Detach the process so it continues running after our process exits 168 | browserProcess.unref(); 169 | 170 | // Wait for browser to start 171 | await new Promise(resolve => setTimeout(resolve, 1000)); 172 | 173 | // Connect to the browser 174 | try { 175 | browser = await puppeteer.connect({ 176 | browserURL: `http://localhost:${debuggingPort}`, 177 | defaultViewport: null 178 | }); 179 | 180 | // Store user data dir for cleanup 181 | (browser as any).__userDataDir = userDataDir; 182 | } catch (error) { 183 | console.error('Failed to connect to browser:', error); 184 | // Fall back to bundled browser 185 | browser = await puppeteer.launch({ 186 | executablePath: defaultBrowserPath ?? undefined, 187 | headless: headless, 188 | args: [ 189 | '--no-sandbox', 190 | '--disable-setuid-sandbox', 191 | '--disable-dev-shm-usage', 192 | '--disable-accelerated-2d-canvas', 193 | '--no-first-run', 194 | '--no-zygote', 195 | '--disable-blink-features=AutomationControlled', 196 | '--disable-features=VizDisplayCompositor', 197 | '--disable-extensions', 198 | '--disable-background-timer-throttling', 199 | '--disable-renderer-backgrounding', 200 | '--disable-backgrounding-occluded-windows', 201 | '--disable-ipc-flooding-protection', 202 | '--disable-default-apps', 203 | '--disable-sync', 204 | '--disable-translate', 205 | '--hide-scrollbars', 206 | '--mute-audio', 207 | '--no-default-browser-check', 208 | '--no-pings', 209 | '--disable-web-security', 210 | '--disable-features=TranslateUI', 211 | '--disable-features=BlinkGenPropertyTrees', 212 | '--disable-client-side-phishing-detection' 213 | ].filter(Boolean) 214 | }); 215 | } 216 | } 217 | } else { 218 | // Use bundled browser 219 | browser = await puppeteer.launch({ 220 | headless: headless, 221 | args: [ 222 | '--no-sandbox', 223 | '--disable-setuid-sandbox', 224 | '--disable-dev-shm-usage', 225 | '--disable-accelerated-2d-canvas', 226 | '--no-first-run', 227 | '--no-zygote', 228 | '--disable-blink-features=AutomationControlled', 229 | '--disable-features=VizDisplayCompositor', 230 | '--disable-extensions', 231 | '--disable-background-timer-throttling', 232 | '--disable-renderer-backgrounding', 233 | '--disable-backgrounding-occluded-windows', 234 | '--disable-ipc-flooding-protection', 235 | '--disable-default-apps', 236 | '--disable-sync', 237 | '--disable-translate', 238 | '--hide-scrollbars', 239 | '--mute-audio', 240 | '--no-default-browser-check', 241 | '--no-pings', 242 | '--disable-web-security', 243 | '--disable-features=TranslateUI', 244 | '--disable-features=BlinkGenPropertyTrees', 245 | '--disable-client-side-phishing-detection', 246 | headless ? '--disable-gpu' : '' 247 | ].filter(Boolean) 248 | }); 249 | } 250 | } 251 | return browser; 252 | } 253 | 254 | // Get domain from URL for cookie storage 255 | function getDomainFromUrl(url: string): string { 256 | try { 257 | const urlObj = new URL(url); 258 | return urlObj.hostname.replace(/\./g, '_'); 259 | } catch { 260 | return 'unknown'; 261 | } 262 | } 263 | 264 | // Save cookies for a domain 265 | async function saveCookies(url: string, cookies: Cookie[]) { 266 | await ensureCookiesDir(); 267 | const domain = getDomainFromUrl(url); 268 | const cookiesPath = path.join(cookiesDir, `${domain}.json`); 269 | await fsPromises.writeFile(cookiesPath, JSON.stringify(cookies, null, 2)); 270 | } 271 | 272 | // Load cookies for a domain 273 | async function loadCookies(url: string): Promise<Cookie[]> { 274 | try { 275 | const domain = getDomainFromUrl(url); 276 | const cookiesPath = path.join(cookiesDir, `${domain}.json`); 277 | const cookiesData = await fsPromises.readFile(cookiesPath, 'utf-8'); 278 | return JSON.parse(cookiesData); 279 | } catch { 280 | return []; 281 | } 282 | } 283 | 284 | // Function to clean up resources 285 | async function cleanupBrowser() { 286 | if (browser) { 287 | // Clean up user data directory if it exists (for default browser) 288 | const userDataDir = (browser as any).__userDataDir; 289 | 290 | try { 291 | await browser.close(); 292 | } catch (error) { 293 | console.error('Error closing browser:', error); 294 | } 295 | 296 | // Clean up user data directory if it exists 297 | if (userDataDir) { 298 | try { 299 | await fsPromises.rm(userDataDir, { recursive: true, force: true }); 300 | } catch (error) { 301 | console.error('Error cleaning up user data directory:', error); 302 | } 303 | } 304 | 305 | browser = null; 306 | persistentPage = null; 307 | } 308 | } 309 | 310 | // Cleanup browser on exit 311 | process.on('exit', () => { 312 | if (browser) { 313 | // Can't use async here, so just do a sync cleanup of what we can 314 | try { 315 | browser.close().catch(() => {}); 316 | } catch (e) { 317 | // Ignore errors on exit 318 | } 319 | } 320 | }); 321 | 322 | process.on('SIGINT', async () => { 323 | await cleanupBrowser(); 324 | process.exit(0); 325 | }); 326 | 327 | process.on('SIGTERM', async () => { 328 | await cleanupBrowser(); 329 | process.exit(0); 330 | }); 331 | 332 | // Register the login-and-wait tool 333 | server.tool( 334 | "login-and-wait", 335 | "Opens a webpage in a visible browser window for manual login, waits for user to complete login, then saves cookies", 336 | { 337 | url: z.string().url().describe("The URL of the login page"), 338 | waitMinutes: z.number().optional().default(3).describe("Maximum minutes to wait for login (default: 3)"), 339 | successIndicator: z.string().optional().describe("Optional CSS selector or URL pattern that indicates successful login"), 340 | useDefaultBrowser: z.boolean().optional().default(true).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium") 341 | }, 342 | async ({ url, waitMinutes, successIndicator, useDefaultBrowser }) => { 343 | let page: Page | null = null; 344 | 345 | try { 346 | // Initialize browser in non-headless mode with default browser option 347 | const browserInstance = await initBrowser(false, useDefaultBrowser); 348 | 349 | // Create or reuse persistent page 350 | if (!persistentPage || persistentPage.isClosed()) { 351 | persistentPage = await browserInstance.newPage(); 352 | } 353 | page = persistentPage; 354 | 355 | // Load existing cookies if available 356 | const existingCookies = await loadCookies(url); 357 | if (existingCookies.length > 0) { 358 | await page.setCookie(...existingCookies); 359 | } 360 | 361 | // Set user agent and anti-detection measures for login 362 | 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'); 363 | 364 | // Additional anti-detection measures for Google login 365 | await page.evaluateOnNewDocument(() => { 366 | // Remove webdriver property 367 | delete (window.navigator as any).webdriver; 368 | 369 | // Override the plugins property to add fake plugins 370 | Object.defineProperty(window.navigator, 'plugins', { 371 | get: () => [1, 2, 3, 4, 5] 372 | }); 373 | 374 | // Override the languages property 375 | Object.defineProperty(window.navigator, 'languages', { 376 | get: () => ['en-US', 'en'] 377 | }); 378 | 379 | // Override permissions 380 | Object.defineProperty(window.navigator, 'permissions', { 381 | get: () => ({ 382 | query: () => Promise.resolve({ state: 'granted' }) 383 | }) 384 | }); 385 | }); 386 | 387 | // Navigate to the URL 388 | await page.goto(url, { 389 | waitUntil: 'networkidle2', 390 | timeout: 30000 391 | }); 392 | 393 | const startTime = Date.now(); 394 | const maxWaitTime = waitMinutes * 60 * 1000; 395 | 396 | // Wait for login 397 | console.error(`Waiting for manual login... (up to ${waitMinutes} minutes)`); 398 | console.error(`Please complete the login in the ${useDefaultBrowser ? 'default' : 'Puppeteer'} browser window.`); 399 | console.error(`To continue immediately after login, use the 'signal-login-complete' tool or navigate away from the login page.`); 400 | 401 | if (successIndicator) { 402 | try { 403 | // If it's a URL pattern 404 | if (successIndicator.startsWith('http') || successIndicator.includes('/')) { 405 | await page.waitForFunction( 406 | (pattern) => window.location.href.includes(pattern), 407 | { timeout: maxWaitTime }, 408 | successIndicator 409 | ); 410 | } else { 411 | // Otherwise treat as CSS selector 412 | await page.waitForSelector(successIndicator, { timeout: maxWaitTime }); 413 | } 414 | } catch (timeoutError) { 415 | // Continue even if indicator not found 416 | console.error('Success indicator not found, but continuing...'); 417 | } 418 | } else { 419 | // Wait for user confirmation via multiple methods 420 | await new Promise((resolve) => { 421 | const checkInterval = setInterval(() => { 422 | if (Date.now() - startTime > maxWaitTime) { 423 | clearInterval(checkInterval); 424 | resolve(null); 425 | } 426 | }, 1000); 427 | 428 | // Method 1: Page navigation detection 429 | page?.on('framenavigated', () => { 430 | const currentUrl = page?.url() || ''; 431 | // Check if we've navigated away from login pages 432 | if (!currentUrl.includes('accounts.google.com') && 433 | !currentUrl.includes('login') && 434 | !currentUrl.includes('signin') && 435 | !currentUrl.includes('auth')) { 436 | setTimeout(() => { 437 | clearInterval(checkInterval); 438 | resolve(null); 439 | }, 2000); 440 | } 441 | }); 442 | 443 | // Method 2: Check for a completion marker file 444 | const completionFile = path.join(os.tmpdir(), 'mcp-login-complete.txt'); 445 | const fileCheckInterval = setInterval(async () => { 446 | try { 447 | if (fs.existsSync(completionFile)) { 448 | await fsPromises.unlink(completionFile).catch(() => {}); 449 | clearInterval(checkInterval); 450 | clearInterval(fileCheckInterval); 451 | resolve(null); 452 | } 453 | } catch (e) { 454 | // Ignore file check errors 455 | } 456 | }, 1000); 457 | 458 | // Clean up file checker when main interval ends 459 | setTimeout(() => { 460 | clearInterval(fileCheckInterval); 461 | }, maxWaitTime); 462 | }); 463 | } 464 | 465 | // Save cookies after login 466 | const cookies = await page.cookies(); 467 | await saveCookies(url, cookies); 468 | 469 | const finalUrl = page.url(); 470 | const browserType = useDefaultBrowser ? 'default browser' : 'Puppeteer browser'; 471 | 472 | return { 473 | content: [ 474 | { 475 | type: "text", 476 | 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.` 477 | } 478 | ], 479 | }; 480 | } catch (error) { 481 | const errorMessage = error instanceof Error ? error.message : String(error); 482 | return { 483 | isError: true, 484 | content: [ 485 | { 486 | type: "text", 487 | text: `Error during login process: ${errorMessage}`, 488 | }, 489 | ], 490 | }; 491 | } 492 | // Don't close the page - keep it for future use 493 | } 494 | ); 495 | 496 | // Updated screenshot-page tool with authentication support 497 | server.tool( 498 | "screenshot-page", 499 | "Captures a screenshot of a given URL and returns it as base64 encoded image. Can use saved cookies from login-and-wait.", 500 | { 501 | url: z.string().url().describe("The URL of the webpage to screenshot"), 502 | fullPage: z.boolean().optional().default(true).describe("Whether to capture the full page or just the viewport"), 503 | width: z.number().optional().default(1920).describe("Viewport width in pixels"), 504 | height: z.number().optional().default(1080).describe("Viewport height in pixels"), 505 | format: z.enum(['png', 'jpeg', 'webp']).optional().default('png').describe("Image format for the screenshot"), 506 | quality: z.number().min(0).max(100).optional().describe("Quality of the image (0-100), only applicable for jpeg and webp"), 507 | waitFor: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().default('networkidle2').describe("When to consider the page loaded"), 508 | delay: z.number().optional().default(0).describe("Additional delay in milliseconds to wait after page load"), 509 | useSavedAuth: z.boolean().optional().default(true).describe("Whether to use saved cookies from previous login"), 510 | reuseAuthPage: z.boolean().optional().default(false).describe("Whether to use the existing authenticated page instead of creating a new one"), 511 | useDefaultBrowser: z.boolean().optional().default(false).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium"), 512 | visibleBrowser: z.boolean().optional().default(false).describe("Whether to show the browser window (non-headless mode)") 513 | }, 514 | async ({ url, fullPage, width, height, format, quality, waitFor, delay, useSavedAuth, reuseAuthPage, useDefaultBrowser, visibleBrowser }) => { 515 | let page: Page | null = null; 516 | let shouldClosePage = true; 517 | 518 | try { 519 | // Initialize browser with appropriate options 520 | const isHeadless = !visibleBrowser; 521 | const browserInstance = await initBrowser(isHeadless, useDefaultBrowser && visibleBrowser); 522 | 523 | // Check if we should reuse the authenticated page 524 | if (reuseAuthPage && persistentPage && !persistentPage.isClosed()) { 525 | page = persistentPage; 526 | shouldClosePage = false; 527 | 528 | // Navigate to the new URL if different 529 | const currentUrl = page.url(); 530 | if (currentUrl !== url) { 531 | await page.goto(url, { 532 | waitUntil: waitFor as any, 533 | timeout: 30000 534 | }); 535 | } 536 | } else { 537 | // Create a new page 538 | page = await browserInstance.newPage(); 539 | 540 | // Load saved cookies if requested 541 | if (useSavedAuth) { 542 | const cookies = await loadCookies(url); 543 | if (cookies.length > 0) { 544 | await page.setCookie(...cookies); 545 | } 546 | } 547 | 548 | // Set viewport 549 | await page.setViewport({ width, height }); 550 | 551 | // Set user agent to avoid bot detection 552 | 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'); 553 | 554 | // Additional anti-detection measures for Google 555 | await page.evaluateOnNewDocument(() => { 556 | // Remove webdriver property 557 | delete (window.navigator as any).webdriver; 558 | 559 | // Override the plugins property to add fake plugins 560 | Object.defineProperty(window.navigator, 'plugins', { 561 | get: () => [1, 2, 3, 4, 5] 562 | }); 563 | 564 | // Override the languages property 565 | Object.defineProperty(window.navigator, 'languages', { 566 | get: () => ['en-US', 'en'] 567 | }); 568 | 569 | // Override permissions 570 | Object.defineProperty(window.navigator, 'permissions', { 571 | get: () => ({ 572 | query: () => Promise.resolve({ state: 'granted' }) 573 | }) 574 | }); 575 | }); 576 | 577 | // Navigate to the URL 578 | await page.goto(url, { 579 | waitUntil: waitFor as any, 580 | timeout: 30000 581 | }); 582 | } 583 | 584 | // Optional delay 585 | if (delay > 0) { 586 | await new Promise(resolve => setTimeout(resolve, delay)); 587 | } 588 | 589 | // Prepare screenshot options 590 | const screenshotOptions: any = { 591 | encoding: 'base64', 592 | fullPage, 593 | type: format 594 | }; 595 | 596 | // Add quality option for jpeg and webp 597 | if ((format === 'jpeg' || format === 'webp') && quality !== undefined) { 598 | screenshotOptions.quality = quality; 599 | } 600 | 601 | // Take screenshot 602 | const screenshot = await page.screenshot(screenshotOptions) as string; 603 | 604 | // Get page title and final URL for context 605 | const pageTitle = await page.title(); 606 | const finalUrl = page.url(); 607 | 608 | // If using a new page, save any new cookies 609 | if (!reuseAuthPage && useSavedAuth) { 610 | const currentCookies = await page.cookies(); 611 | if (currentCookies.length > 0) { 612 | await saveCookies(url, currentCookies); 613 | } 614 | } 615 | 616 | // Determine browser type for response 617 | const browserType = useDefaultBrowser && visibleBrowser ? 'default browser' : 'Puppeteer browser'; 618 | const browserMode = visibleBrowser ? 'visible' : 'headless'; 619 | 620 | return { 621 | content: [ 622 | { 623 | type: "text", 624 | 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}` 625 | }, 626 | { 627 | type: "image", 628 | data: screenshot, 629 | mimeType: `image/${format}` 630 | } 631 | ], 632 | }; 633 | } catch (error) { 634 | const errorMessage = error instanceof Error ? error.message : String(error); 635 | return { 636 | isError: true, 637 | content: [ 638 | { 639 | type: "text", 640 | text: `Error capturing screenshot: ${errorMessage}`, 641 | }, 642 | ], 643 | }; 644 | } finally { 645 | // Only close the page if it's not the persistent one or if we should close it 646 | if (page && shouldClosePage && page !== persistentPage) { 647 | await page.close().catch(() => {}); 648 | } 649 | } 650 | } 651 | ); 652 | 653 | // Tool to signal login completion 654 | server.tool( 655 | "signal-login-complete", 656 | "Signals that manual login is complete and the login-and-wait tool should continue", 657 | {}, 658 | async () => { 659 | try { 660 | const completionFile = path.join(os.tmpdir(), 'mcp-login-complete.txt'); 661 | await fsPromises.writeFile(completionFile, 'complete'); 662 | 663 | return { 664 | content: [ 665 | { 666 | type: "text", 667 | text: "Login completion signal sent! The login-and-wait tool should continue shortly." 668 | } 669 | ], 670 | }; 671 | } catch (error) { 672 | const errorMessage = error instanceof Error ? error.message : String(error); 673 | return { 674 | isError: true, 675 | content: [ 676 | { 677 | type: "text", 678 | text: `Error signaling login completion: ${errorMessage}`, 679 | }, 680 | ], 681 | }; 682 | } 683 | } 684 | ); 685 | 686 | // Tool to clear saved cookies 687 | server.tool( 688 | "clear-auth-cookies", 689 | "Clears saved authentication cookies for a specific domain or all domains", 690 | { 691 | url: z.string().url().optional().describe("URL of the domain to clear cookies for. If not provided, clears all cookies."), 692 | }, 693 | async ({ url }) => { 694 | try { 695 | await ensureCookiesDir(); 696 | 697 | if (url) { 698 | // Clear cookies for specific domain 699 | const domain = getDomainFromUrl(url); 700 | const cookiesPath = path.join(cookiesDir, `${domain}.json`); 701 | try { 702 | await fsPromises.unlink(cookiesPath); 703 | return { 704 | content: [ 705 | { 706 | type: "text", 707 | text: `Cookies cleared for domain: ${domain}` 708 | } 709 | ], 710 | }; 711 | } catch { 712 | return { 713 | content: [ 714 | { 715 | type: "text", 716 | text: `No cookies found for domain: ${domain}` 717 | } 718 | ], 719 | }; 720 | } 721 | } else { 722 | // Clear all cookies 723 | const files = await fsPromises.readdir(cookiesDir); 724 | for (const file of files) { 725 | if (file.endsWith('.json')) { 726 | await fsPromises.unlink(path.join(cookiesDir, file)); 727 | } 728 | } 729 | return { 730 | content: [ 731 | { 732 | type: "text", 733 | text: `All saved cookies cleared (${files.length} domains)` 734 | } 735 | ], 736 | }; 737 | } 738 | } catch (error) { 739 | const errorMessage = error instanceof Error ? error.message : String(error); 740 | return { 741 | isError: true, 742 | content: [ 743 | { 744 | type: "text", 745 | text: `Error clearing cookies: ${errorMessage}`, 746 | }, 747 | ], 748 | }; 749 | } 750 | } 751 | ); 752 | 753 | // Keep the screenshot-element tool as before, but add default browser support 754 | server.tool( 755 | "screenshot-element", 756 | "Captures a screenshot of a specific element on a webpage using a CSS selector", 757 | { 758 | url: z.string().url().describe("The URL of the webpage"), 759 | selector: z.string().describe("CSS selector for the element to screenshot"), 760 | waitForSelector: z.boolean().optional().default(true).describe("Whether to wait for the selector to appear"), 761 | format: z.enum(['png', 'jpeg', 'webp']).optional().default('png').describe("Image format for the screenshot"), 762 | quality: z.number().min(0).max(100).optional().describe("Quality of the image (0-100), only applicable for jpeg and webp"), 763 | padding: z.number().optional().default(0).describe("Padding around the element in pixels"), 764 | useSavedAuth: z.boolean().optional().default(true).describe("Whether to use saved cookies from previous login"), 765 | useDefaultBrowser: z.boolean().optional().default(false).describe("Whether to use the system's default browser instead of Puppeteer's bundled Chromium"), 766 | visibleBrowser: z.boolean().optional().default(false).describe("Whether to show the browser window (non-headless mode)") 767 | }, 768 | async ({ url, selector, waitForSelector, format, quality, padding, useSavedAuth, useDefaultBrowser, visibleBrowser }) => { 769 | let page: Page | null = null; 770 | 771 | try { 772 | // Initialize browser with appropriate options 773 | const isHeadless = !visibleBrowser; 774 | const browserInstance = await initBrowser(isHeadless, useDefaultBrowser && visibleBrowser); 775 | 776 | // Create a new page 777 | page = await browserInstance.newPage(); 778 | 779 | // Load saved cookies if requested 780 | if (useSavedAuth) { 781 | const cookies = await loadCookies(url); 782 | if (cookies.length > 0) { 783 | await page.setCookie(...cookies); 784 | } 785 | } 786 | 787 | // Set viewport (matching screenshot-page tool) 788 | await page.setViewport({ width: 1920, height: 1080 }); 789 | 790 | // Set user agent to avoid bot detection 791 | 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'); 792 | 793 | // Additional anti-detection measures for Google 794 | await page.evaluateOnNewDocument(() => { 795 | // Remove webdriver property 796 | delete (window.navigator as any).webdriver; 797 | 798 | // Override the plugins property to add fake plugins 799 | Object.defineProperty(window.navigator, 'plugins', { 800 | get: () => [1, 2, 3, 4, 5] 801 | }); 802 | 803 | // Override the languages property 804 | Object.defineProperty(window.navigator, 'languages', { 805 | get: () => ['en-US', 'en'] 806 | }); 807 | 808 | // Override permissions 809 | Object.defineProperty(window.navigator, 'permissions', { 810 | get: () => ({ 811 | query: () => Promise.resolve({ state: 'granted' }) 812 | }) 813 | }); 814 | }); 815 | 816 | // Navigate to the URL 817 | await page.goto(url, { 818 | waitUntil: 'networkidle2', 819 | timeout: 30000 820 | }); 821 | 822 | // Wait for the selector if requested 823 | if (waitForSelector) { 824 | await page.waitForSelector(selector, { timeout: 10000 }); 825 | } 826 | 827 | // Get the element 828 | const element = await page.$(selector); 829 | 830 | if (!element) { 831 | return { 832 | isError: true, 833 | content: [ 834 | { 835 | type: "text", 836 | text: `Element not found with selector: ${selector}`, 837 | }, 838 | ], 839 | }; 840 | } 841 | 842 | // Add padding if requested 843 | if (padding > 0) { 844 | await page.evaluate((sel, pad) => { 845 | const el = document.querySelector(sel); 846 | if (el) { 847 | (el as HTMLElement).style.padding = `${pad}px`; 848 | } 849 | }, selector, padding); 850 | } 851 | 852 | // Prepare screenshot options 853 | const screenshotOptions: any = { 854 | encoding: 'base64', 855 | type: format 856 | }; 857 | 858 | // Add quality option for jpeg and webp 859 | if ((format === 'jpeg' || format === 'webp') && quality !== undefined) { 860 | screenshotOptions.quality = quality; 861 | } 862 | 863 | // Take screenshot of the element 864 | const screenshot = await element.screenshot(screenshotOptions) as string; 865 | 866 | // Determine browser type for response 867 | const browserType = useDefaultBrowser && visibleBrowser ? 'default browser' : 'Puppeteer browser'; 868 | const browserMode = visibleBrowser ? 'visible' : 'headless'; 869 | 870 | return { 871 | content: [ 872 | { 873 | type: "text", 874 | text: `Element screenshot captured successfully!\n\nBrowser: ${browserType} (${browserMode})\nURL: ${url}\nSelector: ${selector}\nFormat: ${format}` 875 | }, 876 | { 877 | type: "image", 878 | data: screenshot, 879 | mimeType: `image/${format}` 880 | } 881 | ], 882 | }; 883 | } catch (error) { 884 | const errorMessage = error instanceof Error ? error.message : String(error); 885 | return { 886 | isError: true, 887 | content: [ 888 | { 889 | type: "text", 890 | text: `Error capturing element screenshot: ${errorMessage}`, 891 | }, 892 | ], 893 | }; 894 | } finally { 895 | // Close the page 896 | if (page) { 897 | await page.close().catch(() => {}); 898 | } 899 | } 900 | } 901 | ); 902 | 903 | // Run the server 904 | async function main() { 905 | const transport = new StdioServerTransport(); 906 | await server.connect(transport); 907 | console.error("Screenshot MCP Server running on stdio"); 908 | } 909 | 910 | main().catch((error) => { 911 | console.error("Fatal error in main():", error); 912 | process.exit(1); 913 | }); ```