diff --git a/README.md b/README.md index 4e3c283..74e55d0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,20 @@ This application is a powerful browser monitoring and interaction tool that enables AI-powered applications via Anthropic's Model Context Protocol (MCP) to capture and analyze browser data through a Chrome extension. +## Quick Start + +**Step 1**: Download this repo and load the `chrome-extension` directory in developer mode. + +**Step 2**: Run MCP server (configure your IDE with this): +```sh +npx -y @munawwar-forks/browser-tools-mcp@1.2.3 +``` + +**Step 3**: Run browser-connector server in a terminal +```sh +npx -y @munawwar-forks/browser-tools-server@1.2.3 +``` + Read our [docs](https://browsertools.agentdesk.ai/) for the full installation, quickstart and contribution guides. ## Roadmap @@ -12,6 +26,8 @@ Check out our project roadmap here: [Github Roadmap / Project Board](https://git ## Updates +v1.2.3 - Added ability for agent to inspect any element's HTML, computed styling and dimensions. + v1.2.0 is out! Here's a quick breakdown of the update: - You can now enable "Allow Auto-Paste into Cursor" within the DevTools panel. Screenshots will be automatically pasted into Cursor (just make sure to focus/click into the Agent input field in Cursor, otherwise it won't work!) - Integrated a suite of SEO, performance, accessibility, and best practice analysis tools via Lighthouse @@ -76,6 +92,7 @@ Coding agents like Cursor can run these audits against the current page seamless | **NextJS Audit** | Injects a prompt used to perform a NextJS audit. | | **Audit Mode** | Runs all auditing tools in a sequence. | | **Debugger Mode** | Runs all debugging tools in a sequence. | +| **Element Inspection** | Inspect HTML elements and their CSS properties using CSS selectors. | --- @@ -167,6 +184,17 @@ Runs all debugging tools in a particular sequence > > - "Enter debugger mode." +#### Inspect Elements By Selector (`inspectElementsBySelector`) + +Finds HTML elements that match a CSS selector and returns their HTML and CSS styles. + +> **Example Queries:** +> +> - "Inspect the body element." +> - "Show me all the button elements." +> - "What CSS styles are applied to the header?" +> - "Inspect the element with class 'container' and show its font properties." + ## Architecture There are three core components all used to capture and analyze browser data: @@ -229,6 +257,7 @@ Once installed and configured, the system allows any compatible MCP client to: - Capture network traffic - Take screenshots - Analyze selected elements +- Inspect elements HTML and CSS using CSS selectors - Wipe logs stored in our MCP server - Run accessibility, performance, SEO, and best practices audits diff --git a/browser-tools-mcp/README.md b/browser-tools-mcp/README.md index 059ec32..2aad203 100644 --- a/browser-tools-mcp/README.md +++ b/browser-tools-mcp/README.md @@ -5,6 +5,7 @@ A Model Context Protocol (MCP) server that provides AI-powered browser tools int ## Features - MCP protocol implementation +- Get HTML of all matches to a CSS selector (new) - Browser console log access - Network request analysis - Screenshot capture capabilities @@ -21,13 +22,13 @@ A Model Context Protocol (MCP) server that provides AI-powered browser tools int ## Installation ```bash -npx @agentdeskai/browser-tools-mcp +npx @munawwar-forks/browser-tools-mcp ``` Or install globally: ```bash -npm install -g @agentdeskai/browser-tools-mcp +npm install -g @munawwar-forks/browser-tools-mcp ``` ## Usage @@ -35,17 +36,18 @@ npm install -g @agentdeskai/browser-tools-mcp 1. First, make sure the Browser Tools Server is running: ```bash -npx @agentdeskai/browser-tools-server +npx @munawwar-forks/browser-tools-server ``` 2. Then start the MCP server: ```bash -npx @agentdeskai/browser-tools-mcp +npx @munawwar-forks/browser-tools-mcp ``` 3. The MCP server will connect to the Browser Tools Server and provide the following capabilities: +- Get HTML by selector - Console log retrieval - Network request monitoring - Screenshot capture @@ -57,6 +59,7 @@ npx @agentdeskai/browser-tools-mcp The server provides the following MCP functions: +- `mcp_getHtmlBySelector` - Retrieve HTML by CSS selector - `mcp_getConsoleLogs` - Retrieve browser console logs - `mcp_getConsoleErrors` - Get browser console errors - `mcp_getNetworkErrors` - Get network error logs diff --git a/browser-tools-mcp/mcp-server.ts b/browser-tools-mcp/mcp-server.ts index a7a1272..0399cf1 100644 --- a/browser-tools-mcp/mcp-server.ts +++ b/browser-tools-mcp/mcp-server.ts @@ -4,6 +4,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import path from "path"; import fs from "fs"; +// Using zod from the @modelcontextprotocol/sdk dependencies +// This avoids adding zod as a direct dependency to package.json +import { z } from "zod"; // Create the MCP server const server = new McpServer({ @@ -319,6 +322,62 @@ server.tool( } ); +server.tool( + "inspectElementsBySelector", + "Get HTML elements and their CSS styles matching a CSS selector", + { + selector: z.string().describe("CSS selector to find elements (e.g., '.classname', '#id', 'div.container > p'). Use $0 to inspect the current element."), + resultLimit: z.number().optional().default(1).describe("Maximum number of elements to process (default: 1)"), + includeComputedStyles: z.array(z.string()).optional().default([]).describe("Array of specific CSS properties to include in the computed styles output (empty array means no computed styles)") + }, + async ({ selector, resultLimit = 1, includeComputedStyles = [] }) => { + return await withServerConnection(async () => { + try { + // Call the browser-connector endpoint + const response = await fetch( + `http://${discoveredHost}:${discoveredPort}/inspect-elements-by-selector`, + { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ selector, resultLimit, includeComputedStyles }) + } + ); + + const result = await response.json().catch(() => null); + if (result?.error) { + throw new Error(result.error); + } + if (!response.ok || result === null) { + throw new Error(`Server returned error: ${response.status}`); + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(result.data || {}, null, 2) + } + ] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("Error inspecting elements by selector:", errorMessage); + return { + content: [ + { + type: "text", + text: `Failed to inspect elements by selector: ${errorMessage}` + } + ], + isError: true + }; + } + }); + } +); + server.tool("wipeLogs", "Wipe all browser logs from memory", async () => { return await withServerConnection(async () => { const response = await fetch( diff --git a/browser-tools-mcp/package-lock.json b/browser-tools-mcp/package-lock.json index 0784e8f..cd30c2f 100644 --- a/browser-tools-mcp/package-lock.json +++ b/browser-tools-mcp/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@agentdeskai/browser-tools-mcp", - "version": "1.1.0", + "name": "@munawwar-forks/browser-tools-mcp", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@agentdeskai/browser-tools-mcp", - "version": "1.1.0", + "name": "@munawwar-forks/browser-tools-mcp", + "version": "1.2.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.4.1", diff --git a/browser-tools-mcp/package.json b/browser-tools-mcp/package.json index 3d6e447..491b0f0 100644 --- a/browser-tools-mcp/package.json +++ b/browser-tools-mcp/package.json @@ -1,14 +1,21 @@ { - "name": "@agentdeskai/browser-tools-mcp", - "version": "1.2.0", + "name": "@munawwar-forks/browser-tools-mcp", + "version": "1.2.3", "description": "MCP (Model Context Protocol) server for browser tools integration", "main": "dist/mcp-server.js", + "repository": { + "type": "git", + "url": "https://github.com/Munawwar/browser-tools-mcp" + }, "bin": { "browser-tools-mcp": "dist/mcp-server.js" }, + "publishConfig": { + "access": "public" + }, "scripts": { "inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js", - "inspect-live": "npx @modelcontextprotocol/inspector npx -- @agentdeskai/browser-tools-mcp", + "inspect-live": "npx @modelcontextprotocol/inspector npx -- @munawwar-forks/browser-tools-mcp", "build": "tsc", "start": "tsc && node dist/mcp-server.js", "prepublishOnly": "npm run build", diff --git a/browser-tools-server/README.md b/browser-tools-server/README.md index 47b59e5..58e69e2 100644 --- a/browser-tools-server/README.md +++ b/browser-tools-server/README.md @@ -15,13 +15,13 @@ A powerful browser tools server for capturing and managing browser events, logs, ## Installation ```bash -npx @agentdeskai/browser-tools-server +npx @munawwar-forks/browser-tools-server ``` Or install globally: ```bash -npm install -g @agentdeskai/browser-tools-server +npm install -g @munawwar-forks/browser-tools-server ``` ## Usage @@ -29,7 +29,7 @@ npm install -g @agentdeskai/browser-tools-server 1. Start the server: ```bash -npx @agentdeskai/browser-tools-server +npx @munawwar-forks/browser-tools-server ``` 2. The server will start on port 3025 by default @@ -48,6 +48,7 @@ npx @agentdeskai/browser-tools-server - `/accessibility-audit` - Run accessibility audit on current page - `/performance-audit` - Run performance audit on current page - `/seo-audit` - Run SEO audit on current page +- `/inspect-elements-by-selector` - Get HTML and CSS information for elements matching a given CSS selector ## API Documentation @@ -69,6 +70,7 @@ npx @agentdeskai/browser-tools-server - `POST /accessibility-audit` - Run a WCAG-compliant accessibility audit on the current page - `POST /performance-audit` - Run a performance audit on the current page - `POST /seo-audit` - Run a SEO audit on the current page +- `POST /inspect-elements-by-selector` - Returns HTML and CSS information for elements matching a given CSS selector # Audit Functionality diff --git a/browser-tools-server/browser-connector.ts b/browser-tools-server/browser-connector.ts index a4cc03c..0c8168d 100644 --- a/browser-tools-server/browser-connector.ts +++ b/browser-tools-server/browser-connector.ts @@ -181,6 +181,14 @@ interface ScreenshotCallback { const screenshotCallbacks = new Map(); +// Add new state for tracking selector requests +interface SelectorCallback { + resolve: (value: string[]) => void; + reject: (reason: Error) => void; +} + +const selectorCallbacks = new Map(); + // Function to get available port starting with the given port async function getAvailablePort( startPort: number, @@ -632,6 +640,18 @@ export class BrowserConnector { } ); + // Register the inspect-elements-by-selector endpoint + this.app.post( + "/inspect-elements-by-selector", + async (req: express.Request, res: express.Response) => { + console.log( + "Browser Connector: Received request to /inspect-elements-by-selector endpoint" + ); + console.log("Browser Connector: Request body:", req.body); + await this.inspectElementsBySelector(req, res); + } + ); + // Set up accessibility audit endpoint this.setupAccessibilityAudit(); @@ -741,7 +761,28 @@ export class BrowserConnector { ); screenshotCallbacks.clear(); // Clear all callbacks } - } else { + } + // Handle selector response + if (data.type === "html-by-selector" && data.requestId) { + console.log("Received HTML by selector response"); + const callback = selectorCallbacks.get(data.requestId); + if (callback) { + callback.resolve(data.html || []); + selectorCallbacks.delete(data.requestId); + } else { + console.log("No callback found for selector request:", data.requestId); + } + } + // Handle selector error + else if (data.type === "selector-error" && data.requestId) { + console.log("Received selector error:", data.error); + const callback = selectorCallbacks.get(data.requestId); + if (callback) { + callback.reject(new Error(data.error || "Failed to get HTML by selector")); + selectorCallbacks.delete(data.requestId); + } + } + else { console.log("Unhandled message type:", data.type); } } catch (error) { @@ -1401,6 +1442,113 @@ export class BrowserConnector { } }); } + + // Add method to handle elements with styles requests + private async inspectElementsBySelector(req: express.Request, res: express.Response) { + if (!this.activeConnection) { + return res.status(503).json({ error: "Chrome extension not connected" }); + } + + const { selector, resultLimit = 1, includeComputedStyles = [] } = req.body; + if (!selector) { + return res.status(400).json({ error: "No selector provided" }); + } + + try { + const requestId = Date.now().toString(); + console.log("Browser Connector: Generated requestId for elements with styles request:", requestId); + + // Create promise that will resolve when we get the elements and styles data + const elementsBySelectorPromise = new Promise((resolve, reject) => { + console.log( + `Browser Connector: Setting up elements with styles callback for requestId: ${requestId}` + ); + + // Store callback in a map + const elementsBySelectorCallbacks = new Map void; + reject: (reason: Error) => void; + }>(); + + // Store callback in map + elementsBySelectorCallbacks.set(requestId, { resolve, reject }); + + // Add a message listener for inspect-elements-by-selector response + const messageHandler = (event: WebSocket.MessageEvent) => { + try { + const response = JSON.parse(event.data as string); + + if (response.type === "inspect-elements-response" && response.requestId === requestId) { + console.log("Browser Connector: Received inspect-elements-by-selector response"); + const callback = elementsBySelectorCallbacks.get(requestId); + if (callback) { + callback.resolve(response.data); + elementsBySelectorCallbacks.delete(requestId); + this.activeConnection?.removeEventListener("message", messageHandler); + } + } + else if (response.type === "inspect-elements-error" && response.requestId === requestId) { + console.error("Browser Connector: inspect-elements-by-selector error:", response.error); + const callback = elementsBySelectorCallbacks.get(requestId); + if (callback) { + callback.reject(new Error(response.error || "Failed to get inspect-elements-by-selector")); + elementsBySelectorCallbacks.delete(requestId); + this.activeConnection?.removeEventListener("message", messageHandler); + } + } + } catch (error) { + console.error("Error processing inspect-elements-by-selector response:", error); + } + }; + + // Add the message listener + this.activeConnection?.addEventListener("message", messageHandler); + + // Set timeout to clean up if we don't get a response + setTimeout(() => { + if (elementsBySelectorCallbacks.has(requestId)) { + console.log( + `Browser Connector: inspect-elements-by-selector request timed out for requestId: ${requestId}` + ); + elementsBySelectorCallbacks.delete(requestId); + this.activeConnection?.removeEventListener("message", messageHandler); + reject(new Error("inspect-elements-by-selector request timed out - no response from Chrome extension")); + } + }, 10000); // 10 second timeout + }); + + // Send request to extension + const message = JSON.stringify({ + type: "inspect-elements-by-selector", + selector, + resultLimit, + includeComputedStyles, + requestId, + }); + console.log( + `Browser Connector: Sending WebSocket message to extension:`, + message + ); + this.activeConnection.send(message); + + // Wait for inspect-elements-by-selector data + console.log("Browser Connector: Waiting for inspect-elements-by-selector response..."); + const elementsBySelector = await elementsBySelectorPromise; + console.log(`Browser Connector: Received inspect-elements-by-selector data with ${elementsBySelector.elements.length} elements`); + + res.json({ data: elementsBySelector }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("Browser Connector: Error inspecting elements by selector:", errorMessage); + if (errorMessage.includes("Invalid selector")) { + return res.status(400).json({ error: errorMessage }); + } else if (errorMessage.includes("timed out")) { + return res.status(504).json({ error: errorMessage }); + } else { + return res.status(500).json({ error: errorMessage }); + } + } + } } // Use an async IIFE to allow for async/await in the initial setup diff --git a/browser-tools-server/package-lock.json b/browser-tools-server/package-lock.json index c38a600..bb61c2a 100644 --- a/browser-tools-server/package-lock.json +++ b/browser-tools-server/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@agentdeskai/browser-tools-server", - "version": "1.1.1", + "name": "@munawwar-forks/browser-tools-server", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@agentdeskai/browser-tools-server", - "version": "1.1.1", + "name": "@munawwar-forks/browser-tools-server", + "version": "1.2.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.4.1", @@ -277,6 +277,7 @@ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" diff --git a/browser-tools-server/package.json b/browser-tools-server/package.json index f7c5442..31f09f1 100644 --- a/browser-tools-server/package.json +++ b/browser-tools-server/package.json @@ -1,12 +1,19 @@ { - "name": "@agentdeskai/browser-tools-server", - "version": "1.2.0", + "name": "@munawwar-forks/browser-tools-server", + "version": "1.2.3", "description": "A browser tools server for capturing and managing browser events, logs, and screenshots", "type": "module", "main": "dist/browser-connector.js", + "repository": { + "type": "git", + "url": "https://github.com/Munawwar/browser-tools-mcp" + }, "bin": { "browser-tools-server": "./dist/browser-connector.js" }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsc", "start": "tsc && node dist/browser-connector.js", @@ -38,13 +45,13 @@ "chrome-launcher": "^1.1.2" }, "devDependencies": { - "@types/ws": "^8.5.14", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.13.1", "@types/node-fetch": "^2.6.11", "@types/puppeteer-core": "^7.0.4", + "@types/ws": "^8.5.14", "typescript": "^5.7.3" } } diff --git a/chrome-extension/devtools.js b/chrome-extension/devtools.js index 6197f2f..af6573e 100644 --- a/chrome-extension/devtools.js +++ b/chrome-extension/devtools.js @@ -453,8 +453,13 @@ function wipeLogs() { }); } +// Listen for styleSheetAdded events +let styleSheets = []; + // Listen for page refreshes chrome.devtools.network.onNavigated.addListener((url) => { + // Reset the styleSheets array on navigation/refresh + styleSheets = []; console.log("Page navigated/refreshed - wiping logs"); wipeLogs(); @@ -520,9 +525,47 @@ async function attachDebugger() { }); } +// Helper function to send debugger commands with Promise +async function sendCommand(method, params = {}) { + return new Promise((resolve, reject) => { + chrome.debugger.sendCommand( + { tabId: currentTabId }, + method, + params, + (result) => { + if (chrome.runtime.lastError) { + const err = new Error(chrome.runtime.lastError.message); + err.originalError = chrome.runtime.lastError; + reject(err); + } else { + resolve(result); + } + } + ); + }); +}; + +// Create a stylesheet event listener +const styleSheetEventListener = (source, method, params) => { + // Only process events for our tab + if (source.tabId !== currentTabId) { + return; + } + + if (method === "CSS.styleSheetAdded") { + console.log("Chrome Extension: Style sheet added"); + styleSheets.push(params.header); + } else if (method === "CSS.styleSheetRemoved") { + console.log("Chrome Extension: Style sheet removed"); + styleSheets = styleSheets.filter( + (sheet) => sheet.styleSheetId !== params.styleSheetId + ); + } +}; + function performAttach() { console.log("Performing debugger attachment to tab:", currentTabId); - chrome.debugger.attach({ tabId: currentTabId }, "1.3", () => { + chrome.debugger.attach({ tabId: currentTabId }, "1.3", async () => { if (chrome.runtime.lastError) { console.error("Failed to attach debugger:", chrome.runtime.lastError); isDebuggerAttached = false; @@ -534,19 +577,23 @@ function performAttach() { // Add the event listener when attaching chrome.debugger.onEvent.addListener(consoleMessageListener); + chrome.debugger.onEvent.addListener(styleSheetEventListener); - chrome.debugger.sendCommand( - { tabId: currentTabId }, - "Runtime.enable", - {}, - () => { - if (chrome.runtime.lastError) { - console.error("Failed to enable runtime:", chrome.runtime.lastError); - return; - } - console.log("Runtime API successfully enabled"); - } - ); + // Enable domains (these may fail if already enabled, that's OK) + try { + await sendCommand("DOM.enable"); + } catch (e) {} + // Enable CSS domain to start receiving stylesheet events + try { + await sendCommand("CSS.enable"); + } catch (e) {} + + try { + await sendCommand("Runtime.enable", {}); + console.log("Runtime API successfully enabled"); + } catch (e) { + console.error("Failed to enable runtime:", e?.originalError); + } }); } @@ -686,6 +733,30 @@ window.addEventListener("unload", () => { } }); +/** + * + * @param {Function|string} func + * @param {(string | number)[]} args + */ +async function windowEval(func, args) { + const stringifiedArgs = args.map((arg) => { + if (typeof arg === "string") { + return JSON.stringify(arg); + } + if (Array.isArray(arg)) { + return `JSON.parse(${JSON.stringify(JSON.stringify(arg))})`; + } + return arg; + }).join(", "); + const funcString = typeof func === "string" ? func : func.toString(); + const evalString = `(${funcString})(${stringifiedArgs})`; + return new Promise((resolve) => { + chrome.devtools.inspectedWindow.eval(evalString, (resultInner, exceptionInner) => { + resolve([resultInner, exceptionInner]); + }); + }); +} + // Function to capture and send element data function captureAndSendElement() { chrome.devtools.inspectedWindow.eval( @@ -1002,6 +1073,359 @@ async function setupWebSocket() { ws.send(JSON.stringify(response)); }); + } else if (message.type === "inspect-elements-by-selector") { + console.log( + "Chrome Extension: Received request for inspecting elements by selector:", + message.selector + ); + const resultLimit = message.resultLimit || 1; + const includeComputedStyles = message.includeComputedStyles || []; + + // Now start the inspection process + try { + // First check if elements exist and get basic info about them using eval + const [elementsInfo, elementsInfoException] = await windowEval(function (selector, resultLimit) { + // DO NOT have any closures in this entire function, because the function is stringified. + let elements = []; + if (selector === "$0") { + elements = [$0]; + } else { + elements = document.querySelectorAll(selector); + } + if (elements.length === 0) { + return { error: "No elements found matching selector" }; + } + + // Return basic info about the elements + return { + count: elements.length, + elements: Array.from(elements).slice(0, resultLimit).map((el, i) => { + const rect = el.getBoundingClientRect(); + const randomId = Math.random().toString(36).substring(2); + el.setAttribute(`data-${randomId}`, "1"); + return { + // index: i, + html: el.outerHTML, + dimensions: { + offsetWidth: el.offsetWidth, + offsetHeight: el.offsetHeight, + clientWidth: el.clientWidth, + clientHeight: el.clientHeight + }, + // Extract individual properties from rect as it is not directly JSON serializable + boundingClientRect: { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + x: rect.x, + y: rect.y + }, + uniqueId: randomId, + }; + }) + }; + }.toString(), [message.selector, resultLimit]); + + if (elementsInfoException || !elementsInfo) { + console.error("Chrome Extension: Error finding elements:", elementsInfoException || "No result"); + ws.send( + JSON.stringify({ + type: "inspect-elements-error", + error: + elementsInfoException?.value || + "Failed to find elements by selector", + requestId: message.requestId, + }) + ); + return; + } + + if (elementsInfo.error) { + console.error("Chrome Extension: Element selection error:", elementsInfo.error); + ws.send( + JSON.stringify({ + type: "inspect-elements-error", + error: elementsInfo.error, + requestId: message.requestId, + }) + ); + return; + } + + const results = []; + const ruleStore = {}; + const seenRules = new Set(); + + // Process each element's style rules using CDP + for (const element of elementsInfo.elements) { + try { + + // Give a small delay for the attribute to be set + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Get matched styles using CDP + try { + // Step 1: Get the document root + const root = await sendCommand("DOM.getDocument", { + depth: 1, + }); + + // Step 2: Get the node using the unique attribute + const node = await sendCommand("DOM.querySelector", { + nodeId: root.root.nodeId, + selector: `[data-${element.uniqueId}="1"]`, + }); + + if (!node || !node.nodeId) { + throw new Error("Element not found with temp attribute"); + } + + // Step 4: Get the matched styles + const matchedStyles = await sendCommand( + "CSS.getMatchedStylesForNode", + { nodeId: node.nodeId } + ); + + // Process the matched styles + const matchedRules = []; + + if (matchedStyles && matchedStyles.matchedCSSRules) { + // Process each matched rule + for (const match of matchedStyles.matchedCSSRules) { + const rule = match.rule; + + // Get the actual matched selector from the rule's selectorList + const matchedSelectors = (match?.matchingSelectors || []) + .map((index) => (rule?.selectorList?.selectors[index])) + .filter((v) => v !== undefined) + .map((selector) => ({ + text: selector.text, + specificity: selector.specificity ? [ + selector.specificity.a, + selector.specificity.b, + selector.specificity.c, + ] : undefined, + })); + + // Process arrays to only include specified properties + const processedMedia = + rule.media?.map((item) => ({ + text: item.text, + source: item.source, + sourceURL: item.sourceURL, + })) || []; + + const processedLayers = + rule.layers?.map((item) => ({ + text: item.text, + })) || []; + + const processedSupports = + rule.supports + ?.filter((item) => item.active) + .map((item) => ({ text: item.text })) || []; + + const processedScopes = + rule.scopes?.map((item) => ({ + text: item.text, + })) || []; + + const processedContainerQueries = + rule.containerQueries?.map((item) => ({ + text: item.text, + name: item.name, + physicalAxes: item.physicalAxes, + logicalAxes: item.logicalAxes, + queriesScrollState: item.queriesScrollState, + })) || []; + + const styleSheetIndex = styleSheets.findIndex( + (sheet) => sheet.styleSheetId === rule.styleSheetId + ); + let styleSheet = styleSheets[styleSheetIndex]; + let styleSheetSource; + if (styleSheet?.ownerNode) { + const resolvedNode = await sendCommand( + "DOM.resolveNode", + { backendNodeId: styleSheet.ownerNode } + ); + const evaluateResponse = await sendCommand( + "Runtime.callFunctionOn", + { + objectId: resolvedNode.object.objectId, + functionDeclaration: `function () { + const startTagRegex = ${ + /(<([a-zA-Z][^\s\/>]*)(?:\s+[^\s\/>"'=]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*\s*(\/?)\s*>)/.toString() + }; + const startTag = this.outerHTML.match(startTagRegex)?.[0]; + return startTag; + }` + } + ); + styleSheetSource = evaluateResponse.result.value; + } + + matchedRules.push({ + origin: rule.origin, // 'user-agent', 'regular', 'inspector' or 'injected' + fullSelector: rule.selectorList?.text, + body: rule.style.cssText || (rule.style.cssProperties || []) + .map((property) => `${property.name}: ${property.value}`) + .join("; "), + matchedSelectors: matchedSelectors, + source: styleSheetSource, + // additional info + media: + processedMedia.length > 0 + ? processedMedia + : undefined, + layers: + processedLayers.length > 0 + ? processedLayers + : undefined, + supports: + processedSupports.length > 0 + ? processedSupports + : undefined, + scopes: + processedScopes.length > 0 + ? processedScopes + : undefined, + containerQueries: + processedContainerQueries.length > 0 + ? processedContainerQueries + : undefined, + // rulesTypes is an array of enumerations / strings + ruleTypes: + rule.ruleTypes && rule.ruleTypes.length > 0 + ? rule.ruleTypes + : undefined, + }); + }; + } + + // Sort rules by most specific to least specific + matchedRules.reverse(); + + // Get computed styles if requested + let computedStyles; + if ( + Array.isArray(includeComputedStyles) && + includeComputedStyles.length > 0 + ) { + const [computedStylesResult, computedStylesException] = await windowEval(function (uniqueId, includeComputedStyles) { + const el = document.querySelector(`[data-${uniqueId}="1"]`); + if (!el) return {}; + + const styles = window.getComputedStyle(el); + return includeComputedStyles.reduce((result, prop) => { + result[prop] = styles.getPropertyValue(prop); + return result; + }, {}); + }, [element.uniqueId, includeComputedStyles]); + if (computedStylesException) { + console.error("Error getting computed styles:", computedStylesException); + } else if (Object.keys(computedStylesResult).length > 0) { + computedStyles = computedStylesResult; + } + } + + // Create hash for rules + const fnv1a = (str) => { + let h = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h += + (h << 1) + + (h << 4) + + (h << 7) + + (h << 8) + + (h << 24); + } + return (h >>> 0).toString(36); + }; + + // Hash the rules + const rulesJson = JSON.stringify(matchedRules); + const rulesHash = fnv1a(rulesJson); + + // Check if we've seen these rules before + let seenRulesBefore = false; + if (!seenRules.has(rulesHash)) { + ruleStore[rulesHash] = matchedRules; + seenRules.add(rulesHash); + } else { + seenRulesBefore = true; + } + + // Store the element with style info + results.push({ + ...element, + uniqueId: undefined, // remove temporary attribute + styles: { + // Only include full rules if it's the first occurrence + ...(!seenRulesBefore && { matchedRules }), + matchedRulesHash: rulesHash, + computedStyles, + }, + }); + } catch (error) { + console.error("Error processing element styles:", error); + // Still add the element but with error info + results.push({ + ...element, + styles: { + error: error.message, + }, + }); + } finally { + // Remove the temporary attribute + const [, removeAttributeException] = await windowEval(function (uniqueId) { + document.querySelector(`[data-${uniqueId}="1"]`).removeAttribute(`data-${uniqueId}`) + }, element.uniqueId); + if (removeAttributeException) { + console.warn("Error removing temporary attribute:", removeAttributeException); + } + } + } catch (error) { + console.error("Error in element processing:", error); + } + } + + // Final processing - sort by original index + results.sort((a, b) => a.index - b.index); + + // Prepare and send the final result + const finalResult = { + elements: results, + totalCount: elementsInfo.count, + processedCount: results.length, + // Send this only if we need in future. Keep response payload small. + // ruleStore, + }; + + console.log(`Chrome Extension: Found ${finalResult.totalCount} elements, processed ${finalResult.processedCount}`); + + // Send back the elements with styles data + ws.send( + JSON.stringify({ + type: "inspect-elements-response", + data: finalResult, + requestId: message.requestId, + }) + ); + } catch (error) { + console.error("Chrome Extension: Error in element inspection process:", error); + ws.send( + JSON.stringify({ + type: "inspect-elements-error", + error: error.message || "Unknown error in inspection process", + requestId: message.requestId, + }) + ); + } } else if (message.type === "get-current-url") { console.log("Chrome Extension: Received request for current URL"); diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 4b94126..1ce5aad 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,8 +1,9 @@ { - "name": "BrowserTools MCP", - "version": "1.2.0", + "name": "BrowserTools MCP Fork by Munawwar", + "version": "1.2.3", "description": "MCP tool for AI code editors to capture data from a browser such as console logs, network requests, screenshots and more", "manifest_version": 3, + "homepage_url": "https://github.com/Munawwar/browser-tools-mcp", "devtools_page": "devtools.html", "permissions": [ "activeTab",