Skip to main content
Glama
just-every

Screenshot Website Fast

by just-every

take_screencast

Capture timed screenshots of web pages to create animated screencasts. Records page activity over specified durations with adaptive frame rates, outputting PNG frames or animated WebP files for documentation and analysis.

Instructions

Capture a series of screenshots of a web page over time, producing a screencast. Uses adaptive frame rates: 100ms intervals for ≤5s, 200ms for 5-10s, 500ms for >10s. PNG format: individual frames. WebP format: animated WebP with 4-second pause at end for looping.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesHTTP/HTTPS URL to capture
durationNoTotal duration of screencast in seconds
widthNoViewport width in pixels (max 1072)
heightNoViewport height in pixels (max 1072)
jsEvaluateNoJavaScript code to execute. String: single instruction after first screenshot. Array: takes screenshot before each instruction, then continues capturing until duration ends.
waitUntilNoWait until event: load, domcontentloaded, networkidle0, networkidle2domcontentloaded
directoryNoSave screencast to directory. Specify format with "format" parameter.
formatNoOutput format when using directory: "png" for individual PNG files, "webp" for animated WebP (default)webp
qualityNoWebP quality level (only applies when format is "webp"): low (50), medium (75), high (90)medium

Implementation Reference

  • Core handler function that implements the screencast logic using Puppeteer: launches browser, navigates to URL, optionally executes JS instructions, captures series of PNG screenshots at specified intervals over duration, collects frames into ScreencastResult.
    export async function captureScreencast(
        options: ScreencastOptions
    ): Promise<ScreencastResult> {
        logger.info('captureScreencast called with options:', {
            url: options.url,
            duration: options.duration,
            interval: options.interval,
            viewport: options.viewport,
            waitUntil: options.waitUntil,
            waitFor: options.waitFor,
            hasJsEvaluate: !!options.jsEvaluate,
        });
    
        // Update activity time when screencast is requested
        updateActivityTime();
    
        const frames: ScreencastResult['frames'] = [];
        const startTime = new Date();
    
        let browser: Browser | null = null;
        let page: Page | null = null;
    
        try {
            // Get browser instance
            browser = await getBrowser();
            page = await setupPage(browser);
    
            // Set viewport (only capture top tile - 1072x1072)
            const viewport = {
                width: options.viewport?.width || 1072,
                height: options.viewport?.height || 1072,
            };
            await page.setViewport(viewport);
    
            logger.info(`Starting screencast of ${options.url}`);
    
            // Navigate to the page
            await page.goto(options.url, {
                waitUntil: options.waitUntil || 'domcontentloaded',
                timeout: 60000,
            });
    
            // Wait additional time if specified
            if (options.waitFor) {
                await page.evaluate(
                    ms => new Promise(resolve => setTimeout(resolve, ms)),
                    options.waitFor
                );
            }
    
            // Handle JavaScript execution with configurable screenshot intervals
            let jsInstructionCount = 0;
            const screenshotInterval = options.interval * 1000; // Convert seconds to milliseconds
            const jsExecutionInterval = 1000; // Execute JS instructions every 1 second
    
            if (options.jsEvaluate) {
                const jsInstructions = Array.isArray(options.jsEvaluate)
                    ? options.jsEvaluate
                    : [options.jsEvaluate];
    
                jsInstructionCount = jsInstructions.length;
                logger.info(
                    `Processing ${jsInstructionCount} JavaScript instruction(s) with ${screenshotInterval}ms screenshot intervals`
                );
    
                const startTime = Date.now();
                let nextJsIndex = 0;
                let frameIndex = 0;
    
                // Run for the duration needed to execute all JS instructions
                const jsDuration = jsInstructions.length * jsExecutionInterval;
    
                while (Date.now() - startTime < jsDuration) {
                    const elapsed = Date.now() - startTime;
    
                    // Check if it's time to execute the next JS instruction
                    if (
                        nextJsIndex < jsInstructions.length &&
                        elapsed >= nextJsIndex * jsExecutionInterval
                    ) {
                        logger.info(
                            `Executing JavaScript instruction ${nextJsIndex + 1}/${jsInstructions.length}: ${jsInstructions[nextJsIndex].substring(0, 50)}...`
                        );
                        try {
                            await page.evaluate(jsInstructions[nextJsIndex]);
                            logger.debug(
                                `JavaScript instruction ${nextJsIndex + 1} completed`
                            );
                        } catch (error) {
                            logger.error(
                                `JavaScript instruction ${nextJsIndex + 1} failed:`,
                                error
                            );
                            throw new Error(
                                `Failed to execute JavaScript instruction ${nextJsIndex + 1}: ${error}`
                            );
                        }
                        nextJsIndex++;
                    }
    
                    // Take screenshot
                    const screenshot = (await page.screenshot({
                        type: 'png',
                        fullPage: false,
                        encoding: 'binary',
                    })) as Buffer;
    
                    frames.push({
                        screenshot,
                        timestamp: new Date(),
                        index: frameIndex,
                    });
    
                    frameIndex++;
                    logger.debug(
                        `Captured high-frequency frame ${frameIndex} at ${elapsed}ms`
                    );
    
                    // Wait for next screenshot interval
                    const nextScreenshotTime =
                        startTime + frameIndex * screenshotInterval;
                    const waitTime = Math.max(0, nextScreenshotTime - Date.now());
                    if (waitTime > 0) {
                        await new Promise(resolve => setTimeout(resolve, waitTime));
                    }
                }
    
                // Update jsInstructionCount to reflect actual frames captured during JS execution
                jsInstructionCount = frameIndex;
            }
    
            // Calculate remaining time and frames needed at configured intervals
            const remainingDuration =
                options.duration * 1000 - jsInstructionCount * screenshotInterval; // Remaining time in ms
            const remainingFrames = Math.max(
                0,
                Math.floor(remainingDuration / screenshotInterval)
            );
    
            logger.info(
                `Captured ${jsInstructionCount} frames during JS execution. Capturing ${remainingFrames} additional frames at ${screenshotInterval}ms intervals for remaining ${remainingDuration}ms`
            );
    
            // Capture remaining frames at configured intervals
            for (let i = 0; i < remainingFrames; i++) {
                const frameStart = Date.now();
    
                // Take screenshot of viewport (top tile only)
                const screenshot = (await page.screenshot({
                    type: 'png',
                    fullPage: false,
                    encoding: 'binary',
                })) as Buffer;
    
                const frameIndex = jsInstructionCount + i;
                frames.push({
                    screenshot,
                    timestamp: new Date(),
                    index: frameIndex,
                });
    
                logger.debug(
                    `Captured duration frame ${frameIndex + 1} (${i + 1}/${remainingFrames})`
                );
    
                // Wait for next interval (if not the last frame)
                if (i < remainingFrames - 1) {
                    const elapsed = Date.now() - frameStart;
                    const waitTime = Math.max(0, screenshotInterval - elapsed);
                    if (waitTime > 0) {
                        await new Promise(resolve => setTimeout(resolve, waitTime));
                    }
                }
            }
    
            const endTime = new Date();
    
            const result: ScreencastResult = {
                url: options.url,
                frames,
                startTime,
                endTime,
                duration: options.duration,
                interval: options.interval,
                viewport,
                format: 'png',
            };
    
            logger.info(`Screencast completed: ${frames.length} frames captured`);
    
            // Clean up the page after successful capture
            if (page && !page.isClosed()) {
                await page.close().catch(() => {});
            }
    
            return result;
        } catch (error: any) {
            logger.error('Error capturing screencast:', error);
    
            // Clean up the page
            if (page && !page.isClosed()) {
                await page.close().catch(() => {});
            }
    
            throw error;
        }
    }
  • Tool schema definition specifying name, description, input parameters (url required, duration=10s, viewport 1072x1072, jsEvaluate for interactions, output options), and annotations (readOnly, etc.).
    const SCREENCAST_TOOL: Tool = {
        name: 'take_screencast',
        description:
            'Capture a series of screenshots of a web page over time, producing a screencast. Uses adaptive frame rates: 100ms intervals for ≤5s, 200ms for 5-10s, 500ms for >10s. PNG format: individual frames. WebP format: animated WebP with 4-second pause at end for looping.',
        inputSchema: {
            type: 'object',
            properties: {
                url: {
                    type: 'string',
                    description: 'HTTP/HTTPS URL to capture',
                },
                duration: {
                    type: 'number',
                    description: 'Total duration of screencast in seconds',
                    default: 10,
                },
                width: {
                    type: 'number',
                    description: 'Viewport width in pixels (max 1072)',
                    default: 1072,
                },
                height: {
                    type: 'number',
                    description: 'Viewport height in pixels (max 1072)',
                    default: 1072,
                },
                jsEvaluate: {
                    oneOf: [
                        {
                            type: 'string',
                            description:
                                'Single JavaScript code to execute after the first screenshot',
                        },
                        {
                            type: 'array',
                            items: { type: 'string' },
                            description:
                                'Array of JavaScript instructions - screenshot taken before each one',
                        },
                    ],
                    description:
                        'JavaScript code to execute. String: single instruction after first screenshot. Array: takes screenshot before each instruction, then continues capturing until duration ends.',
                },
                waitUntil: {
                    type: 'string',
                    description:
                        'Wait until event: load, domcontentloaded, networkidle0, networkidle2',
                    default: 'domcontentloaded',
                },
                directory: {
                    type: 'string',
                    description:
                        'Save screencast to directory. Specify format with "format" parameter.',
                },
                format: {
                    type: 'string',
                    description:
                        'Output format when using directory: "png" for individual PNG files, "webp" for animated WebP (default)',
                    enum: ['png', 'webp'],
                    default: 'webp',
                },
                quality: {
                    type: 'string',
                    description:
                        'WebP quality level (only applies when format is "webp"): low (50), medium (75), high (90)',
                    enum: ['low', 'medium', 'high'],
                    default: 'medium',
                },
            },
            required: ['url'],
        },
        annotations: {
            title: 'Take Screencast',
            readOnlyHint: true, // Screencasts 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 via the ListToolsRequestSchema handler that returns the SCREENCAST_TOOL in the tools list, making it discoverable by MCP clients.
    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;
    });
  • MCP server execution handler for 'take_screencast': validates params, computes parameters (e.g. adaptive frame rate), delegates to captureScreencast, processes result into MCP content (images/text or file paths). Handles WebP creation and PNG fallback.
    } else if (request.params.name === 'take_screencast') {
        // 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 screencast request for URL: ${args.url}`);
    
        // Validate format parameter usage
        if (args.format && !args.directory) {
            throw new Error(
                'The "format" parameter can only be used when "directory" parameter is specified'
            );
        }
    
        const duration = args.duration ?? 10;
        const format = args.format ?? 'webp';
        const width = Math.min(args.width ?? 1072, 1072); // Cap at 1072
        const height = Math.min(args.height ?? 1072, 1072); // Cap at 1072
        const quality = args.quality ?? 'medium';
    
        // Adaptive frame rate based on duration to manage memory
        let interval: number;
        if (args.directory && format === 'webp') {
            // For WebP exports, use adaptive intervals based on duration
            if (duration <= 5) {
                interval = 0.1; // 100ms for <= 5s (up to 50 frames)
            } else if (duration <= 10) {
                interval = 0.2; // 200ms for 5-10s (up to 50 frames)
            } else {
                interval = 0.5; // 500ms for > 10s (manageable frame count)
            }
        } else {
            interval = 2; // 2s for base64 returns
        }
    
        logger.debug('Screencast parameters:', {
            url: args.url,
            duration,
            interval,
            width,
            height,
            waitUntil: args.waitUntil,
            jsEvaluate: args.jsEvaluate
                ? Array.isArray(args.jsEvaluate)
                    ? `array(${args.jsEvaluate.length})`
                    : 'string'
                : 'none',
            directory: args.directory,
            format,
            quality,
        });
    
        logger.debug('Calling captureScreencast...');
        const result = await screenshotModule.captureScreencast({
            url: args.url,
            duration,
            interval,
            viewport: {
                width: width,
                height: height,
            },
            waitUntil: args.waitUntil ?? 'domcontentloaded',
            waitFor: undefined, // Removed waitForMS
            jsEvaluate: args.jsEvaluate,
        });
    
        logger.info('Screencast captured successfully');
        logger.debug(`Captured ${result.frames.length} frames`);
    
        // If directory is specified, save based on format
        if (args.directory) {
            logger.debug(
                `Saving screencast to directory: ${args.directory} (format: ${format})`
            );
    
            // Ensure directory exists
            if (!existsSync(args.directory)) {
                logger.debug('Creating directory...');
                await mkdir(args.directory, { recursive: true });
                logger.info('Directory created successfully');
            }
    
            const frames = result.frames.map((f: any) => f.screenshot);
    
            if (format === 'png') {
                // Save individual PNG frames only
                const framePaths: string[] = [];
                for (let i = 0; i < result.frames.length; i++) {
                    const frameFilename = generateFilename(
                        args.url,
                        i,
                        'frame'
                    );
                    const frameFilepath = join(
                        args.directory,
                        frameFilename
                    );
                    await writeFile(
                        frameFilepath,
                        result.frames[i].screenshot
                    );
                    framePaths.push(frameFilepath);
                }
    
                return {
                    content: [
                        {
                            type: 'text',
                            text: `✅ Screencast saved as PNG frames:\n${framePaths.join('\n')}\n\nDuration: ${result.duration}s\nFrames: ${result.frames.length}\nInterval: ${interval}s`,
                        },
                    ],
                };
            } else {
                // Save as animated WebP and clean up PNGs
                let webpBuffer: Buffer | null = null;
    
                try {
                    // Create animated WebP using img2webp with adaptive intervals
                    const frameDelay = interval * 1000; // Convert to milliseconds
                    webpBuffer = await createAnimatedWebP(
                        frames,
                        frameDelay,
                        4000,
                        quality as 'low' | 'medium' | 'high'
                    );
                    logger.info('WebP created using img2webp CLI');
                } catch (error) {
                    logger.error('Failed to create WebP:', error);
                    webpBuffer = null;
                }
    
                if (webpBuffer) {
                    const filename = generateFilename(
                        args.url,
                        undefined,
                        'screencast'
                    ).replace('.png', '.webp');
                    const filepath = join(args.directory, filename);
                    await writeFile(filepath, webpBuffer);
    
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `✅ Screencast saved as animated WebP: ${filepath}\n\nDuration: ${result.duration}s\nFrames: ${result.frames.length}\nCapture Interval: ${interval * 1000}ms (4s pause at end)\nQuality: ${quality}\nMethod: img2webp (optimized with frame deduplication)`,
                            },
                        ],
                    };
                } else {
                    // Fallback to PNG frames if WebP fails
                    const framePaths: string[] = [];
                    for (let i = 0; i < result.frames.length; i++) {
                        const frameFilename = generateFilename(
                            args.url,
                            i,
                            'frame'
                        );
                        const frameFilepath = join(
                            args.directory,
                            frameFilename
                        );
                        await writeFile(
                            frameFilepath,
                            result.frames[i].screenshot
                        );
                        framePaths.push(frameFilepath);
                    }
    
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `⚠️  WebP creation failed, saved as PNG frames:\n${framePaths.join('\n')}\n\nDuration: ${result.duration}s\nFrames: ${result.frames.length}\nInterval: ${interval}s`,
                            },
                        ],
                    };
                }
            }
        } else {
            // Return frames as base64 encoded images
            const content = [];
    
            // Add each frame as an image
            for (let i = 0; i < result.frames.length; i++) {
                content.push({
                    type: 'image',
                    data: result.frames[i].screenshot.toString('base64'),
                    mimeType: 'image/png',
                });
            }
    
            // Add summary text
            content.push({
                type: 'text',
                text: `✅ Captured ${result.frames.length} frames over ${result.duration} seconds (${result.interval}s interval)`,
            });
    
            return { content };
        }
    } else if (request.params.name === 'capture_console') {

Latest Blog Posts

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/just-every/mcp-screenshot-website-fast'

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