Skip to main content
Glama

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