take_screenshot
Capture web page screenshots efficiently with automatic tiling for full-page views. Optimized for CLI coding tools, it ensures accurate display of webpage updates and integrates with AI models. Save tiled images locally or process via API.
Instructions
Fast, efficient screenshot capture of web pages - optimized for CLI coding tools. Use this after performing updates to web pages to ensure your changes are displayed correctly. Automatically tiles full pages into 1072x1072 chunks for optimal processing.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| directory | No | Save tiled screenshots to a local directory (returns file paths instead of base64) | |
| fullPage | No | Capture full page screenshot with tiling. If false, only the viewport is captured. | |
| url | Yes | HTTP/HTTPS URL to capture | |
| waitForMS | No | Additional wait time in milliseconds | |
| waitUntil | No | Wait until event: load, domcontentloaded, networkidle0, networkidle2 | domcontentloaded |
| width | No | Viewport width in pixels (max 1072) |
Implementation Reference
- Core handler function that executes the take_screenshot tool logic: manages Puppeteer browser lifecycle, navigates to the URL with retries, handles full-page tiling, and returns screenshot buffers.export async function captureScreenshot( options: ScreenshotOptions ): Promise<ScreenshotResult | TiledScreenshotResult> { logger.info('captureScreenshot called with options:', { url: options.url, fullPage: options.fullPage, viewport: options.viewport, waitUntil: options.waitUntil, waitFor: options.waitFor, }); // Update activity time when screenshot is requested updateActivityTime(); // Always capture full page with tiling if (options.fullPage !== false) { logger.debug('Delegating to captureTiledScreenshot'); return captureTiledScreenshot(options); } // Viewport-only capture logger.info(`Taking viewport screenshot of ${options.url}`); let browser: Browser | null = null; let page: Page | null = null; let attemptCount = 0; const maxAttempts = 2; while (attemptCount < maxAttempts) { try { attemptCount++; // Get or restart browser browser = await getBrowser(attemptCount > 1); page = await setupPage(browser); // Set viewport const viewport = { width: options.viewport?.width || 1072, height: options.viewport?.height || 1072, }; await page.setViewport(viewport); // Create recovery callback const recoveryCallback = async (): Promise<Page> => { logger.info('Recovering from error, creating new page...'); browser = await getBrowser(true); const newPage = await setupPage(browser); await newPage.setViewport(viewport); return newPage; }; // Navigate to the page with recovery page = await navigateWithRetry( page, options.url, options, recoveryCallback ); // Wait additional time if specified if (options.waitFor) { await page.evaluate( ms => new Promise(resolve => setTimeout(resolve, ms)), options.waitFor ); } // Take screenshot const screenshot = (await page.screenshot({ type: 'png', fullPage: false, encoding: 'binary', })) as Buffer; const result: ScreenshotResult = { url: options.url, screenshot, timestamp: new Date(), viewport, format: 'png', }; // Clean up the page after successful capture if (page && !page.isClosed()) { await page.close().catch(() => {}); } return result; } catch (error: any) { logger.error( `Error taking screenshot (attempt ${attemptCount}/${maxAttempts}):`, error ); // Clean up the page if (page && !page.isClosed()) { await page.close().catch(() => {}); } // If this was our last attempt, throw the error if (attemptCount >= maxAttempts) { throw error; } // Otherwise, wait a bit before retrying logger.info('Retrying with fresh browser...'); await new Promise(resolve => setTimeout(resolve, 2000)); } } throw new Error('Failed to capture screenshot after all attempts'); }
- src/serve.ts:339-471 (handler)MCP CallTool request handler dispatch for 'take_screenshot': imports the screenshot module, calls captureScreenshot, processes results (base64 or file paths).if (request.params.name === 'take_screenshot') { // Lazy load the module on first use (may already be loaded from warmup) if (!screenshotModule) { logger.debug('Loading screenshot module...'); screenshotModule = await import( './internal/screenshotCapture.js' ); logger.info('Screenshot module loaded successfully'); } const args = request.params.arguments as any; logger.info(`Processing screenshot request for URL: ${args.url}`); logger.debug('Screenshot parameters:', { url: args.url, viewport: { width: args.width }, fullPage: args.fullPage, waitUntil: args.waitUntil, waitForMS: args.waitForMS, directory: args.directory, }); logger.debug('Calling captureScreenshot...'); const result = await screenshotModule.captureScreenshot({ url: args.url, viewport: { width: Math.min(args.width ?? 1072, 1072), }, fullPage: args.fullPage ?? true, waitUntil: args.waitUntil ?? 'domcontentloaded', waitFor: args.waitForMS, }); logger.info('Screenshot captured successfully'); logger.debug( 'Result type:', 'tiles' in result ? 'TiledScreenshot' : 'RegularScreenshot' ); // If directory is specified, save to disk if (args.directory) { logger.debug( `Saving screenshots to directory: ${args.directory}` ); // Ensure directory exists if (!existsSync(args.directory)) { logger.debug('Creating directory...'); await mkdir(args.directory, { recursive: true }); logger.info('Directory created successfully'); } const savedPaths: string[] = []; if ('tiles' in result) { // Handle tiled screenshot result const tiledResult = result as any; // TiledScreenshotResult // Save each tile for (let i = 0; i < tiledResult.tiles.length; i++) { const tile = tiledResult.tiles[i]; const filename = generateFilename(args.url, i); const filepath = join(args.directory, filename); await writeFile(filepath, tile.screenshot); savedPaths.push(filepath); } return { content: [ { type: 'text', text: `✅ Saved ${tiledResult.tiles.length} screenshot tiles to:\n${savedPaths.join('\n')}\n\nPage size: ${tiledResult.fullWidth}x${tiledResult.fullHeight} pixels\nTile size: ${tiledResult.tileSize}x${tiledResult.tileSize} pixels`, }, ], }; } else { // Handle regular screenshot const filename = generateFilename(args.url); const filepath = join(args.directory, filename); await writeFile(filepath, result.screenshot); savedPaths.push(filepath); return { content: [ { type: 'text', text: `✅ Screenshot saved to: ${filepath}\n\nDimensions: ${result.viewport.width}x${result.viewport.height} pixels`, }, ], }; } } else { // Return base64 encoded images as before if ('tiles' in result) { // Handle tiled screenshot result const tiledResult = result as any; // TiledScreenshotResult const content = []; // Add each tile as an image for (const tile of tiledResult.tiles) { content.push({ type: 'image', data: tile.screenshot.toString('base64'), mimeType: 'image/png', }); } // Add summary text content.push({ type: 'text', text: `✅ Captured ${tiledResult.tiles.length} tiles (${tiledResult.tileSize}x${tiledResult.tileSize} each) from page measuring ${tiledResult.fullWidth}x${tiledResult.fullHeight} pixels`, }); return { content }; } else { // Handle regular screenshot const base64Screenshot = result.screenshot.toString('base64'); return { content: [ { type: 'image', data: base64Screenshot, mimeType: 'image/png', }, { type: 'text', text: `✅ Screenshot captured: ${result.viewport.width}x${result.viewport.height} pixels`, }, ], }; } }
- src/serve.ts:42-89 (schema)Tool schema definition including input schema, description, annotations for the take_screenshot tool.const SCREENSHOT_TOOL: Tool = { name: 'take_screenshot', description: 'Fast, efficient screenshot capture of web pages - optimized for CLI coding tools. Use this after performing updates to web pages to ensure your changes are displayed correctly. Automatically tiles full pages into 1072x1072 chunks for optimal processing.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'HTTP/HTTPS URL to capture', }, width: { type: 'number', description: 'Viewport width in pixels (max 1072)', default: 1072, }, fullPage: { type: 'boolean', description: 'Capture full page screenshot with tiling. If false, only the viewport is captured.', default: true, }, waitUntil: { type: 'string', description: 'Wait until event: load, domcontentloaded, networkidle0, networkidle2', default: 'domcontentloaded', }, waitForMS: { type: 'number', description: 'Additional wait time in milliseconds', }, directory: { type: 'string', description: 'Save tiled screenshots to a local directory (returns file paths instead of base64)', }, }, required: ['url'], }, annotations: { title: 'Take Screenshot', readOnlyHint: true, // Screenshots don't modify anything destructiveHint: false, idempotentHint: false, // Each call captures fresh content openWorldHint: true, // Interacts with external websites }, };
- src/serve.ts:214-224 (registration)Registration of the take_screenshot tool in the MCP ListTools handler response.server.setRequestHandler(ListToolsRequestSchema, async () => { logger.debug('Received ListTools request'); const response = { tools: [SCREENSHOT_TOOL, SCREENCAST_TOOL, CONSOLE_CAPTURE_TOOL], }; logger.debug( 'Returning tools:', response.tools.map(t => t.name) ); return response; });
- Helper function for full-page tiled screenshots: captures full height image then tiles it using Sharp library.async function captureTiledScreenshot( options: ScreenshotOptions ): Promise<TiledScreenshotResult> { const tileSize = options.viewport?.width || 1072; logger.info(`Taking tiled screenshot of ${options.url}`); let browser: Browser | null = null; let page: Page | null = null; let attemptCount = 0; const maxAttempts = 2; while (attemptCount < maxAttempts) { try { attemptCount++; // Get or restart browser browser = await getBrowser(attemptCount > 1); page = await setupPage(browser); // Set viewport to capture full width in tile size await page.setViewport({ width: tileSize, height: tileSize, }); // Create recovery callback const recoveryCallback = async (): Promise<Page> => { logger.info('Recovering from error, creating new page...'); browser = await getBrowser(true); const newPage = await setupPage(browser); await newPage.setViewport({ width: tileSize, height: tileSize, }); return newPage; }; // Navigate to the page with recovery page = await navigateWithRetry( page, options.url, options, recoveryCallback ); // Wait additional time if specified if (options.waitFor) { await page.evaluate( ms => new Promise(resolve => setTimeout(resolve, ms)), options.waitFor ); } // Get the full page height const fullPageHeight = await page.evaluate(() => { // This runs in the browser context where document is available return (globalThis as any).document.documentElement .scrollHeight; }); // Take a screenshot with explicit dimensions to ensure width is constrained logger.info('Capturing full page screenshot...'); const fullPageScreenshot = (await page.screenshot({ type: 'png', fullPage: false, encoding: 'binary', clip: { x: 0, y: 0, width: tileSize, height: fullPageHeight, }, })) as Buffer; // Import sharp dynamically to process the image const sharp = await import('sharp'); const metadata = await sharp.default(fullPageScreenshot).metadata(); const dimensions = { width: Math.min(metadata.width!, tileSize), height: metadata.height!, }; logger.info( `Full page dimensions: ${dimensions.width}x${dimensions.height} (viewport width: ${tileSize})` ); // Calculate number of tiles needed const cols = Math.ceil(dimensions.width / tileSize); const rows = Math.ceil(dimensions.height / tileSize); const tiles = []; logger.info( `Creating ${rows}x${cols} tiles (${rows * cols} total)` ); // Cut the full page screenshot into tiles for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const x = col * tileSize; const y = row * tileSize; const width = Math.min(tileSize, dimensions.width - x); const height = Math.min(tileSize, dimensions.height - y); // Extract tile from full page screenshot const tileBuffer = await sharp .default(fullPageScreenshot) .extract({ left: x, top: y, width, height, }) .png() .toBuffer(); tiles.push({ screenshot: tileBuffer, index: row * cols + col, row, col, x, y, width, height, }); logger.debug( `Created tile ${row},${col} at ${x},${y} (${width}x${height})` ); } } const result: TiledScreenshotResult = { url: options.url, tiles, timestamp: new Date(), fullWidth: dimensions.width, fullHeight: dimensions.height, tileSize, format: 'png', }; // Clean up the page after successful capture if (page && !page.isClosed()) { await page.close().catch(() => {}); } return result; } catch (error: any) { logger.error( `Error taking tiled screenshot (attempt ${attemptCount}/${maxAttempts}):`, error ); // Clean up the page if (page && !page.isClosed()) { await page.close().catch(() => {}); } // If this was our last attempt, throw the error if (attemptCount >= maxAttempts) { throw error; } // Otherwise, wait a bit before retrying logger.info('Retrying with fresh browser...'); await new Promise(resolve => setTimeout(resolve, 2000)); } } throw new Error('Failed to capture tiled screenshot after all attempts'); }