UNPKG

browser-agent-mcp

Version:

Chrome extension and MCP server for comprehensive browser automation with AI agents

656 lines (638 loc) 25.1 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ "./src/extension/background/devtools-manager.ts": /*!******************************************************!*\ !*** ./src/extension/background/devtools-manager.ts ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ DevToolsManager: () => (/* binding */ DevToolsManager) /* harmony export */ }); // DevTools Manager for Chrome DevTools integration class DevToolsManager { constructor() { this.consoleLogs = new Map(); this.networkRequests = new Map(); this.attachedTabs = new Set(); } initialize() { console.log("DevToolsManager initialized"); } async attachToTab(tabId) { if (this.attachedTabs.has(tabId)) { return; // Already attached } try { // Attach debugger to tab await chrome.debugger.attach({ tabId }, "1.3"); this.attachedTabs.add(tabId); // Enable Console domain await chrome.debugger.sendCommand({ tabId }, "Console.enable"); // Enable Network domain await chrome.debugger.sendCommand({ tabId }, "Network.enable"); // Listen for console messages chrome.debugger.onEvent.addListener((source, method, params) => { if (source.tabId === tabId) { this.handleDebuggerEvent(tabId, method, params); } }); console.log(`DevTools attached to tab ${tabId}`); } catch (error) { console.error(`Failed to attach DevTools to tab ${tabId}:`, error); } } async detachFromTab(tabId) { if (!this.attachedTabs.has(tabId)) { return; // Not attached } try { await chrome.debugger.detach({ tabId }); this.attachedTabs.delete(tabId); console.log(`DevTools detached from tab ${tabId}`); } catch (error) { console.error(`Failed to detach DevTools from tab ${tabId}:`, error); } } handleDebuggerEvent(tabId, method, params) { switch (method) { case "Console.messageAdded": this.handleConsoleMessage(tabId, params); break; case "Network.requestWillBeSent": this.handleNetworkRequest(tabId, params); break; case "Network.responseReceived": this.handleNetworkResponse(tabId, params); break; } } handleConsoleMessage(tabId, params) { if (!this.consoleLogs.has(tabId)) { this.consoleLogs.set(tabId, []); } const logs = this.consoleLogs.get(tabId); logs.push({ level: params.level, text: params.text, timestamp: Date.now(), source: params.source, line: params.line, column: params.column, }); // Keep only last 1000 logs per tab if (logs.length > 1000) { logs.splice(0, logs.length - 1000); } } handleNetworkRequest(tabId, params) { if (!this.networkRequests.has(tabId)) { this.networkRequests.set(tabId, []); } const requests = this.networkRequests.get(tabId); requests.push({ requestId: params.requestId, url: params.request.url, method: params.request.method, headers: params.request.headers, timestamp: params.timestamp, type: "request", }); // Keep only last 500 requests per tab if (requests.length > 500) { requests.splice(0, requests.length - 500); } } handleNetworkResponse(tabId, params) { if (!this.networkRequests.has(tabId)) { return; } const requests = this.networkRequests.get(tabId); const request = requests.find((r) => r.requestId === params.requestId); if (request) { request.response = { status: params.response.status, statusText: params.response.statusText, headers: params.response.headers, mimeType: params.response.mimeType, }; } } async getConsoleLogs(tabId) { // Ensure we're attached to the tab await this.attachToTab(tabId); return this.consoleLogs.get(tabId) || []; } async getNetworkRequests(tabId) { // Ensure we're attached to the tab await this.attachToTab(tabId); return this.networkRequests.get(tabId) || []; } clearLogs(tabId) { this.consoleLogs.delete(tabId); this.networkRequests.delete(tabId); } } /***/ }), /***/ "./src/extension/background/mcp-connection.ts": /*!****************************************************!*\ !*** ./src/extension/background/mcp-connection.ts ***! \****************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ MCPConnection: () => (/* binding */ MCPConnection) /* harmony export */ }); // MCP Connection handler for communicating with the MCP server class MCPConnection { constructor() { this.ws = null; this.messageHandlers = []; this.isConnected = false; this.currentPort = 3000; this.maxPort = 3005; } async initialize() { await this.tryConnectToServer(this.currentPort); } async tryConnectToServer(port) { try { console.log(`🔍 Attempting to connect to MCP server on port ${port}...`); // Connect to MCP server this.ws = new WebSocket(`ws://localhost:${port}`); this.ws.onopen = () => { console.log(`🚀 MCP WebSocket connected successfully to localhost:${port}`); this.isConnected = true; this.currentPort = port; // Send a test message to verify connection this.sendMessage({ type: "connection_test", message: "Extension connected successfully!", timestamp: new Date().toISOString(), }); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log("📥 Received message from MCP server:", message); // Handle connection test response if (message.type === "connection_test_response") { console.log("🎉 Two-way communication confirmed! MCP server responded:", message.message); } this.messageHandlers.forEach((handler) => handler(message)); } catch (error) { console.error("Error parsing MCP message:", error); } }; this.ws.onclose = () => { console.log(`MCP WebSocket disconnected from port ${port}`); this.isConnected = false; // Attempt to reconnect after 5 seconds, starting from port 3000 setTimeout(() => this.tryConnectToServer(3000), 5000); }; this.ws.onerror = (error) => { console.error(`MCP WebSocket error on port ${port}:`, error); this.handleConnectionError(port); }; } catch (error) { console.error(`Failed to initialize MCP connection on port ${port}:`, error); this.handleConnectionError(port); } } handleConnectionError(failedPort) { // Try next port if available if (failedPort < this.maxPort) { console.log(`Port ${failedPort} failed, trying ${failedPort + 1}...`); setTimeout(() => this.tryConnectToServer(failedPort + 1), 1000); } else { console.log(`All ports (3000-${this.maxPort}) failed, retrying from 3000 in 5 seconds...`); // Retry from the beginning after 5 seconds setTimeout(() => this.tryConnectToServer(3000), 5000); } } onMessage(handler) { this.messageHandlers.push(handler); } async sendMessage(message) { if (!this.isConnected || !this.ws) { console.warn("MCP not connected, message queued"); return; } try { this.ws.send(JSON.stringify(message)); } catch (error) { console.error("Error sending MCP message:", error); } } async sendResponse(response) { await this.sendMessage(response); } } /***/ }), /***/ "./src/extension/background/tab-manager.ts": /*!*************************************************!*\ !*** ./src/extension/background/tab-manager.ts ***! \*************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ TabManager: () => (/* binding */ TabManager) /* harmony export */ }); // Tab Manager for handling Chrome tab operations class TabManager { constructor() { this.activeTabs = new Map(); } initialize() { // Listen for tab updates chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (tab) { this.activeTabs.set(tabId, tab); } }); // Listen for tab removal chrome.tabs.onRemoved.addListener((tabId) => { this.activeTabs.delete(tabId); }); // Listen for tab activation chrome.tabs.onActivated.addListener(async (activeInfo) => { try { const tab = await chrome.tabs.get(activeInfo.tabId); this.activeTabs.set(activeInfo.tabId, tab); } catch (error) { console.error("Error getting active tab:", error); } }); console.log("TabManager initialized"); } async getActiveTabId() { try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true, }); if (tab?.id) { return tab.id; } throw new Error("No active tab found"); } catch (error) { console.error("Error getting active tab ID:", error); throw error; } } async getTab(tabId) { try { return await chrome.tabs.get(tabId); } catch (error) { console.error("Error getting tab:", error); return null; } } async getAllTabs() { try { return await chrome.tabs.query({}); } catch (error) { console.error("Error getting all tabs:", error); return []; } } getActiveTabsMap() { return this.activeTabs; } } /***/ }) /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. (() => { /*!*******************************************!*\ !*** ./src/extension/background/index.ts ***! \*******************************************/ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _mcp_connection__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./mcp-connection */ "./src/extension/background/mcp-connection.ts"); /* harmony import */ var _tab_manager__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./tab-manager */ "./src/extension/background/tab-manager.ts"); /* harmony import */ var _devtools_manager__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./devtools-manager */ "./src/extension/background/devtools-manager.ts"); // Background service worker for Browser Agent MCP // Handles MCP server communication and coordinates extension functionality class BackgroundService { constructor() { this.mcpConnection = new _mcp_connection__WEBPACK_IMPORTED_MODULE_0__.MCPConnection(); this.tabManager = new _tab_manager__WEBPACK_IMPORTED_MODULE_1__.TabManager(); this.devToolsManager = new _devtools_manager__WEBPACK_IMPORTED_MODULE_2__.DevToolsManager(); this.initialize(); } async initialize() { console.log("Browser Agent MCP: Background service starting..."); // Set up message listeners this.setupMessageListeners(); // Initialize MCP connection await this.mcpConnection.initialize(); // Set up tab event listeners this.tabManager.initialize(); // Set up DevTools integration this.devToolsManager.initialize(); console.log("Browser Agent MCP: Background service initialized"); } setupMessageListeners() { // Listen for messages from content scripts chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { this.handleMessage(message, sender, sendResponse); return true; // Keep message channel open for async response }); // Listen for messages from MCP server this.mcpConnection.onMessage((message) => { this.handleMCPMessage(message); }); } async handleMessage(message, sender, sendResponse) { try { switch (message.type) { case "DOM_QUERY": const domResult = await this.handleDOMQuery(message.payload, sender.tab?.id); sendResponse({ success: true, data: domResult }); break; case "CONSOLE_LOG": await this.handleConsoleLog(message.payload, sender.tab?.id); sendResponse({ success: true }); break; case "NETWORK_REQUEST": await this.handleNetworkRequest(message.payload, sender.tab?.id); sendResponse({ success: true }); break; default: sendResponse({ success: false, error: "Unknown message type" }); } } catch (error) { console.error("Error handling message:", error); sendResponse({ success: false, error: error.message }); } } async handleMCPMessage(message) { // Handle incoming MCP commands and route to appropriate handlers console.log("Received MCP message:", message); try { let result; // Handle different message types from MCP server switch (message.type) { case "DOM_QUERY": result = await this.handleDOMQuery(message.payload); this.sendMCPResponse(message.id, { success: true, data: result }); break; case "GET_CONSOLE_LOGS": result = await this.getConsoleLogs(message.payload); this.sendMCPResponse(message.id, { success: true, data: result }); break; case "GET_NETWORK_REQUESTS": result = await this.getNetworkRequests(message.payload); this.sendMCPResponse(message.id, { success: true, data: result }); break; case "GET_PAGE_INFO": result = await this.getPageInfo(message.payload); this.sendMCPResponse(message.id, { success: true, data: result }); break; case "DOM_CLICK": await this.handleDOMClick(message.payload); this.sendMCPResponse(message.id, { success: true }); break; case "DOM_TYPE": await this.handleDOMType(message.payload); this.sendMCPResponse(message.id, { success: true }); break; // Legacy method-based messages (keep for compatibility) case "dom/query": await this.executeDOMQuery(message.params); break; case "console/getLogs": await this.getConsoleLogs(message.params); break; case "network/getRequests": await this.getNetworkRequests(message.params); break; default: if (message.type !== "connection_test_response") { console.warn("Unknown MCP message type:", message.type); } } } catch (error) { console.error("Error handling MCP message:", error); if (message.id) { this.sendMCPResponse(message.id, { success: false, error: error.message, }); } } } async handleDOMQuery(payload, tabId) { const targetTabId = tabId || payload.tabId || (await this.tabManager.getActiveTabId()); // Execute DOM query in the specified tab const results = await chrome.scripting.executeScript({ target: { tabId: targetTabId }, func: (selector, action = "query") => { const elements = document.querySelectorAll(selector); if (action === "getText") { return Array.from(elements).map((el) => el.textContent?.trim() || ""); } else if (action === "getAttributes") { return Array.from(elements).map((el) => Array.from(el.attributes).reduce((acc, attr) => { acc[attr.name] = attr.value; return acc; }, {})); } else if (action === "getHTML") { return Array.from(elements).map((el) => el.outerHTML); } else { // Default "query" action return Array.from(elements).map((el) => ({ tagName: el.tagName, textContent: el.textContent?.trim() || "", innerHTML: el.innerHTML, attributes: Array.from(el.attributes).reduce((acc, attr) => { acc[attr.name] = attr.value; return acc; }, {}), })); } }, args: [payload.selector, payload.action], }); return results[0]?.result || []; } async handleConsoleLog(payload, tabId) { // Forward console log to MCP server await this.mcpConnection.sendMessage({ method: "console/log", params: { tabId, level: payload.level, message: payload.message, timestamp: payload.timestamp, }, }); } async handleNetworkRequest(payload, tabId) { // Forward network request to MCP server await this.mcpConnection.sendMessage({ method: "network/request", params: { tabId, url: payload.url, method: payload.method, status: payload.status, timestamp: payload.timestamp, }, }); } async executeDOMQuery(params) { const tabId = params.tabId || (await this.tabManager.getActiveTabId()); const result = await this.handleDOMQuery(params, tabId); await this.mcpConnection.sendResponse({ id: params.id, result, }); } async getConsoleLogs(params) { const logs = await this.devToolsManager.getConsoleLogs(params.tabId); await this.mcpConnection.sendResponse({ id: params.id, result: logs, }); } async getNetworkRequests(params) { const requests = await this.devToolsManager.getNetworkRequests(params.tabId); await this.mcpConnection.sendResponse({ id: params.id, result: requests, }); } async sendMCPResponse(messageId, response) { const responseMessage = { id: messageId, ...response, }; await this.mcpConnection.sendMessage(responseMessage); } async getPageInfo(params) { const tabId = params.tabId || (await this.tabManager.getActiveTabId()); // Get page information using chrome.tabs API const tab = await chrome.tabs.get(tabId); return { title: tab.title, url: tab.url, favIconUrl: tab.favIconUrl, status: tab.status, }; } async handleDOMClick(params) { const tabId = params.tabId || (await this.tabManager.getActiveTabId()); await chrome.scripting.executeScript({ target: { tabId }, func: (selector) => { const element = document.querySelector(selector); if (element) { element.click(); } else { throw new Error(`Element not found: ${selector}`); } }, args: [params.selector], }); } async handleDOMType(params) { const tabId = params.tabId || (await this.tabManager.getActiveTabId()); await chrome.scripting.executeScript({ target: { tabId }, func: (selector, text, clear = true) => { const element = document.querySelector(selector); if (element) { if (clear) { element.value = ""; } element.value += text; // Trigger input event element.dispatchEvent(new Event("input", { bubbles: true })); } else { throw new Error(`Element not found: ${selector}`); } }, args: [params.selector, params.text, params.clear], }); } } // Initialize the background service new BackgroundService(); })(); /******/ })() ; //# sourceMappingURL=background.js.map