browser-agent-mcp
Version:
Chrome extension and MCP server for comprehensive browser automation with AI agents
656 lines (638 loc) • 25.1 kB
JavaScript
/******/ (() => { // 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