UNPKG

supapup

Version:

โšก Lightning-fast MCP browser dev tool. Navigate โ†’ Get instant structured data. No screenshots needed! Puppeteer: ๐Ÿ“ธ โ†’ CSS selectors โ†’ JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.

316 lines (315 loc) โ€ข 14.9 kB
import { randomUUID } from 'crypto'; export class ScreenshotTools { page = null; screenshotChunkData = new Map(); initialize(page) { this.page = page; } async capture(params = {}) { try { if (!this.page) { throw new Error('No page available. Use browser_navigate tool to open a webpage before taking screenshots.'); } const { fullPage = false, quality = 80, selector, scrollTo, viewport } = params; console.log(`๐Ÿ“ธ Taking screenshot (fullPage: ${fullPage}, quality: ${quality})`); // Set viewport if specified if (viewport) { await this.page.setViewport(viewport); } // Scroll to position if specified if (typeof scrollTo === 'number') { await this.page.evaluate((y) => { window.scrollTo(0, y); }, scrollTo); await new Promise(resolve => setTimeout(resolve, 500)); // Wait for scroll } let screenshotOptions = { type: 'png', fullPage, encoding: 'base64' }; if (quality && quality < 100) { screenshotOptions.type = 'jpeg'; screenshotOptions.quality = quality; } let screenshot; if (selector) { // Screenshot specific element const element = await this.page.$(selector); if (!element) { throw new Error(`Element not found: ${selector}. Use agent_generate_page to see available elements, or try a different CSS selector.`); } screenshot = await element.screenshot(screenshotOptions); } else { // Screenshot full page or viewport screenshot = await this.page.screenshot(screenshotOptions); } // Check if screenshot needs to be chunked // Increased limit: Claude can handle ~25k tokens, base64 is roughly 1.33x original size // So a 50KB image = ~67KB base64 = ~67k chars. We'll use 45k as a safe limit. const shouldChunk = screenshot.length > 45000; // Safe limit for Claude (was 8800) console.log(`๐Ÿ“ธ Screenshot size: ${screenshot.length} chars, shouldChunk: ${shouldChunk}`); if (shouldChunk && fullPage) { // For full page screenshots that are too large, use pagination console.log(`๐Ÿ“ธ Full page screenshot too large (${screenshot.length} chars), switching to paginated capture`); // Call capturePaginated with appropriate parameters return await this.capturePaginated({ quality: quality || 50, // Use lower quality for paginated screenshots overlap: 100 // Default overlap }); } else if (shouldChunk && !fullPage) { // For viewport screenshots that are too large, reduce quality and retry console.log(`๐Ÿ“ธ Viewport screenshot too large (${screenshot.length} chars), reducing quality`); const reducedQuality = Math.min(30, quality * 0.5); // Reduce quality by half, max 30 screenshotOptions.type = 'jpeg'; screenshotOptions.quality = reducedQuality; screenshot = await this.page.screenshot(screenshotOptions); if (screenshot.length > 45000) { // If still too large after quality reduction, chunk it console.log(`๐Ÿ“ธ Viewport screenshot still too large (${screenshot.length} chars) after quality reduction to ${reducedQuality}, chunking...`); // Store the screenshot for chunking const chunkId = randomUUID(); this.screenshotChunkData.set(chunkId, { id: chunkId, totalChunks: Math.ceil(screenshot.length / 8000), chunks: this.splitScreenshotData(screenshot), metadata: { quality: reducedQuality, fullPage: false, timestamp: new Date() } }); return { content: [{ type: 'text', text: `๐Ÿ“ธ Viewport screenshot captured but too large (${screenshot.length} chars).\n` + `๐Ÿงฉ Automatically chunked into ${Math.ceil(screenshot.length / 8000)} pieces.\n` + `๐Ÿ“‹ Chunk ID: ${chunkId}\n` + `๐Ÿ” Use screenshot_get_chunk({id: "${chunkId}", chunk: 1}) to view the first chunk.` }] }; } return { content: [{ type: 'image', data: screenshot, mimeType: 'image/jpeg' }] }; } else { // Screenshot is within size limits return { content: [{ type: 'image', data: screenshot, mimeType: screenshotOptions.type === 'jpeg' ? 'image/jpeg' : 'image/png' }] }; } } catch (error) { console.error('โŒ Screenshot capture failed:', error); return { content: [{ type: 'text', text: `โŒ Screenshot failed: ${error.message}` }] }; } } async capturePaginated(params = {}) { try { if (!this.page) { throw new Error('No page available. Use browser_navigate tool to open a webpage before taking screenshots.'); } const { segments, quality = 50, // Lower quality for paginated screenshots overlap = 100 } = params; console.log(`๐Ÿ“ธ Taking paginated screenshots (segments: ${segments || 'auto'}, quality: ${quality})`); // Get page dimensions const dimensions = await this.page.evaluate(() => { return { width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth), height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight), viewportHeight: window.innerHeight }; }); // Calculate segments const totalSegments = segments || Math.ceil(dimensions.height / (dimensions.viewportHeight - overlap)); const segmentHeight = Math.floor((dimensions.height - (totalSegments - 1) * overlap) / totalSegments); console.log(`๐Ÿ“ Page dimensions: ${dimensions.width}x${dimensions.height}`); console.log(`๐Ÿงฉ Creating ${totalSegments} segments of ${segmentHeight}px each`); const screenshots = []; for (let i = 0; i < totalSegments; i++) { const scrollY = i * (segmentHeight - overlap); console.log(`๐Ÿ“ธ Capturing segment ${i + 1}/${totalSegments} at scroll position ${scrollY}`); // Scroll to position await this.page.evaluate((y) => { window.scrollTo(0, y); }, scrollY); // Wait for content to load await new Promise(resolve => setTimeout(resolve, 500)); // Take screenshot const screenshot = await this.page.screenshot({ type: 'jpeg', quality, encoding: 'base64' }); // Validate screenshot before adding to array if (screenshot && screenshot.length > 0) { screenshots.push(screenshot); } else { console.warn(`โš ๏ธ Invalid screenshot for segment ${i + 1}, skipping`); } } // Validate that we have valid screenshots if (screenshots.length === 0) { throw new Error('Failed to capture any valid screenshots'); } // Store chunked data const chunkId = randomUUID(); this.screenshotChunkData.set(chunkId, { id: chunkId, totalChunks: screenshots.length, chunks: screenshots, metadata: { fullPage: true, quality, timestamp: new Date() } }); return { content: [{ type: 'text', text: `๐Ÿ“ธ Paginated screenshots captured successfully!\n` + `๐Ÿงฉ Total segments: ${screenshots.length}\n` + `๐Ÿ“‹ Screenshot ID: ${chunkId}\n` + `๐Ÿ” Use screenshot_get_chunk({id: "${chunkId}", chunk: 1}) to view the first segment.` }] }; } catch (error) { console.error('โŒ Paginated screenshot failed:', error); return { content: [{ type: 'text', text: `โŒ Paginated screenshot failed: ${error.message}` }] }; } } async getChunk(id, chunk) { try { const chunkData = this.screenshotChunkData.get(id); if (!chunkData) { throw new Error(`Screenshot chunk ID not found: ${id}`); } if (chunk < 1 || chunk > chunkData.totalChunks) { throw new Error(`Invalid chunk number: ${chunk}. Available chunks: 1-${chunkData.totalChunks}`); } const screenshot = chunkData.chunks[chunk - 1]; // Validate screenshot data before returning if (!screenshot || screenshot.length === 0) { throw new Error(`Screenshot chunk ${chunk} is empty or invalid. This may be due to a chunking error.`); } const mimeType = chunkData.metadata.quality < 100 ? 'image/jpeg' : 'image/png'; return { content: [ { type: 'text', text: `๐Ÿ“ธ Screenshot chunk ${chunk} of ${chunkData.totalChunks}\n๐Ÿ“‹ ID: ${id}` }, { type: 'image', data: screenshot, mimeType } ] }; } catch (error) { console.error('โŒ Error getting screenshot chunk:', error); return { content: [{ type: 'text', text: `โŒ Error getting screenshot chunk: ${error.message}` }] }; } } async getLoadingSequenceFrame(id, frame) { try { console.log(`๐Ÿ” Looking for loading sequence ID: ${id}`); console.log(`๐Ÿ“Š Available chunk IDs: ${Array.from(this.screenshotChunkData.keys()).join(', ')}`); const chunkData = this.screenshotChunkData.get(id); if (!chunkData) { throw new Error(`Loading sequence ID not found: ${id}. Make sure you used browser_navigate_and_capture_loading_sequence first.`); } if (frame < 1 || frame > chunkData.totalChunks) { throw new Error(`Invalid frame number: ${frame}. Available frames: 1-${chunkData.totalChunks}`); } const screenshot = chunkData.chunks[frame - 1]; // Validate screenshot data before returning if (!screenshot || screenshot.length === 0) { throw new Error(`Loading sequence frame ${frame} is empty or invalid. This may be due to a capture error.`); } const mimeType = 'image/jpeg'; // Loading sequences always use JPEG // Get frame metadata if available const frameMetadata = chunkData.metadata.frameMetadata; let frameDescription = ''; if (frameMetadata && frameMetadata[frame - 1]) { const meta = frameMetadata[frame - 1]; frameDescription = `\nโฑ๏ธ Timestamp: ${meta.timestamp}ms\n๐Ÿ“ State: ${meta.description}`; } // Navigation hints const prevFrame = Math.max(1, frame - 1); const nextFrame = Math.min(chunkData.totalChunks, frame + 1); const navigationText = frame === 1 ? `\nโญ๏ธ Next: frame ${nextFrame}` : frame === chunkData.totalChunks ? `\nโฎ๏ธ Previous: frame ${prevFrame}` : `\nโฎ๏ธ Previous: frame ${prevFrame}\nโญ๏ธ Next: frame ${nextFrame}`; return { content: [ { type: 'text', text: `๐ŸŽฌ Loading sequence frame ${frame} of ${chunkData.totalChunks}${frameDescription}${navigationText}\n๐Ÿ†” Sequence ID: ${id}` }, { type: 'image', data: screenshot, mimeType } ] }; } catch (error) { console.error('โŒ Error getting loading sequence frame:', error); return { content: [{ type: 'text', text: `โŒ Error getting loading sequence frame: ${error.message}` }] }; } } // Private helper methods removed - chunkLargeScreenshot was fundamentally flawed // We now use capturePaginated for large screenshots instead // Utility method to clean up old chunks // Split screenshot data into chunks splitScreenshotData(screenshot) { const chunkSize = 8000; const chunks = []; for (let i = 0; i < screenshot.length; i += chunkSize) { chunks.push(screenshot.slice(i, i + chunkSize)); } return chunks; } cleanupOldChunks(maxAge = 3600000) { const now = Date.now(); for (const [id, chunk] of this.screenshotChunkData.entries()) { if (now - chunk.metadata.timestamp.getTime() > maxAge) { this.screenshotChunkData.delete(id); console.log(`๐Ÿงน Cleaned up old screenshot chunks: ${id}`); } } } // Get chunk data for external access getChunkData() { return this.screenshotChunkData; } setChunkData(data) { this.screenshotChunkData = data; } }