Skip to main content
Glama

Firefox MCP Server

by JediLuke
index-multi-enhanced.js31.9 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 MultiTabFirefoxMCPServer { // constructor() { // this.server = new Server( // { // name: 'firefox-multi-mcp-server', // version: '2.1.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; // this.setupToolHandlers(); // } // setupToolHandlers() { // this.server.setRequestHandler(ListToolsRequestSchema, async () => { // return { // tools: [ // { // name: 'launch_firefox_multi', // description: 'Launch Firefox browser with multi-tab support', // inputSchema: { // type: 'object', // properties: { // headless: { // type: 'boolean', // description: 'Run browser in headless mode', // default: false // } // } // } // }, // { // name: 'create_tab', // description: 'Create a new tab with isolated session', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Unique identifier for this tab' // }, // url: { // type: 'string', // description: 'Initial URL to navigate to', // default: 'about:blank' // }, // contextId: { // type: 'string', // description: 'Context ID for session isolation (optional, auto-generated if not provided)' // } // }, // 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', // description: 'Tab ID to close' // } // }, // required: ['tabId'] // } // }, // { // name: 'navigate', // description: 'Navigate to a URL', // inputSchema: { // type: 'object', // properties: { // url: { // type: 'string', // description: 'URL to navigate to' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // }, // required: ['url'] // } // }, // { // name: 'click', // description: 'Click on an element', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector or text content to click' // }, // coordinates: { // type: 'object', // properties: { // x: { type: 'number' }, // y: { type: 'number' } // }, // description: 'Click at specific coordinates (alternative to selector)' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'type_text', // description: 'Type text into an input field', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector of the input field' // }, // text: { // type: 'string', // description: 'Text to type' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // }, // required: ['selector', 'text'] // } // }, // { // name: 'send_key', // description: 'Send keyboard events to the page or a specific element', // inputSchema: { // type: 'object', // properties: { // key: { // type: 'string', // description: 'Key to press (e.g., "ArrowLeft", "Enter", "a", "A")' // }, // selector: { // type: 'string', // description: 'CSS selector to focus before sending key (optional)' // }, // modifiers: { // type: 'array', // items: { // type: 'string', // enum: ['Shift', 'Control', 'Alt', 'Meta'] // }, // description: 'Modifier keys to hold while pressing the key' // }, // repeat: { // type: 'number', // description: 'Number of times to press the key', // default: 1 // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // }, // required: ['key'] // } // }, // { // name: 'drag', // description: 'Perform a drag operation from one point to another', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector of element to drag from' // }, // fromCoordinates: { // type: 'object', // properties: { // x: { type: 'number' }, // y: { type: 'number' } // }, // description: 'Starting coordinates (alternative to selector)' // }, // toSelector: { // type: 'string', // description: 'CSS selector of element to drag to' // }, // toCoordinates: { // type: 'object', // properties: { // x: { type: 'number' }, // y: { type: 'number' } // }, // description: 'Ending coordinates' // }, // offsetX: { // type: 'number', // description: 'Horizontal offset from start position (alternative to absolute destination)' // }, // offsetY: { // type: 'number', // description: 'Vertical offset from start position (alternative to absolute destination)' // }, // duration: { // type: 'number', // description: 'Duration of drag in milliseconds', // default: 0 // }, // steps: { // type: 'number', // description: 'Number of intermediate mouse move events', // default: 1 // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'get_page_content', // description: 'Get the HTML content of the current page', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector to get specific element content (optional)' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'get_page_text', // description: 'Get the visible text content of the current page', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector to get specific element text (optional)' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'screenshot', // description: 'Take a screenshot of the current page', // inputSchema: { // type: 'object', // properties: { // path: { // type: 'string', // description: 'File path to save screenshot', // default: 'screenshot.png' // }, // fullPage: { // type: 'boolean', // description: 'Capture full page', // default: false // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'wait_for_element', // description: 'Wait for an element to appear on the page', // inputSchema: { // type: 'object', // properties: { // selector: { // type: 'string', // description: 'CSS selector to wait for' // }, // timeout: { // type: 'number', // description: 'Timeout in milliseconds', // default: 30000 // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // }, // required: ['selector'] // } // }, // { // name: 'execute_script', // description: 'Execute JavaScript in the browser', // inputSchema: { // type: 'object', // properties: { // script: { // type: 'string', // description: 'JavaScript code to execute' // }, // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // }, // required: ['script'] // } // }, // { // name: 'close_browser', // description: 'Close the Firefox browser and all tabs', // inputSchema: { // type: 'object', // properties: {} // } // }, // { // name: 'get_current_url', // description: 'Get the current page URL', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'back', // description: 'Navigate back in browser history', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'forward', // description: 'Navigate forward in browser history', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'reload', // description: 'Reload the current page', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Tab ID (uses active tab if not provided)' // } // } // } // }, // { // name: 'set_active_tab', // description: 'Set the active tab for operations that don\'t specify a tabId', // inputSchema: { // type: 'object', // properties: { // tabId: { // type: 'string', // description: 'Tab ID to set as active' // } // }, // required: ['tabId'] // } // } // ], // }; // }); // this.server.setRequestHandler(CallToolRequestSchema, async (request) => { // const { name, arguments: args } = request.params; // try { // switch (name) { // 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); // default: // throw new McpError( // ErrorCode.MethodNotFound, // `Unknown tool: ${name}` // ); // } // } catch (error) { // throw new McpError( // ErrorCode.InternalError, // `Error executing ${name}: ${error.message}` // ); // } // }); // } // async launchFirefoxMulti(args = {}) { // const { headless = false } = 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 multi-tab support. Use create_tab to create new tabs.` // } // ] // }; // } catch (error) { // throw new Error(`Failed to launch Firefox: ${error.message}`); // } // } // async createTab(args) { // this.ensureBrowserRunning(); // const { tabId, url = 'about:blank', contextId } = 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({ // // Each context has isolated storage, cookies, etc. // storageState: undefined // Start with clean state // }); // this.contexts.set(effectiveContextId, context); // } // // Create new page in the context // const page = await context.newPage(); // 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 successfully. Navigated to: ${url}. Context: ${effectiveContextId}` // } // ] // }; // } // 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); // // 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 successfully.${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); // } // } // 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 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 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)}` // } // ] // }; // } // async closeBrowser() { // if (this.browser) { // await this.browser.close(); // this.browser = null; // this.contexts.clear(); // this.pages.clear(); // this.activeTabId = null; // } // return { // content: [ // { // type: 'text', // text: 'Firefox browser closed, all tabs and contexts 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}'` // } // ] // }; // } // 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('Multi-tab Firefox MCP server running on stdio'); // } // } // const server = new MultiTabFirefoxMCPServer(); // 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