Skip to main content
Glama
mcp-tools.ts•21.3 kB
/** * MCP tool implementations for screenshot capture * * This module implements the five MCP tools exposed by the screenshot server: * - screenshot_capture_full: Capture full screen or specific display * - screenshot_capture_window: Capture specific application window * - screenshot_capture_region: Capture rectangular screen region * - screenshot_list_displays: List all connected displays * - screenshot_list_windows: List all visible windows * * @module tools */ import { z } from "zod"; import { createCaptureEngine } from "../capture"; import { ImageProcessor } from "../processing"; import { PrivacyManager } from "../privacy"; import { SecurityManager } from "../security"; import { FileOperations } from "../storage"; import { ImageFormat, ScreenshotResponse, ScreenshotMetadata, SecurityPolicy, } from "../types"; import { ScreenshotError, WindowNotFoundError, DisplayNotFoundError, formatErrorResponse, EncodingFailedError, FileSystemError, InvalidRegionError, } from "../errors"; /** * MCP Tools class that implements all screenshot tools * * This class provides the implementation for all five MCP screenshot tools, * handling capture operations, security enforcement, PII masking, and file operations. * * @example * ```typescript * import { MCPTools } from '@ai-capabilities-suite/mcp-screenshot'; * * const tools = new MCPTools({ * allowedDirectories: ['/home/user/screenshots'], * maxCapturesPerMinute: 60, * enableAuditLog: true * }); * * const result = await tools.captureFullScreen({ * format: 'png', * savePath: '/home/user/screenshots/test.png' * }); * ``` */ export class MCPTools { private captureEngine = createCaptureEngine(); private imageProcessor = new ImageProcessor(); private privacyManager: PrivacyManager; private securityManager: SecurityManager; private fileOperations = new FileOperations(); /** * Create a new MCPTools instance * * @param {Partial<SecurityPolicy>} securityPolicy - Optional security policy configuration * @param {string[]} excludedWindowPatterns - Optional window title patterns to exclude from captures * * @example * ```typescript * const tools = new MCPTools({ * allowedDirectories: ['/home/user/screenshots'], * blockedWindowPatterns: ['.*Password.*'], * maxCapturesPerMinute: 60, * enableAuditLog: true * }, ['.*1Password.*', '.*LastPass.*']); * ``` */ constructor( securityPolicy?: Partial<SecurityPolicy>, excludedWindowPatterns?: string[] ) { this.securityManager = new SecurityManager(securityPolicy); this.privacyManager = new PrivacyManager(excludedWindowPatterns); } /** * Cleanup resources * * Releases resources used by the privacy manager (e.g., Tesseract worker). * Should be called when the tools instance is no longer needed. * * @returns {Promise<void>} * * @example * ```typescript * const tools = new MCPTools(); * // ... use tools ... * await tools.cleanup(); * ``` */ async cleanup(): Promise<void> { await this.privacyManager.cleanup(); } /** * Tool: screenshot_capture_full * * Capture full screen screenshot with optional PII masking and format selection. * * @param {Object} args - Capture arguments * @param {string} [args.display] - Display ID to capture (defaults to primary display) * @param {ImageFormat} [args.format='png'] - Image format (png, jpeg, webp, bmp) * @param {number} [args.quality=90] - Compression quality for lossy formats (1-100) * @param {string} [args.savePath] - File path to save screenshot (returns base64 if not provided) * @param {boolean} [args.enablePIIMasking=false] - Enable PII detection and masking * * @returns {Promise<ScreenshotResponse>} Screenshot response with metadata * * @throws {DisplayNotFoundError} If specified display does not exist * @throws {PathValidationError} If save path is outside allowed directories * @throws {RateLimitError} If rate limit is exceeded * @throws {CaptureFailedError} If screenshot capture fails * * @example * ```typescript * // Capture primary display and save to file * const result = await tools.captureFullScreen({ * format: 'png', * savePath: '/home/user/screenshots/desktop.png' * }); * * // Capture specific display with PII masking * const result = await tools.captureFullScreen({ * display: '1', * format: 'jpeg', * quality: 85, * enablePIIMasking: true * }); * ``` */ async captureFullScreen(args: { display?: string; format?: ImageFormat; quality?: number; savePath?: string; enablePIIMasking?: boolean; }): Promise<ScreenshotResponse> { try { // Check rate limit (using a default agent ID for now) this.securityManager.checkRateLimit("default"); // Validate save path if provided if (args.savePath) { this.securityManager.validatePath(args.savePath); } // Set defaults const format = args.format || "png"; const quality = args.quality; const enablePIIMasking = args.enablePIIMasking || false; // Capture screen let buffer = await this.captureEngine.captureScreen(args.display); // Get display info for metadata const displays = await this.captureEngine.getDisplays(); const display = args.display ? displays.find((d) => d.id === args.display) : displays.find((d) => d.isPrimary); if (args.display && !display) { throw new DisplayNotFoundError(`Display not found: ${args.display}`, { displayId: args.display, }); } // Apply PII masking if enabled let maskingStats; if (enablePIIMasking) { const result = await this.privacyManager.maskPII(buffer); buffer = result.maskedBuffer; maskingStats = result.stats; } // Encode in requested format const encoded = await this.imageProcessor.encode(buffer, format, quality); // Get metadata const imageMetadata = await this.imageProcessor.getMetadata(encoded); const metadata: ScreenshotMetadata = { width: imageMetadata.width, height: imageMetadata.height, format, fileSize: imageMetadata.size, timestamp: new Date().toISOString(), display, piiMasking: maskingStats, }; // Save or return base64 let response: ScreenshotResponse; if (args.savePath) { const { absolutePath, fileSize } = await this.fileOperations.saveToFile( encoded, args.savePath ); response = { status: "success", filePath: absolutePath, metadata: { ...metadata, fileSize, }, }; } else { const { data, mimeType } = this.fileOperations.encodeBase64( encoded, format ); response = { status: "success", data, mimeType, metadata, }; } // Audit log this.securityManager.auditLog("screenshot_capture_full", args, response); return response; } catch (error) { const errorResponse = formatErrorResponse(error); // Audit log error this.securityManager.auditLog( "screenshot_capture_full", args, errorResponse ); return errorResponse; } } /** * Tool: screenshot_capture_window * * Capture specific application window by ID or title pattern. * * @param {Object} args - Capture arguments * @param {string} [args.windowId] - Window identifier (use windowId or windowTitle) * @param {string} [args.windowTitle] - Window title pattern to match (use windowId or windowTitle) * @param {boolean} [args.includeFrame=false] - Include window frame and title bar * @param {ImageFormat} [args.format='png'] - Image format (png, jpeg, webp, bmp) * @param {number} [args.quality=90] - Compression quality for lossy formats (1-100) * @param {string} [args.savePath] - File path to save screenshot (returns base64 if not provided) * * @returns {Promise<ScreenshotResponse>} Screenshot response with window metadata * * @throws {WindowNotFoundError} If specified window does not exist or is minimized * @throws {PathValidationError} If save path is outside allowed directories * @throws {RateLimitError} If rate limit is exceeded * @throws {CaptureFailedError} If screenshot capture fails * * @example * ```typescript * // Capture window by ID * const result = await tools.captureWindow({ * windowId: '12345', * format: 'png' * }); * * // Capture window by title pattern with frame * const result = await tools.captureWindow({ * windowTitle: 'Chrome', * includeFrame: true, * format: 'jpeg', * quality: 85 * }); * ``` * * @remarks * - Either windowId or windowTitle must be provided * - Window title matching is case-insensitive and supports partial matches * - Minimized windows cannot be captured */ async captureWindow(args: { windowId?: string; windowTitle?: string; includeFrame?: boolean; format?: ImageFormat; quality?: number; savePath?: string; }): Promise<ScreenshotResponse> { try { // Check rate limit this.securityManager.checkRateLimit("default"); // Validate save path if provided if (args.savePath) { this.securityManager.validatePath(args.savePath); } // Set defaults const format = args.format || "png"; const quality = args.quality; const includeFrame = args.includeFrame || false; // Find window let window; if (args.windowId) { window = await this.captureEngine.getWindowById(args.windowId); } else if (args.windowTitle) { window = await this.captureEngine.getWindowByTitle(args.windowTitle); } else { throw new WindowNotFoundError( "Either windowId or windowTitle must be provided" ); } if (!window) { throw new WindowNotFoundError( args.windowId ? `Window not found: ${args.windowId}` : `Window not found with title: ${args.windowTitle}`, { windowId: args.windowId, windowTitle: args.windowTitle, } ); } if (window.isMinimized) { throw new WindowNotFoundError("Cannot capture minimized window", { windowId: window.id, windowTitle: window.title, }); } // Capture window let buffer = await this.captureEngine.captureWindow( window.id, includeFrame ); // Encode in requested format const encoded = await this.imageProcessor.encode(buffer, format, quality); // Get metadata const imageMetadata = await this.imageProcessor.getMetadata(encoded); const metadata: ScreenshotMetadata = { width: imageMetadata.width, height: imageMetadata.height, format, fileSize: imageMetadata.size, timestamp: new Date().toISOString(), window: { id: window.id, title: window.title, processName: window.processName, pid: window.pid, bounds: window.bounds, isMinimized: window.isMinimized, }, }; // Save or return base64 let response: ScreenshotResponse; if (args.savePath) { const { absolutePath, fileSize } = await this.fileOperations.saveToFile( encoded, args.savePath ); response = { status: "success", filePath: absolutePath, metadata: { ...metadata, fileSize, }, }; } else { const { data, mimeType } = this.fileOperations.encodeBase64( encoded, format ); response = { status: "success", data, mimeType, metadata, }; } // Audit log this.securityManager.auditLog( "screenshot_capture_window", args, response ); return response; } catch (error) { const errorResponse = formatErrorResponse(error); // Audit log error this.securityManager.auditLog( "screenshot_capture_window", args, errorResponse ); return errorResponse; } } /** * Tool: screenshot_capture_region * * Capture specific rectangular region of the screen by coordinates. * * @param {Object} args - Capture arguments * @param {number} args.x - X coordinate of top-left corner (must be non-negative) * @param {number} args.y - Y coordinate of top-left corner (must be non-negative) * @param {number} args.width - Width of region in pixels (must be positive) * @param {number} args.height - Height of region in pixels (must be positive) * @param {ImageFormat} [args.format='png'] - Image format (png, jpeg, webp, bmp) * @param {number} [args.quality=90] - Compression quality for lossy formats (1-100) * @param {string} [args.savePath] - File path to save screenshot (returns base64 if not provided) * * @returns {Promise<ScreenshotResponse>} Screenshot response with region metadata * * @throws {InvalidRegionError} If coordinates are negative or dimensions are non-positive * @throws {PathValidationError} If save path is outside allowed directories * @throws {RateLimitError} If rate limit is exceeded * @throws {CaptureFailedError} If screenshot capture fails * * @example * ```typescript * // Capture 800x600 region starting at (100, 100) * const result = await tools.captureRegion({ * x: 100, * y: 100, * width: 800, * height: 600, * format: 'png' * }); * ``` * * @remarks * - Coordinates are relative to the virtual desktop coordinate system * - If region extends beyond screen boundaries, only the visible portion is captured * - Actual captured dimensions are reported in metadata (may differ if clipped) */ async captureRegion(args: { x: number; y: number; width: number; height: number; format?: ImageFormat; quality?: number; savePath?: string; }): Promise<ScreenshotResponse> { try { // Check rate limit this.securityManager.checkRateLimit("default"); // Validate save path if provided if (args.savePath) { this.securityManager.validatePath(args.savePath); } // Set defaults const format = args.format || "png"; const quality = args.quality; // Validate dimensions if (args.x < 0 || args.y < 0) { throw new InvalidRegionError( "Region coordinates must be non-negative", { x: args.x, y: args.y } ); } if (args.width <= 0 || args.height <= 0) { throw new InvalidRegionError("Region dimensions must be positive", { width: args.width, height: args.height, }); } // Capture region (validation and clipping handled by captureEngine) let buffer = await this.captureEngine.captureRegion( args.x, args.y, args.width, args.height ); // Encode in requested format const encoded = await this.imageProcessor.encode(buffer, format, quality); // Get metadata const imageMetadata = await this.imageProcessor.getMetadata(encoded); const metadata: ScreenshotMetadata = { width: imageMetadata.width, height: imageMetadata.height, format, fileSize: imageMetadata.size, timestamp: new Date().toISOString(), region: { x: args.x, y: args.y, width: imageMetadata.width, // Actual captured width (may be clipped) height: imageMetadata.height, // Actual captured height (may be clipped) }, }; // Save or return base64 let response: ScreenshotResponse; if (args.savePath) { const { absolutePath, fileSize } = await this.fileOperations.saveToFile( encoded, args.savePath ); response = { status: "success", filePath: absolutePath, metadata: { ...metadata, fileSize, }, }; } else { const { data, mimeType } = this.fileOperations.encodeBase64( encoded, format ); response = { status: "success", data, mimeType, metadata, }; } // Audit log this.securityManager.auditLog( "screenshot_capture_region", args, response ); return response; } catch (error) { const errorResponse = formatErrorResponse(error); // Audit log error this.securityManager.auditLog( "screenshot_capture_region", args, errorResponse ); return errorResponse; } } /** * Tool: screenshot_list_displays * * List all connected displays with resolution and position information. * * @returns {Promise<Object>} Response containing display information * @returns {string} return.status - Operation status ('success' or 'error') * @returns {Array} [return.displays] - Array of display information objects * @returns {Object} [return.error] - Error information if operation failed * * @example * ```typescript * const result = await tools.listDisplays(); * if (result.status === 'success') { * result.displays.forEach(display => { * console.log(`${display.name}: ${display.resolution.width}x${display.resolution.height}`); * }); * } * ``` * * @remarks * - Display IDs can be used with captureFullScreen to capture specific displays * - Position coordinates are relative to the virtual desktop coordinate system * - Primary display is indicated by isPrimary flag */ async listDisplays(): Promise<{ status: "success" | "error"; displays?: Array<{ id: string; name: string; resolution: { width: number; height: number }; position: { x: number; y: number }; isPrimary: boolean; }>; error?: { code: string; message: string; details?: any; }; }> { try { const displays = await this.captureEngine.getDisplays(); const response = { status: "success" as const, displays: displays.map((d) => ({ id: d.id, name: d.name, resolution: d.resolution, position: d.position, isPrimary: d.isPrimary, })), }; // Audit log this.securityManager.auditLog("screenshot_list_displays", {}, response); return response; } catch (error) { const errorResponse = formatErrorResponse(error); // Audit log error this.securityManager.auditLog( "screenshot_list_displays", {}, errorResponse ); return errorResponse; } } /** * Tool: screenshot_list_windows * * List all visible windows with title, process, and position information. * * @returns {Promise<Object>} Response containing window information * @returns {string} return.status - Operation status ('success' or 'error') * @returns {Array} [return.windows] - Array of window information objects * @returns {Object} [return.error] - Error information if operation failed * * @example * ```typescript * const result = await tools.listWindows(); * if (result.status === 'success') { * result.windows.forEach(window => { * console.log(`${window.title} (${window.processName})`); * }); * } * ``` * * @remarks * - Window IDs can be used with captureWindow to capture specific windows * - Minimized windows are included but cannot be captured * - Window titles may be truncated by the operating system */ async listWindows(): Promise<{ status: "success" | "error"; windows?: Array<{ id: string; title: string; processName: string; pid: number; bounds: { x: number; y: number; width: number; height: number }; isMinimized: boolean; }>; error?: { code: string; message: string; details?: any; }; }> { try { const windows = await this.captureEngine.getWindows(); const response = { status: "success" as const, windows: windows.map((w) => ({ id: w.id, title: w.title, processName: w.processName, pid: w.pid, bounds: w.bounds, isMinimized: w.isMinimized, })), }; // Audit log this.securityManager.auditLog("screenshot_list_windows", {}, response); return response; } catch (error) { const errorResponse = formatErrorResponse(error); // Audit log error this.securityManager.auditLog( "screenshot_list_windows", {}, errorResponse ); return errorResponse; } } }

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/Digital-Defiance/mcp-screenshot'

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