Skip to main content
Glama

Firefox MCP Server

by JediLuke
index-multi-debug.js43 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { chromium, firefox } from 'playwright'; class DebugEnhancedFirefoxMCPServer { constructor() { this.server = new Server( { name: 'firefox-debug-mcp-server', version: '3.0.0', }, { capabilities: { tools: {}, }, } ); this.browser = null; this.contexts = new Map(); // Map of contextId -> BrowserContext this.pages = new Map(); // Map of tabId -> Page this.activeTabId = null; // Debug event buffers - per tab this.consoleLogs = new Map(); // tabId -> array of log events this.jsErrors = new Map(); // tabId -> array of error events this.networkActivity = new Map(); // tabId -> array of network events this.wsMessages = new Map(); // tabId -> array of WebSocket messages this.performanceMetrics = new Map(); // tabId -> performance data this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Original tools (abbreviated for space) { name: 'launch_firefox_multi', description: 'Launch Firefox browser with multi-tab support and debugging', inputSchema: { type: 'object', properties: { headless: { type: 'boolean', default: false }, enableDebugLogging: { type: 'boolean', default: true } } } }, { name: 'create_tab', description: 'Create a new tab with isolated session and debugging', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, url: { type: 'string', default: 'about:blank' }, contextId: { type: 'string' }, enableMonitoring: { type: 'boolean', default: true } }, required: ['tabId'] } }, { name: 'list_tabs', description: 'List all active tabs', inputSchema: { type: 'object', properties: {} } }, { name: 'close_tab', description: 'Close a specific tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } }, required: ['tabId'] } }, { name: 'navigate', description: 'Navigate to a URL', inputSchema: { type: 'object', properties: { url: { type: 'string' }, tabId: { type: 'string' } }, required: ['url'] } }, { name: 'click', description: 'Click on an element', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, coordinates: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } }, tabId: { type: 'string' } } } }, { name: 'type_text', description: 'Type text into an input field', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, text: { type: 'string' }, tabId: { type: 'string' } }, required: ['selector', 'text'] } }, { name: 'send_key', description: 'Send keyboard events', inputSchema: { type: 'object', properties: { key: { type: 'string' }, selector: { type: 'string' }, modifiers: { type: 'array', items: { type: 'string' } }, repeat: { type: 'number', default: 1 }, tabId: { type: 'string' } }, required: ['key'] } }, { name: 'drag', description: 'Perform drag operation', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, fromCoordinates: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } }, toCoordinates: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } }, offsetX: { type: 'number' }, offsetY: { type: 'number' }, duration: { type: 'number', default: 0 }, steps: { type: 'number', default: 1 }, tabId: { type: 'string' } } } }, { name: 'execute_script', description: 'Execute JavaScript in the browser', inputSchema: { type: 'object', properties: { script: { type: 'string' }, tabId: { type: 'string' } }, required: ['script'] } }, { name: 'screenshot', description: 'Take a screenshot', inputSchema: { type: 'object', properties: { path: { type: 'string', default: 'screenshot.png' }, fullPage: { type: 'boolean', default: false }, tabId: { type: 'string' } } } }, { name: 'get_page_content', description: 'Get HTML content', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, tabId: { type: 'string' } } } }, { name: 'get_page_text', description: 'Get visible text content', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, tabId: { type: 'string' } } } }, { name: 'wait_for_element', description: 'Wait for element to appear', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number', default: 30000 }, tabId: { type: 'string' } }, required: ['selector'] } }, { name: 'close_browser', description: 'Close browser and all tabs', inputSchema: { type: 'object', properties: {} } }, { name: 'get_current_url', description: 'Get current page URL', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } } } }, { name: 'back', description: 'Navigate back', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } } } }, { name: 'forward', description: 'Navigate forward', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } } } }, { name: 'reload', description: 'Reload page', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } } } }, { name: 'set_active_tab', description: 'Set active tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } }, required: ['tabId'] } }, // NEW DEBUG TOOLS { name: 'get_console_logs', description: 'Get captured console logs from browser', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, since: { type: 'number', description: 'Timestamp to filter logs since' }, types: { type: 'array', items: { type: 'string', enum: ['log', 'error', 'warn', 'info', 'debug'] }, description: 'Filter by log types' }, limit: { type: 'number', default: 50, description: 'Max number of logs to return' } } } }, { name: 'get_javascript_errors', description: 'Get captured JavaScript errors', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, since: { type: 'number' }, limit: { type: 'number', default: 20 } } } }, { name: 'get_network_activity', description: 'Get captured network requests and responses', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, since: { type: 'number' }, filter: { type: 'string', enum: ['all', 'xhr', 'websocket', 'fetch'], default: 'all' }, limit: { type: 'number', default: 30 } } } }, { name: 'get_websocket_messages', description: 'Get captured WebSocket messages (for LiveView debugging)', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, since: { type: 'number' }, limit: { type: 'number', default: 50 } } } }, { name: 'get_performance_metrics', description: 'Get performance metrics (timing, memory usage)', inputSchema: { type: 'object', properties: { tabId: { type: 'string' } } } }, { name: 'start_monitoring', description: 'Start/restart monitoring for a tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, types: { type: 'array', items: { type: 'string', enum: ['console', 'errors', 'network', 'websocket', 'performance'] }, default: ['console', 'errors', 'network', 'websocket'] } } } }, { name: 'clear_debug_buffers', description: 'Clear debug event buffers for a tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, types: { type: 'array', items: { type: 'string', enum: ['console', 'errors', 'network', 'websocket'] } } } } }, { name: 'get_all_debug_activity', description: 'Get combined feed of all debug events', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, since: { type: 'number' }, limit: { type: 'number', default: 100 } } } }, { name: 'inject_debugging_helpers', description: 'Inject debugging helper functions into the page', inputSchema: { type: 'object', properties: { tabId: { type: 'string' }, includeWebSocketMonitoring: { type: 'boolean', default: true } } } } ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // Original tools case 'launch_firefox_multi': return await this.launchFirefoxMulti(args); case 'create_tab': return await this.createTab(args); case 'list_tabs': return await this.listTabs(); case 'close_tab': return await this.closeTab(args); case 'set_active_tab': return await this.setActiveTab(args); case 'navigate': return await this.navigate(args); case 'click': return await this.click(args); case 'type_text': return await this.typeText(args); case 'send_key': return await this.sendKey(args); case 'drag': return await this.drag(args); case 'get_page_content': return await this.getPageContent(args); case 'get_page_text': return await this.getPageText(args); case 'screenshot': return await this.screenshot(args); case 'wait_for_element': return await this.waitForElement(args); case 'execute_script': return await this.executeScript(args); case 'close_browser': return await this.closeBrowser(); case 'get_current_url': return await this.getCurrentUrl(args); case 'back': return await this.back(args); case 'forward': return await this.forward(args); case 'reload': return await this.reload(args); // NEW DEBUG TOOLS case 'get_console_logs': return await this.getConsoleLogs(args); case 'get_javascript_errors': return await this.getJavaScriptErrors(args); case 'get_network_activity': return await this.getNetworkActivity(args); case 'get_websocket_messages': return await this.getWebSocketMessages(args); case 'get_performance_metrics': return await this.getPerformanceMetrics(args); case 'start_monitoring': return await this.startMonitoring(args); case 'clear_debug_buffers': return await this.clearDebugBuffers(args); case 'get_all_debug_activity': return await this.getAllDebugActivity(args); case 'inject_debugging_helpers': return await this.injectDebuggingHelpers(args); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError(ErrorCode.InternalError, `Error executing ${name}: ${error.message}`); } }); } // Initialize debug buffers for a tab initDebugBuffers(tabId) { this.consoleLogs.set(tabId, []); this.jsErrors.set(tabId, []); this.networkActivity.set(tabId, []); this.wsMessages.set(tabId, []); this.performanceMetrics.set(tabId, { startTime: Date.now(), metrics: [] }); } // Clear debug buffers for a tab clearTabDebugBuffers(tabId) { this.consoleLogs.delete(tabId); this.jsErrors.delete(tabId); this.networkActivity.delete(tabId); this.wsMessages.delete(tabId); this.performanceMetrics.delete(tabId); } // Setup monitoring listeners for a page async setupPageMonitoring(page, tabId) { console.error(`Setting up monitoring for tab: ${tabId}`); // Console monitoring page.on('console', (msg) => { const logs = this.consoleLogs.get(tabId) || []; logs.push({ type: msg.type(), text: msg.text(), location: msg.location(), timestamp: Date.now() }); this.consoleLogs.set(tabId, logs); }); // JavaScript error monitoring page.on('pageerror', (error) => { const errors = this.jsErrors.get(tabId) || []; errors.push({ message: error.message, stack: error.stack, timestamp: Date.now() }); this.jsErrors.set(tabId, errors); }); // Network monitoring page.on('request', (request) => { const activity = this.networkActivity.get(tabId) || []; activity.push({ type: 'request', url: request.url(), method: request.method(), headers: request.headers(), resourceType: request.resourceType(), timestamp: Date.now() }); this.networkActivity.set(tabId, activity); }); page.on('response', (response) => { const activity = this.networkActivity.get(tabId) || []; activity.push({ type: 'response', url: response.url(), status: response.status(), headers: response.headers(), timestamp: Date.now() }); this.networkActivity.set(tabId, activity); }); // Inject WebSocket monitoring await this.injectWebSocketMonitoring(page, tabId); } // Inject WebSocket monitoring code async injectWebSocketMonitoring(page, tabId) { await page.addInitScript(() => { // Store original WebSocket const OriginalWebSocket = window.WebSocket; // Array to store WebSocket messages window._wsMessages = window._wsMessages || []; // Override WebSocket constructor window.WebSocket = function(...args) { const ws = new OriginalWebSocket(...args); // Monitor incoming messages ws.addEventListener('message', (event) => { window._wsMessages.push({ type: 'received', data: event.data, timestamp: Date.now(), url: ws.url }); }); // Monitor outgoing messages const originalSend = ws.send; ws.send = function(data) { window._wsMessages.push({ type: 'sent', data: data, timestamp: Date.now(), url: ws.url }); return originalSend.call(this, data); }; return ws; }; // Copy static properties Object.setPrototypeOf(window.WebSocket, OriginalWebSocket); Object.defineProperty(window.WebSocket, 'prototype', { value: OriginalWebSocket.prototype, writable: false }); }); } async launchFirefoxMulti(args = {}) { const { headless = false, enableDebugLogging = true } = args; try { this.browser = await firefox.launch({ headless, firefoxUserPrefs: { 'dom.webnotifications.enabled': false, 'media.navigator.permission.disabled': true } }); return { content: [{ type: 'text', text: `Firefox launched successfully with debugging capabilities. Debug logging: ${enableDebugLogging ? 'enabled' : 'disabled'}` }] }; } catch (error) { throw new Error(`Failed to launch Firefox: ${error.message}`); } } async createTab(args) { this.ensureBrowserRunning(); const { tabId, url = 'about:blank', contextId, enableMonitoring = true } = args; if (this.pages.has(tabId)) { throw new Error(`Tab with ID '${tabId}' already exists`); } // Create or reuse context const effectiveContextId = contextId || `context-${tabId}`; let context; if (this.contexts.has(effectiveContextId)) { context = this.contexts.get(effectiveContextId); } else { context = await this.browser.newContext({ storageState: undefined // Start with clean state }); this.contexts.set(effectiveContextId, context); } // Create new page in the context const page = await context.newPage(); // Initialize debug buffers this.initDebugBuffers(tabId); // Setup monitoring if enabled if (enableMonitoring) { await this.setupPageMonitoring(page, tabId); } await page.goto(url); this.pages.set(tabId, page); // Set as active tab if no active tab exists if (!this.activeTabId) { this.activeTabId = tabId; } return { content: [{ type: 'text', text: `Tab '${tabId}' created with debugging ${enableMonitoring ? 'enabled' : 'disabled'}. URL: ${url}. Context: ${effectiveContextId}` }] }; } // DEBUG TOOL IMPLEMENTATIONS async getConsoleLogs(args = {}) { const { tabId, since, types, limit = 50 } = args; const effectiveTabId = tabId || this.activeTabId; if (!effectiveTabId || !this.consoleLogs.has(effectiveTabId)) { return { content: [{ type: 'text', text: 'No console logs available for this tab' }] }; } let logs = this.consoleLogs.get(effectiveTabId); // Filter by timestamp if (since) { logs = logs.filter(log => log.timestamp >= since); } // Filter by types if (types && types.length > 0) { logs = logs.filter(log => types.includes(log.type)); } // Limit results logs = logs.slice(-limit); return { content: [{ type: 'text', text: `Console Logs (${logs.length}):\n` + JSON.stringify(logs, null, 2) }] }; } async getJavaScriptErrors(args = {}) { const { tabId, since, limit = 20 } = args; const effectiveTabId = tabId || this.activeTabId; if (!effectiveTabId || !this.jsErrors.has(effectiveTabId)) { return { content: [{ type: 'text', text: 'No JavaScript errors captured for this tab' }] }; } let errors = this.jsErrors.get(effectiveTabId); if (since) { errors = errors.filter(error => error.timestamp >= since); } errors = errors.slice(-limit); return { content: [{ type: 'text', text: `JavaScript Errors (${errors.length}):\n` + JSON.stringify(errors, null, 2) }] }; } async getNetworkActivity(args = {}) { const { tabId, since, filter = 'all', limit = 30 } = args; const effectiveTabId = tabId || this.activeTabId; if (!effectiveTabId || !this.networkActivity.has(effectiveTabId)) { return { content: [{ type: 'text', text: 'No network activity captured for this tab' }] }; } let activity = this.networkActivity.get(effectiveTabId); if (since) { activity = activity.filter(item => item.timestamp >= since); } if (filter !== 'all') { switch (filter) { case 'xhr': activity = activity.filter(item => item.resourceType === 'xhr'); break; case 'websocket': activity = activity.filter(item => item.resourceType === 'websocket'); break; case 'fetch': activity = activity.filter(item => item.resourceType === 'fetch'); break; } } activity = activity.slice(-limit); return { content: [{ type: 'text', text: `Network Activity (${activity.length}):\n` + JSON.stringify(activity, null, 2) }] }; } async getWebSocketMessages(args = {}) { const { tabId, since, limit = 50 } = args; const effectiveTabId = tabId || this.activeTabId; const page = this.getPage(effectiveTabId); // Get WebSocket messages from injected monitoring const wsMessages = await page.evaluate(() => { return window._wsMessages || []; }); let filteredMessages = wsMessages; if (since) { filteredMessages = filteredMessages.filter(msg => msg.timestamp >= since); } filteredMessages = filteredMessages.slice(-limit); return { content: [{ type: 'text', text: `WebSocket Messages (${filteredMessages.length}):\n` + JSON.stringify(filteredMessages, null, 2) }] }; } async getPerformanceMetrics(args = {}) { const { tabId } = args; const effectiveTabId = tabId || this.activeTabId; const page = this.getPage(effectiveTabId); // Get performance metrics from browser const metrics = await page.evaluate(() => { const nav = performance.getEntriesByType('navigation')[0]; const paint = performance.getEntriesByType('paint'); return { navigation: nav ? { domContentLoaded: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart, loadComplete: nav.loadEventEnd - nav.loadEventStart, totalTime: nav.loadEventEnd - nav.fetchStart } : null, paint: paint.map(p => ({ name: p.name, startTime: p.startTime })), memory: performance.memory ? { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit } : null, timestamp: Date.now() }; }); return { content: [{ type: 'text', text: `Performance Metrics:\n` + JSON.stringify(metrics, null, 2) }] }; } async getAllDebugActivity(args = {}) { const { tabId, since, limit = 100 } = args; const effectiveTabId = tabId || this.activeTabId; // Combine all debug events with timestamps const allEvents = []; // Console logs const logs = this.consoleLogs.get(effectiveTabId) || []; logs.forEach(log => allEvents.push({ ...log, source: 'console' })); // JavaScript errors const errors = this.jsErrors.get(effectiveTabId) || []; errors.forEach(error => allEvents.push({ ...error, source: 'error' })); // Network activity const network = this.networkActivity.get(effectiveTabId) || []; network.forEach(activity => allEvents.push({ ...activity, source: 'network' })); // WebSocket messages (get from page) if (this.pages.has(effectiveTabId)) { const page = this.pages.get(effectiveTabId); const wsMessages = await page.evaluate(() => window._wsMessages || []); wsMessages.forEach(msg => allEvents.push({ ...msg, source: 'websocket' })); } // Sort by timestamp allEvents.sort((a, b) => a.timestamp - b.timestamp); // Filter by since let filtered = since ? allEvents.filter(event => event.timestamp >= since) : allEvents; // Limit results filtered = filtered.slice(-limit); return { content: [{ type: 'text', text: `All Debug Activity (${filtered.length} events):\n` + JSON.stringify(filtered, null, 2) }] }; } async clearDebugBuffers(args = {}) { const { tabId, types } = args; const effectiveTabId = tabId || this.activeTabId; if (!types || types.length === 0) { // Clear all buffers this.consoleLogs.set(effectiveTabId, []); this.jsErrors.set(effectiveTabId, []); this.networkActivity.set(effectiveTabId, []); // Clear WebSocket messages if (this.pages.has(effectiveTabId)) { const page = this.pages.get(effectiveTabId); await page.evaluate(() => { window._wsMessages = []; }); } } else { // Clear specific buffers if (types.includes('console')) this.consoleLogs.set(effectiveTabId, []); if (types.includes('errors')) this.jsErrors.set(effectiveTabId, []); if (types.includes('network')) this.networkActivity.set(effectiveTabId, []); if (types.includes('websocket') && this.pages.has(effectiveTabId)) { const page = this.pages.get(effectiveTabId); await page.evaluate(() => { window._wsMessages = []; }); } } return { content: [{ type: 'text', text: `Debug buffers cleared for tab '${effectiveTabId}': ${types ? types.join(', ') : 'all'}` }] }; } async injectDebuggingHelpers(args = {}) { const { tabId, includeWebSocketMonitoring = true } = args; const effectiveTabId = tabId || this.activeTabId; const page = this.getPage(effectiveTabId); // Inject comprehensive debugging helpers await page.evaluate(() => { // Enhanced console capture window._debugLogs = window._debugLogs || []; if (!window._originalConsole) { window._originalConsole = { log: console.log, error: console.error, warn: console.warn, info: console.info, debug: console.debug }; ['log', 'error', 'warn', 'info', 'debug'].forEach(method => { console[method] = function(...args) { window._debugLogs.push({ type: method, args: args, timestamp: Date.now(), stack: new Error().stack }); window._originalConsole[method].apply(console, args); }; }); } // Helper functions window.getDebugLogs = () => JSON.stringify(window._debugLogs, null, 2); window.clearDebugLogs = () => window._debugLogs = []; window.getWSMessages = () => JSON.stringify(window._wsMessages || [], null, 2); window.clearWSMessages = () => window._wsMessages = []; // Global error handler if (!window._errorHandlerInstalled) { window.addEventListener('error', (event) => { window._debugLogs.push({ type: 'uncaught-error', message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error ? event.error.toString() : null, timestamp: Date.now() }); }); window.addEventListener('unhandledrejection', (event) => { window._debugLogs.push({ type: 'unhandled-promise-rejection', reason: event.reason ? event.reason.toString() : 'Unknown', timestamp: Date.now() }); }); window._errorHandlerInstalled = true; } }); return { content: [{ type: 'text', text: `Debugging helpers injected into tab '${effectiveTabId}'. Use window.getDebugLogs(), window.getWSMessages(), etc.` }] }; } // Copy all the original methods from the base class (abbreviated for space) async listTabs() { const tabs = []; for (const [tabId, page] of this.pages) { const url = page.url(); const isActive = tabId === this.activeTabId; tabs.push({ tabId, url, active: isActive }); } return { content: [{ type: 'text', text: `Active tabs (${tabs.length}):\n` + tabs.map(tab => `- ${tab.tabId}: ${tab.url}${tab.active ? ' (active)' : ''}`).join('\n') }] }; } async closeTab(args) { const { tabId } = args; if (!this.pages.has(tabId)) { throw new Error(`Tab '${tabId}' not found`); } const page = this.pages.get(tabId); await page.close(); this.pages.delete(tabId); // Clear debug buffers this.clearTabDebugBuffers(tabId); // If this was the active tab, clear active tab if (this.activeTabId === tabId) { this.activeTabId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null; } return { content: [{ type: 'text', text: `Tab '${tabId}' closed and debug buffers cleared.${this.activeTabId ? ` Active tab is now '${this.activeTabId}'` : ''}` }] }; } async setActiveTab(args) { const { tabId } = args; if (!this.pages.has(tabId)) { throw new Error(`Tab '${tabId}' not found`); } this.activeTabId = tabId; return { content: [{ type: 'text', text: `Active tab set to '${tabId}'` }] }; } getPage(tabId) { if (tabId) { if (!this.pages.has(tabId)) { throw new Error(`Tab '${tabId}' not found`); } return this.pages.get(tabId); } else { if (!this.activeTabId || !this.pages.has(this.activeTabId)) { throw new Error('No active tab. Use create_tab or set_active_tab first.'); } return this.pages.get(this.activeTabId); } } // Implement all remaining methods from original (navigate, click, etc.) // For brevity, I'll include a few key ones: async navigate(args) { this.ensureBrowserRunning(); const { url, tabId } = args; const page = this.getPage(tabId); await page.goto(url); return { content: [{ type: 'text', text: `Tab '${tabId || this.activeTabId}' navigated to: ${url}` }] }; } async click(args) { this.ensureBrowserRunning(); const { selector, coordinates, tabId } = args; const page = this.getPage(tabId); if (coordinates) { await page.click(`body`, { position: coordinates }); return { content: [{ type: 'text', text: `Clicked at coordinates (${coordinates.x}, ${coordinates.y}) in tab '${tabId || this.activeTabId}'` }] }; } else if (selector) { await page.click(selector); return { content: [{ type: 'text', text: `Clicked element '${selector}' in tab '${tabId || this.activeTabId}'` }] }; } else { throw new Error('Either selector or coordinates must be provided'); } } async executeScript(args) { this.ensureBrowserRunning(); const { script, tabId } = args; const page = this.getPage(tabId); const result = await page.evaluate(script); return { content: [{ type: 'text', text: `Script executed in tab '${tabId || this.activeTabId}'. Result: ${JSON.stringify(result)}` }] }; } // Complete remaining method implementations async typeText(args) { this.ensureBrowserRunning(); const { selector, text, tabId } = args; const page = this.getPage(tabId); await page.fill(selector, text); return { content: [{ type: 'text', text: `Typed "${text}" into '${selector}' in tab '${tabId || this.activeTabId}'` }] }; } async sendKey(args) { this.ensureBrowserRunning(); const { key, selector, modifiers = [], repeat = 1, tabId } = args; const page = this.getPage(tabId); // If selector is provided, focus the element first if (selector) { await page.focus(selector); } // Build modifier string for Playwright const modifierString = modifiers.length > 0 ? modifiers.join('+') + '+' : ''; const fullKey = modifierString + key; // Press the key the specified number of times for (let i = 0; i < repeat; i++) { await page.keyboard.press(fullKey); // Small delay between repeated presses to ensure they register if (repeat > 1 && i < repeat - 1) { await new Promise(resolve => setTimeout(resolve, 50)); } } return { content: [{ type: 'text', text: `Sent key '${fullKey}'${repeat > 1 ? ` ${repeat} times` : ''}${selector ? ` to element '${selector}'` : ''} in tab '${tabId || this.activeTabId}'` }] }; } async drag(args) { this.ensureBrowserRunning(); const { selector, fromCoordinates, toSelector, toCoordinates, offsetX, offsetY, duration = 0, steps = 1, tabId } = args; const page = this.getPage(tabId); // Validate inputs if (!selector && !fromCoordinates) { throw new Error('Either selector or fromCoordinates must be provided'); } if (!toSelector && !toCoordinates && offsetX === undefined && offsetY === undefined) { throw new Error('Either toSelector, toCoordinates, or offset values must be provided'); } // Get starting position let startX, startY; if (selector) { const element = await page.$(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } const box = await element.boundingBox(); if (!box) { throw new Error(`Cannot get bounding box for element: ${selector}`); } startX = box.x + box.width / 2; startY = box.y + box.height / 2; } else { startX = fromCoordinates.x; startY = fromCoordinates.y; } // Get ending position let endX, endY; if (toSelector) { const element = await page.$(toSelector); if (!element) { throw new Error(`Target element not found: ${toSelector}`); } const box = await element.boundingBox(); if (!box) { throw new Error(`Cannot get bounding box for target element: ${toSelector}`); } endX = box.x + box.width / 2; endY = box.y + box.height / 2; } else if (toCoordinates) { endX = toCoordinates.x; endY = toCoordinates.y; } else { // Use offset from start position endX = startX + (offsetX || 0); endY = startY + (offsetY || 0); } // Perform the drag await page.mouse.move(startX, startY); await page.mouse.down(); if (duration > 0 && steps > 1) { // Smooth drag with intermediate steps const stepDelay = duration / steps; for (let i = 1; i <= steps; i++) { const progress = i / steps; const currentX = startX + (endX - startX) * progress; const currentY = startY + (endY - startY) * progress; await page.mouse.move(currentX, currentY); if (i < steps) { await new Promise(resolve => setTimeout(resolve, stepDelay)); } } } else { // Direct drag await page.mouse.move(endX, endY); } await page.mouse.up(); return { content: [{ type: 'text', text: `Dragged from (${Math.round(startX)}, ${Math.round(startY)}) to (${Math.round(endX)}, ${Math.round(endY)}) in tab '${tabId || this.activeTabId}'${duration > 0 ? ` over ${duration}ms` : ''}` }] }; } async getPageContent(args = {}) { this.ensureBrowserRunning(); const { selector, tabId } = args; const page = this.getPage(tabId); let content; if (selector) { content = await page.innerHTML(selector); } else { content = await page.content(); } return { content: [{ type: 'text', text: content }] }; } async getPageText(args = {}) { this.ensureBrowserRunning(); const { selector, tabId } = args; const page = this.getPage(tabId); let text; if (selector) { text = await page.textContent(selector); } else { text = await page.textContent('body'); } return { content: [{ type: 'text', text: text || '' }] }; } async screenshot(args = {}) { this.ensureBrowserRunning(); const { path = 'screenshot.png', fullPage = false, tabId } = args; const page = this.getPage(tabId); // Add tab ID to path if not specified const effectiveTabId = tabId || this.activeTabId; const effectivePath = path.includes(effectiveTabId) ? path : path.replace(/\.([^.]+)$/, `_${effectiveTabId}.$1`); await page.screenshot({ path: effectivePath, fullPage }); return { content: [{ type: 'text', text: `Screenshot of tab '${effectiveTabId}' saved to: ${effectivePath}` }] }; } async waitForElement(args) { this.ensureBrowserRunning(); const { selector, timeout = 30000, tabId } = args; const page = this.getPage(tabId); await page.waitForSelector(selector, { timeout }); return { content: [{ type: 'text', text: `Element '${selector}' found in tab '${tabId || this.activeTabId}'` }] }; } async closeBrowser() { if (this.browser) { await this.browser.close(); this.browser = null; this.contexts.clear(); this.pages.clear(); this.activeTabId = null; // Clear all debug buffers this.consoleLogs.clear(); this.jsErrors.clear(); this.networkActivity.clear(); this.wsMessages.clear(); this.performanceMetrics.clear(); } return { content: [{ type: 'text', text: 'Firefox browser closed, all tabs and debug buffers cleared' }] }; } async getCurrentUrl(args = {}) { this.ensureBrowserRunning(); const { tabId } = args; const page = this.getPage(tabId); const url = page.url(); return { content: [{ type: 'text', text: `Current URL in tab '${tabId || this.activeTabId}': ${url}` }] }; } async back(args = {}) { this.ensureBrowserRunning(); const { tabId } = args; const page = this.getPage(tabId); await page.goBack(); return { content: [{ type: 'text', text: `Navigated back in tab '${tabId || this.activeTabId}'` }] }; } async forward(args = {}) { this.ensureBrowserRunning(); const { tabId } = args; const page = this.getPage(tabId); await page.goForward(); return { content: [{ type: 'text', text: `Navigated forward in tab '${tabId || this.activeTabId}'` }] }; } async reload(args = {}) { this.ensureBrowserRunning(); const { tabId } = args; const page = this.getPage(tabId); await page.reload(); return { content: [{ type: 'text', text: `Page reloaded in tab '${tabId || this.activeTabId}'` }] }; } async startMonitoring(args) { const { tabId, types = ['console', 'errors', 'network', 'websocket'] } = args; const effectiveTabId = tabId || this.activeTabId; const page = this.getPage(effectiveTabId); if (types.includes('console') || types.includes('errors') || types.includes('network')) { await this.setupPageMonitoring(page, effectiveTabId); } if (types.includes('websocket')) { await this.injectWebSocketMonitoring(page, effectiveTabId); } return { content: [{ type: 'text', text: `Monitoring started for tab '${effectiveTabId}': ${types.join(', ')}` }] }; } ensureBrowserRunning() { if (!this.browser) { throw new Error('Firefox browser is not running. Please launch it first using the launch_firefox_multi tool.'); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Debug-Enhanced Firefox MCP server running on stdio'); } } const server = new DebugEnhancedFirefoxMCPServer(); server.run().catch(console.error);

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/JediLuke/firefox-mcp-server'

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