index-multi-debug.js•43 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);