This commit is contained in:
Ted Werbel 2025-02-14 04:18:43 -05:00
parent 6cf3f14403
commit c615cc635f
19 changed files with 2453 additions and 734 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,65 @@
# Browser Tools MCP Server
A Model Context Protocol (MCP) server that provides AI-powered browser tools integration. This server works in conjunction with the Browser Tools Server to provide AI capabilities for browser debugging and analysis.
## Features
- MCP protocol implementation
- Browser console log access
- Network request analysis
- Screenshot capture capabilities
- Element selection and inspection
- Real-time browser state monitoring
## Installation
```bash
npx @agentdeskai/browser-tools-mcp
```
Or install globally:
```bash
npm install -g @agentdeskai/browser-tools-mcp
```
## Usage
1. First, make sure the Browser Tools Server is running:
```bash
npx @agentdeskai/browser-tools-server
```
2. Then start the MCP server:
```bash
npx @agentdeskai/browser-tools-mcp
```
3. The MCP server will connect to the Browser Tools Server and provide the following capabilities:
- Console log retrieval
- Network request monitoring
- Screenshot capture
- Element selection
- Browser state analysis
## MCP Functions
The server provides the following MCP functions:
- `mcp_getConsoleLogs` - Retrieve browser console logs
- `mcp_getConsoleErrors` - Get browser console errors
- `mcp_getNetworkErrors` - Get network error logs
- `mcp_getNetworkSuccess` - Get successful network requests
- `mcp_getNetworkLogs` - Get all network logs
- `mcp_getSelectedElement` - Get the currently selected DOM element
## Integration
This server is designed to work with AI tools and platforms that support the Model Context Protocol (MCP). It provides a standardized interface for AI models to interact with browser state and debugging information.
## License
MIT

View File

@ -1,13 +1,15 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import path from "path";
import { z } from "zod";
// import { z } from "zod";
// import fs from "fs";
// Create the MCP server
const server = new McpServer({
name: "AI Browser Connector",
version: "1.0.0",
name: "Browsert Tools MCP",
version: "1.0.9",
});
// Function to get the port from the .port file
@ -71,19 +73,19 @@ server.tool("getNetworkErrors", "Check our network ERROR logs", async () => {
};
});
// Return all XHR/fetch requests
server.tool("getNetworkSuccess", "Check our network SUCCESS logs", async () => {
const response = await fetch(`http://127.0.0.1:${PORT}/all-xhr`);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
});
// // Return all XHR/fetch requests
// server.tool("getNetworkSuccess", "Check our network SUCCESS logs", async () => {
// const response = await fetch(`http://127.0.0.1:${PORT}/all-xhr`);
// const json = await response.json();
// return {
// content: [
// {
// type: "text",
// text: JSON.stringify(json, null, 2),
// },
// ],
// };
// });
// Return all XHR/fetch requests
server.tool("getNetworkLogs", "Check ALL our network logs", async () => {
@ -105,12 +107,12 @@ server.tool(
"Take a screenshot of the current browser tab",
async () => {
try {
const response = await fetch(`http://127.0.0.1:${PORT}/screenshot`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`http://127.0.0.1:${PORT}/capture-screenshot`,
{
method: "POST",
}
);
const result = await response.json();
@ -168,6 +170,22 @@ server.tool(
}
);
// Add new tool for wiping logs
server.tool("wipeLogs", "Wipe all browser logs from memory", async () => {
const response = await fetch(`http://127.0.0.1:${PORT}/wipelogs`, {
method: "POST",
});
const json = await response.json();
return {
content: [
{
type: "text",
text: json.message,
},
],
};
});
// Start receiving messages on stdio
(async () => {
const transport = new StdioServerTransport();

View File

@ -1,34 +1,37 @@
{
"name": "mcp-server",
"version": "1.0.0",
"name": "@agentdeskai/browser-tools-mcp",
"version": "1.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mcp-server",
"version": "1.0.0",
"license": "ISC",
"name": "@agentdeskai/browser-tools-mcp",
"version": "1.0.9",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"@types/ws": "^8.5.14",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"llm-cost": "^1.0.5",
"ws": "^8.18.0"
},
"bin": {
"browser-tools-mcp": "dist/mcp-server.js"
},
"devDependencies": {
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.13.1",
"@types/ws": "^8.5.14",
"typescript": "^5.7.3"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.4.1.tgz",
"integrity": "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.5.0.tgz",
"integrity": "sha512-IJ+5iVVs8FCumIHxWqpwgkwOzyhtHVKy45s6Ug7Dv0MfRpaYisH8QQ87rIWeWdOzlk8sfhitZ7HCyQZk7d6b8w==",
"dependencies": {
"content-type": "^1.0.5",
"eventsource": "^3.0.2",
@ -40,31 +43,6 @@
"node": ">=18"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -130,9 +108,10 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.13.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
"version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"dev": true,
"dependencies": {
"undici-types": "~6.20.0"
}
@ -174,6 +153,7 @@
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -218,6 +198,20 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -227,9 +221,9 @@
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -761,19 +755,30 @@
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -970,7 +975,8 @@
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true
},
"node_modules/unpipe": {
"version": "1.0.0",
@ -1017,9 +1023,9 @@
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@ -0,0 +1,44 @@
{
"name": "@agentdeskai/browser-tools-mcp",
"version": "1.0.9",
"description": "MCP (Model Context Protocol) server for browser tools integration",
"main": "dist/mcp-server.js",
"bin": {
"browser-tools-mcp": "dist/mcp-server.js"
},
"scripts": {
"inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js",
"inspect-live": "npx @modelcontextprotocol/inspector npx -- @agentdeskai/browser-tools-mcp",
"build": "tsc",
"start": "tsc && node dist/mcp-server.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"mcp",
"model-context-protocol",
"browser",
"tools",
"debugging",
"ai",
"chrome",
"extension"
],
"author": "AgentDesk AI",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"llm-cost": "^1.0.5",
"ws": "^8.18.0"
},
"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",
"typescript": "^5.7.3"
}
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,68 @@
# Browser Tools Server
A powerful browser tools server for capturing and managing browser events, logs, and screenshots. This server works in conjunction with the Browser Tools Chrome Extension to provide comprehensive browser debugging capabilities.
## Features
- Console log capture
- Network request monitoring
- Screenshot capture
- Element selection tracking
- WebSocket real-time communication
- Configurable log limits and settings
## Installation
```bash
npx @agentdeskai/browser-tools-server
```
Or install globally:
```bash
npm install -g @agentdeskai/browser-tools-server
```
## Usage
1. Start the server:
```bash
npx @agentdeskai/browser-tools-server
```
2. The server will start on port 3025 by default
3. Install and enable the Browser Tools Chrome Extension
4. The server exposes the following endpoints:
- `/console-logs` - Get console logs
- `/console-errors` - Get console errors
- `/network-errors` - Get network error logs
- `/network-success` - Get successful network requests
- `/all-xhr` - Get all network requests
- `/screenshot` - Capture screenshots
- `/selected-element` - Get currently selected DOM element
## API Documentation
### GET Endpoints
- `GET /console-logs` - Returns recent console logs
- `GET /console-errors` - Returns recent console errors
- `GET /network-errors` - Returns recent network errors
- `GET /network-success` - Returns recent successful network requests
- `GET /all-xhr` - Returns all recent network requests
- `GET /selected-element` - Returns the currently selected DOM element
### POST Endpoints
- `POST /extension-log` - Receive logs from the extension
- `POST /screenshot` - Capture and save screenshots
- `POST /selected-element` - Update the selected element
- `POST /wipelogs` - Clear all stored logs
## License
MIT

View File

@ -0,0 +1,686 @@
#!/usr/bin/env node
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { tokenizeAndEstimateCost } from "llm-cost";
import WebSocket from "ws";
import fs from "fs";
import path from "path";
import { IncomingMessage } from "http";
import { Socket } from "net";
import os from "os";
// Function to get default downloads folder
function getDefaultDownloadsFolder(): string {
const homeDir = os.homedir();
// Downloads folder is typically the same path on Windows, macOS, and Linux
const downloadsPath = path.join(homeDir, "Downloads", "mcp-screenshots");
return downloadsPath;
}
// We store logs in memory
const consoleLogs: any[] = [];
const consoleErrors: any[] = [];
const networkErrors: any[] = [];
const networkSuccess: any[] = [];
const allXhr: any[] = [];
// Add settings state
let currentSettings = {
logLimit: 50,
queryLimit: 30000,
showRequestHeaders: false,
showResponseHeaders: false,
model: "claude-3-sonnet",
stringSizeLimit: 500,
maxLogSize: 20000,
screenshotPath: getDefaultDownloadsFolder(),
};
// Add new storage for selected element
let selectedElement: any = null;
// Add new state for tracking screenshot requests
interface ScreenshotCallback {
resolve: (value: { data: string; path?: string }) => void;
reject: (reason: Error) => void;
}
const screenshotCallbacks = new Map<string, ScreenshotCallback>();
const app = express();
const PORT = 3025;
app.use(cors());
// Increase JSON body parser limit to 50MB to handle large screenshots
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
// Helper to recursively truncate strings in any data structure
function truncateStringsInData(data: any, maxLength: number): any {
if (typeof data === "string") {
return data.length > maxLength
? data.substring(0, maxLength) + "... (truncated)"
: data;
}
if (Array.isArray(data)) {
return data.map((item) => truncateStringsInData(item, maxLength));
}
if (typeof data === "object" && data !== null) {
const result: any = {};
for (const [key, value] of Object.entries(data)) {
result[key] = truncateStringsInData(value, maxLength);
}
return result;
}
return data;
}
// Helper to safely parse and process JSON strings
function processJsonString(jsonString: string, maxLength: number): string {
try {
// Try to parse the string as JSON
const parsed = JSON.parse(jsonString);
// Process any strings within the parsed JSON
const processed = truncateStringsInData(parsed, maxLength);
// Stringify the processed data
return JSON.stringify(processed);
} catch (e) {
// If it's not valid JSON, treat it as a regular string
return truncateStringsInData(jsonString, maxLength);
}
}
// Helper to process logs based on settings
function processLogsWithSettings(logs: any[]) {
return logs.map((log) => {
const processedLog = { ...log };
if (log.type === "network-request") {
// Handle headers visibility
if (!currentSettings.showRequestHeaders) {
delete processedLog.requestHeaders;
}
if (!currentSettings.showResponseHeaders) {
delete processedLog.responseHeaders;
}
}
return processedLog;
});
}
// Helper to calculate size of a log entry
function calculateLogSize(log: any): number {
return JSON.stringify(log).length;
}
// Helper to truncate logs based on character limit
function truncateLogsToQueryLimit(logs: any[]): any[] {
if (logs.length === 0) return logs;
// First process logs according to current settings
const processedLogs = processLogsWithSettings(logs);
let currentSize = 0;
const result = [];
for (const log of processedLogs) {
const logSize = calculateLogSize(log);
// Check if adding this log would exceed the limit
if (currentSize + logSize > currentSettings.queryLimit) {
console.log(
`Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`
);
break;
}
// Add log and update size
result.push(log);
currentSize += logSize;
console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);
}
return result;
}
// Endpoint for the extension to POST data
app.post("/extension-log", (req, res) => {
console.log("\n=== Received Extension Log ===");
console.log("Request body:", {
dataType: req.body.data?.type,
timestamp: req.body.data?.timestamp,
hasSettings: !!req.body.settings,
});
const { data, settings } = req.body;
// Update settings if provided
if (settings) {
console.log("Updating settings:", settings);
currentSettings = {
...currentSettings,
...settings,
};
}
if (!data) {
console.log("Warning: No data received in log request");
res.status(400).json({ status: "error", message: "No data provided" });
return;
}
console.log(`Processing ${data.type} log entry`);
switch (data.type) {
case "console-log":
console.log("Adding console log:", {
level: data.level,
message:
data.message?.substring(0, 100) +
(data.message?.length > 100 ? "..." : ""),
timestamp: data.timestamp,
});
consoleLogs.push(data);
if (consoleLogs.length > currentSettings.logLimit) {
console.log(
`Console logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
);
consoleLogs.shift();
}
break;
case "console-error":
console.log("Adding console error:", {
level: data.level,
message:
data.message?.substring(0, 100) +
(data.message?.length > 100 ? "..." : ""),
timestamp: data.timestamp,
});
consoleErrors.push(data);
if (consoleErrors.length > currentSettings.logLimit) {
console.log(
`Console errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
);
consoleErrors.shift();
}
break;
case "network-request":
const logEntry = {
url: data.url,
method: data.method,
status: data.status,
timestamp: data.timestamp,
};
console.log("Adding network request:", logEntry);
// Route network requests based on status code
if (data.status >= 400) {
networkErrors.push(data);
if (networkErrors.length > currentSettings.logLimit) {
console.log(
`Network errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
);
networkErrors.shift();
}
} else {
networkSuccess.push(data);
if (networkSuccess.length > currentSettings.logLimit) {
console.log(
`Network success logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
);
networkSuccess.shift();
}
}
break;
case "selected-element":
console.log("Updating selected element:", {
tagName: data.element?.tagName,
id: data.element?.id,
className: data.element?.className,
});
selectedElement = data.element;
break;
default:
console.log("Unknown log type:", data.type);
}
console.log("Current log counts:", {
consoleLogs: consoleLogs.length,
consoleErrors: consoleErrors.length,
networkErrors: networkErrors.length,
networkSuccess: networkSuccess.length,
});
console.log("=== End Extension Log ===\n");
res.json({ status: "ok" });
});
// Update GET endpoints to use the new function
app.get("/console-logs", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);
res.json(truncatedLogs);
});
app.get("/console-errors", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);
res.json(truncatedLogs);
});
app.get("/network-errors", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(networkErrors);
res.json(truncatedLogs);
});
app.get("/network-success", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);
res.json(truncatedLogs);
});
app.get("/all-xhr", (req, res) => {
// Merge and sort network success and error logs by timestamp
const mergedLogs = [...networkSuccess, ...networkErrors].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);
res.json(truncatedLogs);
});
// Add new endpoint for selected element
app.post("/selected-element", (req, res) => {
const { data } = req.body;
selectedElement = data;
res.json({ status: "ok" });
});
app.get("/selected-element", (req, res) => {
res.json(selectedElement || { message: "No element selected" });
});
app.get("/.port", (req, res) => {
res.send(PORT.toString());
});
// Add function to clear all logs
function clearAllLogs() {
console.log("Wiping all logs...");
consoleLogs.length = 0;
consoleErrors.length = 0;
networkErrors.length = 0;
networkSuccess.length = 0;
allXhr.length = 0;
selectedElement = null;
console.log("All logs have been wiped");
}
// Add endpoint to wipe logs
app.post("/wipelogs", (req, res) => {
clearAllLogs();
res.json({ status: "ok", message: "All logs cleared successfully" });
});
interface ScreenshotMessage {
type: "screenshot-data" | "screenshot-error";
data?: string;
path?: string;
error?: string;
}
export class BrowserConnector {
private wss: WebSocket.Server;
private activeConnection: WebSocket | null = null;
private app: express.Application;
private server: any;
constructor(app: express.Application, server: any) {
this.app = app;
this.server = server;
// Initialize WebSocket server using the existing HTTP server
this.wss = new WebSocket.Server({
noServer: true,
path: "/extension-ws",
});
// Register the capture-screenshot endpoint
this.app.post(
"/capture-screenshot",
async (req: express.Request, res: express.Response) => {
console.log(
"Browser Connector: Received request to /capture-screenshot endpoint"
);
console.log("Browser Connector: Request body:", req.body);
console.log(
"Browser Connector: Active WebSocket connection:",
!!this.activeConnection
);
await this.captureScreenshot(req, res);
}
);
// Handle upgrade requests for WebSocket
this.server.on(
"upgrade",
(request: IncomingMessage, socket: Socket, head: Buffer) => {
if (request.url === "/extension-ws") {
this.wss.handleUpgrade(request, socket, head, (ws) => {
this.wss.emit("connection", ws, request);
});
}
}
);
this.wss.on("connection", (ws) => {
console.log("Chrome extension connected via WebSocket");
this.activeConnection = ws;
ws.on("message", (message) => {
try {
const data = JSON.parse(message.toString());
// Log message without the base64 data
console.log("Received WebSocket message:", {
...data,
data: data.data ? "[base64 data]" : undefined,
});
// Handle screenshot response
if (data.type === "screenshot-data" && data.data) {
console.log("Received screenshot data");
console.log("Screenshot path from extension:", data.path);
// Get the most recent callback since we're not using requestId anymore
const callbacks = Array.from(screenshotCallbacks.values());
if (callbacks.length > 0) {
const callback = callbacks[0];
console.log("Found callback, resolving promise");
// Pass both the data and path to the resolver
callback.resolve({ data: data.data, path: data.path });
screenshotCallbacks.clear(); // Clear all callbacks
} else {
console.log("No callbacks found for screenshot");
}
}
// Handle screenshot error
else if (data.type === "screenshot-error") {
console.log("Received screenshot error:", data.error);
const callbacks = Array.from(screenshotCallbacks.values());
if (callbacks.length > 0) {
const callback = callbacks[0];
callback.reject(
new Error(data.error || "Screenshot capture failed")
);
screenshotCallbacks.clear(); // Clear all callbacks
}
} else {
console.log("Unhandled message type:", data.type);
}
} catch (error) {
console.error("Error processing WebSocket message:", error);
}
});
ws.on("close", () => {
console.log("Chrome extension disconnected");
if (this.activeConnection === ws) {
this.activeConnection = null;
}
});
});
// Add screenshot endpoint
this.app.post(
"/screenshot",
(req: express.Request, res: express.Response): void => {
console.log(
"Browser Connector: Received request to /screenshot endpoint"
);
console.log("Browser Connector: Request body:", req.body);
try {
console.log("Received screenshot capture request");
const { data, path: outputPath } = req.body;
if (!data) {
console.log("Screenshot request missing data");
res.status(400).json({ error: "Missing screenshot data" });
return;
}
// Use provided path or default to downloads folder
const targetPath = outputPath || getDefaultDownloadsFolder();
console.log(`Using screenshot path: ${targetPath}`);
// Remove the data:image/png;base64, prefix
const base64Data = data.replace(/^data:image\/png;base64,/, "");
// Create the full directory path if it doesn't exist
fs.mkdirSync(targetPath, { recursive: true });
console.log(`Created/verified directory: ${targetPath}`);
// Generate a unique filename using timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const fullPath = path.join(targetPath, filename);
console.log(`Saving screenshot to: ${fullPath}`);
// Write the file
fs.writeFileSync(fullPath, base64Data, "base64");
console.log("Screenshot saved successfully");
res.json({
path: fullPath,
filename: filename,
});
} catch (error: unknown) {
console.error("Error saving screenshot:", error);
if (error instanceof Error) {
res.status(500).json({ error: error.message });
} else {
res.status(500).json({ error: "An unknown error occurred" });
}
}
}
);
}
private async handleScreenshot(req: express.Request, res: express.Response) {
if (!this.activeConnection) {
return res.status(503).json({ error: "Chrome extension not connected" });
}
try {
const result = await new Promise((resolve, reject) => {
// Set up one-time message handler for this screenshot request
const messageHandler = (message: WebSocket.Data) => {
try {
const response: ScreenshotMessage = JSON.parse(message.toString());
if (response.type === "screenshot-error") {
reject(new Error(response.error));
return;
}
if (
response.type === "screenshot-data" &&
response.data &&
response.path
) {
// Remove the data:image/png;base64, prefix
const base64Data = response.data.replace(
/^data:image\/png;base64,/,
""
);
// Ensure the directory exists
const dir = path.dirname(response.path);
fs.mkdirSync(dir, { recursive: true });
// Generate a unique filename using timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const fullPath = path.join(response.path, filename);
// Write the file
fs.writeFileSync(fullPath, base64Data, "base64");
resolve({
path: fullPath,
filename: filename,
});
}
} catch (error) {
reject(error);
} finally {
this.activeConnection?.removeListener("message", messageHandler);
}
};
// Add temporary message handler
this.activeConnection?.on("message", messageHandler);
// Request screenshot
this.activeConnection?.send(
JSON.stringify({ type: "take-screenshot" })
);
// Set timeout
setTimeout(() => {
this.activeConnection?.removeListener("message", messageHandler);
reject(new Error("Screenshot timeout"));
}, 30000); // 30 second timeout
});
res.json(result);
} catch (error: unknown) {
if (error instanceof Error) {
res.status(500).json({ error: error.message });
} else {
res.status(500).json({ error: "An unknown error occurred" });
}
}
}
// Add new endpoint for programmatic screenshot capture
async captureScreenshot(req: express.Request, res: express.Response) {
console.log("Browser Connector: Starting captureScreenshot method");
console.log("Browser Connector: Request headers:", req.headers);
console.log("Browser Connector: Request method:", req.method);
if (!this.activeConnection) {
console.log(
"Browser Connector: No active WebSocket connection to Chrome extension"
);
return res.status(503).json({ error: "Chrome extension not connected" });
}
try {
console.log("Browser Connector: Starting screenshot capture...");
const requestId = Date.now().toString();
console.log("Browser Connector: Generated requestId:", requestId);
// Create promise that will resolve when we get the screenshot data
const screenshotPromise = new Promise<{ data: string; path?: string }>(
(resolve, reject) => {
console.log(
`Browser Connector: Setting up screenshot callback for requestId: ${requestId}`
);
// Store callback in map
screenshotCallbacks.set(requestId, { resolve, reject });
console.log(
"Browser Connector: Current callbacks:",
Array.from(screenshotCallbacks.keys())
);
// Set timeout to clean up if we don't get a response
setTimeout(() => {
if (screenshotCallbacks.has(requestId)) {
console.log(
`Browser Connector: Screenshot capture timed out for requestId: ${requestId}`
);
screenshotCallbacks.delete(requestId);
reject(
new Error(
"Screenshot capture timed out - no response from Chrome extension"
)
);
}
}, 10000);
}
);
// Send screenshot request to extension
const message = JSON.stringify({
type: "take-screenshot",
requestId: requestId,
});
console.log(
`Browser Connector: Sending WebSocket message to extension:`,
message
);
this.activeConnection.send(message);
// Wait for screenshot data
console.log("Browser Connector: Waiting for screenshot data...");
const { data: base64Data, path: customPath } = await screenshotPromise;
console.log("Browser Connector: Received screenshot data, saving...");
console.log("Browser Connector: Custom path from extension:", customPath);
// Determine target path
const targetPath =
customPath ||
currentSettings.screenshotPath ||
getDefaultDownloadsFolder();
console.log(`Browser Connector: Using path: ${targetPath}`);
if (!base64Data) {
throw new Error("No screenshot data received from Chrome extension");
}
fs.mkdirSync(targetPath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const fullPath = path.join(targetPath, filename);
// Remove the data:image/png;base64, prefix if present
const cleanBase64 = base64Data.replace(/^data:image\/png;base64,/, "");
// Save the file
fs.writeFileSync(fullPath, cleanBase64, "base64");
console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
res.json({
path: fullPath,
filename: filename,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
"Browser Connector: Error capturing screenshot:",
errorMessage
);
res.status(500).json({
error: errorMessage,
});
}
}
}
// Move the server creation before BrowserConnector instantiation
const server = app.listen(PORT, () => {
console.log(`Aggregator listening on http://127.0.0.1:${PORT}`);
});
// Initialize the browser connector with the existing app AND server
const browserConnector = new BrowserConnector(app, server);
// Handle shutdown gracefully
process.on("SIGINT", () => {
server.close(() => {
console.log("Server shut down");
process.exit(0);
});
});

1042
browser-tools-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
{
"name": "@agentdeskai/browser-tools-server",
"version": "1.0.5",
"description": "A browser tools server for capturing and managing browser events, logs, and screenshots",
"main": "dist/browser-connector.js",
"bin": {
"browser-tools-server": "./dist/browser-connector.js"
},
"scripts": {
"build": "tsc",
"start": "tsc && node dist/browser-connector.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"browser",
"tools",
"debugging",
"logging",
"screenshots",
"chrome",
"extension"
],
"author": "AgentDesk AI",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"llm-cost": "^1.0.5",
"ws": "^8.18.0"
},
"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",
"typescript": "^5.7.3"
}
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8" />
<title>DevTools Logs</title>
<title>BrowserTools MCP</title>
</head>
<body>
<!-- DevTools extension script -->

View File

@ -13,7 +13,10 @@ let settings = {
// Keep track of debugger state
let isDebuggerAttached = false;
let attachDebuggerRetries = 0;
const currentTabId = chrome.devtools.inspectedWindow.tabId;
const MAX_ATTACH_RETRIES = 3;
const ATTACH_RETRY_DELAY = 1000; // 1 second
// Load saved settings on startup
chrome.storage.local.get(["browserConnectorSettings"], (result) => {
@ -22,10 +25,50 @@ chrome.storage.local.get(["browserConnectorSettings"], (result) => {
}
});
// Listen for settings updates
chrome.runtime.onMessage.addListener((message) => {
// Listen for settings updates and screenshot capture requests
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "SETTINGS_UPDATED") {
settings = message.settings;
} else if (message.type === "CAPTURE_SCREENSHOT") {
// Capture screenshot of the current tab
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
if (chrome.runtime.lastError) {
console.error("Screenshot capture failed:", chrome.runtime.lastError);
sendResponse({
success: false,
error: chrome.runtime.lastError.message,
});
return;
}
// Send screenshot data to browser connector via HTTP POST
fetch("http://127.0.0.1:3025/screenshot", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data: dataUrl,
path: settings.screenshotPath,
}),
})
.then((response) => response.json())
.then((result) => {
if (result.error) {
sendResponse({ success: false, error: result.error });
} else {
sendResponse({ success: true, path: result.path });
}
})
.catch((error) => {
console.error("Failed to save screenshot:", error);
sendResponse({
success: false,
error: error.message || "Failed to save screenshot",
});
});
});
return true; // Required to use sendResponse asynchronously
}
});
@ -157,9 +200,17 @@ function processJsonString(jsonString, maxLength) {
}
}
// Utility to send logs to browser-connector
// Helper to send logs to browser-connector
function sendToBrowserConnector(logData) {
console.log("Original log data size:", JSON.stringify(logData).length);
if (!logData) {
console.error("No log data provided to sendToBrowserConnector");
return;
}
console.log("Sending log data to browser connector:", {
type: logData.type,
timestamp: logData.timestamp,
});
// Process any string fields that might contain JSON
const processedData = { ...logData };
@ -224,7 +275,6 @@ function sendToBrowserConnector(logData) {
console.log("Final payload size:", finalPayloadSize);
if (finalPayloadSize > 1000000) {
// 1MB warning threshold
console.warn("Warning: Large payload detected:", finalPayloadSize);
console.warn(
"Payload preview:",
@ -236,11 +286,38 @@ function sendToBrowserConnector(logData) {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log("Successfully sent log to browser-connector");
return response.json();
})
.then((data) => {
console.log("Browser connector response:", data);
})
.catch((error) => {
console.error("Failed to send log to browser-connector:", error);
});
}
// Add function to wipe logs
function wipeLogs() {
fetch("http://127.0.0.1:3025/wipelogs", {
method: "POST",
headers: { "Content-Type": "application/json" },
}).catch((error) => {
console.error("Failed to send log to browser-connector:", error);
console.error("Failed to wipe logs:", error);
});
}
// Listen for page refreshes
chrome.devtools.network.onNavigated.addListener(() => {
console.log("Page navigated/refreshed - wiping logs");
wipeLogs();
});
// 1) Listen for network requests
chrome.devtools.network.onRequestFinished.addListener((request) => {
if (request._resourceType === "xhr" || request._resourceType === "fetch") {
@ -260,84 +337,65 @@ chrome.devtools.network.onRequestFinished.addListener((request) => {
}
});
// Move the console message listener outside the panel creation
const consoleMessageListener = (source, method, params) => {
console.log("Debugger event:", method, source.tabId, currentTabId);
// Only process events for our tab
if (source.tabId !== currentTabId) {
console.log("Ignoring event from different tab");
return;
}
if (method === "Console.messageAdded") {
console.log("Console message received:", params.message);
const entry = {
type: params.message.level === "error" ? "console-error" : "console-log",
level: params.message.level,
message: params.message.text,
timestamp: Date.now(),
};
console.log("Sending console entry:", entry);
sendToBrowserConnector(entry);
} else {
console.log("Unhandled debugger method:", method);
}
};
// Helper function to attach debugger
function attachDebugger() {
if (isDebuggerAttached) {
console.log("Debugger already attached");
return;
}
async function attachDebugger() {
// First check if we're already attached to this tab
chrome.debugger.getTargets((targets) => {
const isAlreadyAttached = targets.some(
(target) => target.tabId === currentTabId && target.attached
);
// Check if the tab still exists
chrome.tabs.get(currentTabId, (tab) => {
if (isAlreadyAttached) {
console.log("Found existing debugger attachment, detaching first...");
// Force detach first to ensure clean state
chrome.debugger.detach({ tabId: currentTabId }, () => {
// Ignore any errors during detach
if (chrome.runtime.lastError) {
console.log("Error during forced detach:", chrome.runtime.lastError);
}
// Now proceed with fresh attachment
performAttach();
});
} else {
// No existing attachment, proceed directly
performAttach();
}
});
}
function performAttach() {
console.log("Performing debugger attachment to tab:", currentTabId);
chrome.debugger.attach({ tabId: currentTabId }, "1.3", () => {
if (chrome.runtime.lastError) {
console.log("Tab no longer exists:", chrome.runtime.lastError);
console.error("Failed to attach debugger:", chrome.runtime.lastError);
isDebuggerAttached = false;
return;
}
console.log("Attaching debugger to tab:", currentTabId);
chrome.debugger.attach({ tabId: currentTabId }, "1.3", () => {
if (chrome.runtime.lastError) {
console.error("Failed to attach debugger:", chrome.runtime.lastError);
isDebuggerAttached = false;
return;
}
isDebuggerAttached = true;
console.log("Debugger successfully attached");
isDebuggerAttached = true;
console.log("Debugger successfully attached");
// Add the event listener when attaching
chrome.debugger.onEvent.addListener(consoleMessageListener);
// Add the event listener when attaching
chrome.debugger.onEvent.addListener(consoleMessageListener);
chrome.debugger.sendCommand(
{ tabId: currentTabId },
"Console.enable",
{},
() => {
if (chrome.runtime.lastError) {
console.error(
"Failed to enable console:",
chrome.runtime.lastError
);
return;
}
console.log("Console API successfully enabled");
chrome.debugger.sendCommand(
{ tabId: currentTabId },
"Console.enable",
{},
() => {
if (chrome.runtime.lastError) {
console.error("Failed to enable console:", chrome.runtime.lastError);
return;
}
);
});
console.log("Console API successfully enabled");
}
);
});
}
// Helper function to detach debugger
function detachDebugger() {
if (!isDebuggerAttached) return;
// Remove the event listener when detaching
// Remove the event listener first
chrome.debugger.onEvent.removeListener(consoleMessageListener);
// Check if debugger is actually attached before trying to detach
@ -354,9 +412,10 @@ function detachDebugger() {
chrome.debugger.detach({ tabId: currentTabId }, () => {
if (chrome.runtime.lastError) {
console.error("Failed to detach debugger:", chrome.runtime.lastError);
isDebuggerAttached = false;
return;
console.warn(
"Warning during debugger detach:",
chrome.runtime.lastError
);
}
isDebuggerAttached = false;
console.log("Debugger detached");
@ -364,20 +423,48 @@ function detachDebugger() {
});
}
// Move the console message listener outside the panel creation
const consoleMessageListener = (source, method, params) => {
// Only process events for our tab
if (source.tabId !== currentTabId) {
return;
}
if (method === "Console.messageAdded") {
console.log("Console message received:", params.message);
const entry = {
type: params.message.level === "error" ? "console-error" : "console-log",
level: params.message.level,
message: params.message.text,
timestamp: Date.now(),
};
console.log("Sending console entry:", entry);
sendToBrowserConnector(entry);
}
};
// 2) Use DevTools Protocol to capture console logs
chrome.devtools.panels.create("Browser Logs", "", "panel.html", (panel) => {
chrome.devtools.panels.create("BrowserToolsMCP", "", "panel.html", (panel) => {
// Initial attach - we'll keep the debugger attached as long as DevTools is open
attachDebugger();
// Add message passing to panel.js
// Handle panel showing
panel.onShown.addListener((panelWindow) => {
panelWindow.postMessage({ type: "initializeSelectionButton" }, "*");
if (!isDebuggerAttached) {
attachDebugger();
}
});
});
// Clean up only when the entire DevTools window is closed
// Clean up when DevTools window is closed
window.addEventListener("unload", () => {
detachDebugger();
if (ws) {
ws.close();
}
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
}
});
// Function to capture and send element data
@ -439,62 +526,73 @@ function setupWebSocket() {
ws = new WebSocket("ws://localhost:3025/extension-ws");
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log("Chrome Extension: Received WebSocket message:", message);
if (message.type === "take-screenshot") {
console.log("Chrome Extension: Taking screenshot...");
// Capture screenshot of the current tab
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
if (chrome.runtime.lastError) {
console.error(
"Chrome Extension: Screenshot capture failed:",
chrome.runtime.lastError
);
ws.send(
JSON.stringify({
type: "screenshot-error",
error: chrome.runtime.lastError.message,
requestId: message.requestId,
})
);
return;
}
console.log("Chrome Extension: Screenshot captured successfully");
// Just send the screenshot data, let the server handle paths
const response = {
type: "screenshot-data",
data: dataUrl,
requestId: message.requestId,
// Only include path if it's configured in settings
...(settings.screenshotPath && { path: settings.screenshotPath }),
};
console.log("Chrome Extension: Sending screenshot data response", {
...response,
data: "[base64 data]",
});
ws.send(JSON.stringify(response));
});
}
} catch (error) {
console.error(
"Chrome Extension: Error processing WebSocket message:",
error
);
}
};
ws.onopen = () => {
console.log("WebSocket connected");
console.log("Chrome Extension: WebSocket connected");
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
};
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === "take-screenshot") {
if (!settings.screenshotPath) {
ws.send(
JSON.stringify({
type: "screenshot-error",
error: "Screenshot path not configured",
})
);
return;
}
// Capture screenshot of the current tab
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
if (chrome.runtime.lastError) {
ws.send(
JSON.stringify({
type: "screenshot-error",
error: chrome.runtime.lastError.message,
})
);
return;
}
ws.send(
JSON.stringify({
type: "screenshot-data",
data: dataUrl,
path: settings.screenshotPath,
})
);
});
}
} catch (error) {
console.error("Error processing WebSocket message:", error);
}
};
ws.onclose = () => {
console.log("WebSocket disconnected, attempting to reconnect...");
console.log(
"Chrome Extension: WebSocket disconnected, attempting to reconnect..."
);
wsReconnectTimeout = setTimeout(setupWebSocket, WS_RECONNECT_DELAY);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
console.error("Chrome Extension: WebSocket error:", error);
};
}

View File

@ -1,7 +1,7 @@
{
"name": "AI Browser Connector",
"name": "BrowserTools MCP",
"version": "1.0.0",
"description": "Captures console and network logs then sends them to a local aggregator for MCP usage",
"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,
"devtools_page": "devtools.html",
"permissions": [

View File

@ -62,55 +62,122 @@
margin-bottom: 16px;
border-radius: 4px;
}
.settings-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.settings-header h3 {
margin: 0;
}
.settings-content {
display: none;
margin-top: 16px;
}
.settings-content.visible {
display: block;
}
.chevron {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
}
.chevron.open {
transform: rotate(180deg);
}
.quick-actions {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.action-button {
background-color: #4a4a4a;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button:hover {
background-color: #5a5a5a;
}
.action-button.danger {
background-color: #f44336;
}
.action-button.danger:hover {
background-color: #d32f2f;
}
</style>
</head>
<body>
<div class="settings-section">
<h3>Log Settings</h3>
<div class="form-group">
<label for="log-limit">Log Limit (number of logs)</label>
<input type="number" id="log-limit" min="1" value="50">
</div>
<div class="form-group">
<label for="query-limit">Query Limit (characters)</label>
<input type="number" id="query-limit" min="1" value="30000">
</div>
<div class="form-group">
<label for="string-size-limit">String Size Limit (characters)</label>
<input type="number" id="string-size-limit" min="1" value="500">
</div>
<div class="form-group">
<label for="max-log-size">Max Log Size (characters)</label>
<input type="number" id="max-log-size" min="1000" value="20000">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="show-request-headers">
Include Request Headers
</label>
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="show-response-headers">
Include Response Headers
</label>
<h3>Quick Actions</h3>
<div class="quick-actions">
<button id="capture-screenshot" class="action-button">
Capture Screenshot
</button>
<button id="wipe-logs" class="action-button danger">
Wipe All Logs
</button>
</div>
</div>
<div class="settings-section">
<h3>Screenshot Settings</h3>
<div class="form-group">
<label for="screenshot-path">Screenshot Save Path</label>
<label for="screenshot-path">Provide a directory to save screenshots to (by default screenshots will be saved to your downloads folder if no path is provided)</label>
<input type="text" id="screenshot-path" placeholder="/path/to/screenshots">
</div>
</div>
<div class="settings-section">
<div class="settings-header" id="advanced-settings-header">
<h3>Advanced Settings</h3>
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<div class="settings-content" id="advanced-settings-content">
<div class="form-group">
<label for="log-limit">Log Limit (number of logs)</label>
<input type="number" id="log-limit" min="1" value="50">
</div>
<div class="form-group">
<label for="query-limit">Query Limit (characters)</label>
<input type="number" id="query-limit" min="1" value="30000">
</div>
<div class="form-group">
<label for="string-size-limit">String Size Limit (characters)</label>
<input type="number" id="string-size-limit" min="1" value="500">
</div>
<div class="form-group">
<label for="max-log-size">Max Log Size (characters)</label>
<input type="number" id="max-log-size" min="1000" value="20000">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="show-request-headers">
Include Request Headers
</label>
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="show-response-headers">
Include Response Headers
</label>
</div>
</div>
</div>
<script src="panel.js"></script>
</body>
</html>

View File

@ -29,6 +29,21 @@ const showResponseHeadersCheckbox = document.getElementById(
);
const maxLogSizeInput = document.getElementById("max-log-size");
const screenshotPathInput = document.getElementById("screenshot-path");
const captureScreenshotButton = document.getElementById("capture-screenshot");
// Initialize collapsible advanced settings
const advancedSettingsHeader = document.getElementById(
"advanced-settings-header"
);
const advancedSettingsContent = document.getElementById(
"advanced-settings-content"
);
const chevronIcon = advancedSettingsHeader.querySelector(".chevron");
advancedSettingsHeader.addEventListener("click", () => {
advancedSettingsContent.classList.toggle("visible");
chevronIcon.classList.toggle("open");
});
// Update UI from settings
function updateUIFromSettings() {
@ -86,3 +101,48 @@ screenshotPathInput.addEventListener("change", (e) => {
settings.screenshotPath = e.target.value;
saveSettings();
});
// Add screenshot capture functionality
captureScreenshotButton.addEventListener("click", () => {
captureScreenshotButton.textContent = "Capturing...";
// Send message to devtools.js to capture screenshot
chrome.runtime.sendMessage({ type: "CAPTURE_SCREENSHOT" }, (response) => {
if (!response) {
captureScreenshotButton.textContent = "Failed to capture!";
console.error("Screenshot capture failed: No response received");
} else if (!response.success) {
captureScreenshotButton.textContent = "Failed to capture!";
console.error("Screenshot capture failed:", response.error);
} else {
captureScreenshotButton.textContent = "Screenshot captured!";
}
setTimeout(() => {
captureScreenshotButton.textContent = "Capture Screenshot";
}, 2000);
});
});
// Add wipe logs functionality
const wipeLogsButton = document.getElementById("wipe-logs");
wipeLogsButton.addEventListener("click", () => {
fetch("http://127.0.0.1:3025/wipelogs", {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((result) => {
console.log("Logs wiped successfully:", result.message);
wipeLogsButton.textContent = "Logs Wiped!";
setTimeout(() => {
wipeLogsButton.textContent = "Wipe All Logs";
}, 2000);
})
.catch((error) => {
console.error("Failed to wipe logs:", error);
wipeLogsButton.textContent = "Failed to Wipe Logs";
setTimeout(() => {
wipeLogsButton.textContent = "Wipe All Logs";
}, 2000);
});
});

View File

@ -1,31 +0,0 @@
{
"name": "mcp-server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "tsc && node dist/browser-connector.js",
"mcp": "tsc && node dist/mcp-server.js",
"inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"llm-cost": "^1.0.5",
"ws": "^8.18.0"
},
"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",
"typescript": "^5.7.3"
}
}

View File

@ -1,364 +0,0 @@
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { tokenizeAndEstimateCost } from "llm-cost";
import WebSocket from "ws";
import fs from "fs";
import path from "path";
import { IncomingMessage } from "http";
import { Socket } from "net";
// We store logs in memory
const consoleLogs: any[] = [];
const consoleErrors: any[] = [];
const networkErrors: any[] = [];
const networkSuccess: any[] = [];
const allXhr: any[] = [];
// Add settings state
let currentSettings = {
logLimit: 50,
queryLimit: 30000,
showRequestHeaders: false,
showResponseHeaders: false,
model: "claude-3-sonnet",
stringSizeLimit: 500,
maxLogSize: 20000,
};
// Add new storage for selected element
let selectedElement: any = null;
const app = express();
const PORT = 3025;
app.use(cors());
app.use(bodyParser.json());
// Helper to recursively truncate strings in any data structure
function truncateStringsInData(data: any, maxLength: number): any {
if (typeof data === "string") {
return data.length > maxLength
? data.substring(0, maxLength) + "... (truncated)"
: data;
}
if (Array.isArray(data)) {
return data.map((item) => truncateStringsInData(item, maxLength));
}
if (typeof data === "object" && data !== null) {
const result: any = {};
for (const [key, value] of Object.entries(data)) {
result[key] = truncateStringsInData(value, maxLength);
}
return result;
}
return data;
}
// Helper to safely parse and process JSON strings
function processJsonString(jsonString: string, maxLength: number): string {
try {
// Try to parse the string as JSON
const parsed = JSON.parse(jsonString);
// Process any strings within the parsed JSON
const processed = truncateStringsInData(parsed, maxLength);
// Stringify the processed data
return JSON.stringify(processed);
} catch (e) {
// If it's not valid JSON, treat it as a regular string
return truncateStringsInData(jsonString, maxLength);
}
}
// Helper to process logs based on settings
function processLogsWithSettings(logs: any[]) {
return logs.map((log) => {
const processedLog = { ...log };
if (log.type === "network-request") {
// Handle headers visibility
if (!currentSettings.showRequestHeaders) {
delete processedLog.requestHeaders;
}
if (!currentSettings.showResponseHeaders) {
delete processedLog.responseHeaders;
}
}
return processedLog;
});
}
// Helper to calculate size of a log entry
function calculateLogSize(log: any): number {
return JSON.stringify(log).length;
}
// Helper to truncate logs based on character limit
function truncateLogsToQueryLimit(logs: any[]): any[] {
if (logs.length === 0) return logs;
// First process logs according to current settings
const processedLogs = processLogsWithSettings(logs);
let currentSize = 0;
const result = [];
for (const log of processedLogs) {
const logSize = calculateLogSize(log);
// Check if adding this log would exceed the limit
if (currentSize + logSize > currentSettings.queryLimit) {
console.log(
`Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`
);
break;
}
// Add log and update size
result.push(log);
currentSize += logSize;
console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);
}
return result;
}
// Endpoint for the extension to POST data
app.post("/extension-log", (req, res) => {
const { data, settings } = req.body;
// Update settings if provided
if (settings) {
currentSettings = {
...currentSettings,
...settings,
};
}
console.log("Received log:", data);
switch (data.type) {
case "console-log":
consoleLogs.push(data);
if (consoleLogs.length > currentSettings.logLimit) consoleLogs.shift();
break;
case "console-error":
consoleErrors.push(data);
if (consoleErrors.length > currentSettings.logLimit)
consoleErrors.shift();
break;
case "network-request":
// Route network requests based on status code
if (data.status >= 400) {
networkErrors.push(data);
if (networkErrors.length > currentSettings.logLimit)
networkErrors.shift();
} else {
networkSuccess.push(data);
if (networkSuccess.length > currentSettings.logLimit)
networkSuccess.shift();
}
break;
case "selected-element":
selectedElement = data.element;
break;
}
res.json({ status: "ok" });
});
// Update GET endpoints to use the new function
app.get("/console-logs", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);
res.json(truncatedLogs);
});
app.get("/console-errors", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);
res.json(truncatedLogs);
});
app.get("/network-errors", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(networkErrors);
res.json(truncatedLogs);
});
app.get("/network-success", (req, res) => {
const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);
res.json(truncatedLogs);
});
app.get("/all-xhr", (req, res) => {
// Merge and sort network success and error logs by timestamp
const mergedLogs = [...networkSuccess, ...networkErrors].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);
res.json(truncatedLogs);
});
// Add new endpoint for selected element
app.post("/selected-element", (req, res) => {
const { data } = req.body;
selectedElement = data;
res.json({ status: "ok" });
});
app.get("/selected-element", (req, res) => {
res.json(selectedElement || { message: "No element selected" });
});
app.get("/.port", (req, res) => {
res.send(PORT.toString());
});
interface ScreenshotMessage {
type: "screenshot-data" | "screenshot-error";
data?: string;
path?: string;
error?: string;
}
export class BrowserConnector {
private wss: WebSocket.Server;
private activeConnection: WebSocket | null = null;
private app: express.Application;
private server: any;
constructor(app: express.Application, server: any) {
this.app = app;
this.server = server;
// Initialize WebSocket server using the existing HTTP server
this.wss = new WebSocket.Server({
noServer: true,
path: "/extension-ws",
});
// Handle upgrade requests for WebSocket
this.server.on(
"upgrade",
(request: IncomingMessage, socket: Socket, head: Buffer) => {
if (request.url === "/extension-ws") {
this.wss.handleUpgrade(request, socket, head, (ws) => {
this.wss.emit("connection", ws, request);
});
}
}
);
this.wss.on("connection", (ws) => {
console.log("Chrome extension connected via WebSocket");
this.activeConnection = ws;
ws.on("close", () => {
console.log("Chrome extension disconnected");
if (this.activeConnection === ws) {
this.activeConnection = null;
}
});
});
// Add screenshot endpoint
this.app.post("/screenshot", async (req, res) => {
await this.handleScreenshot(req, res);
});
}
private async handleScreenshot(req: express.Request, res: express.Response) {
if (!this.activeConnection) {
return res.status(503).json({ error: "Chrome extension not connected" });
}
try {
const result = await new Promise((resolve, reject) => {
// Set up one-time message handler for this screenshot request
const messageHandler = (message: WebSocket.Data) => {
try {
const response: ScreenshotMessage = JSON.parse(message.toString());
if (response.type === "screenshot-error") {
reject(new Error(response.error));
return;
}
if (
response.type === "screenshot-data" &&
response.data &&
response.path
) {
// Remove the data:image/png;base64, prefix
const base64Data = response.data.replace(
/^data:image\/png;base64,/,
""
);
// Ensure the directory exists
const dir = path.dirname(response.path);
fs.mkdirSync(dir, { recursive: true });
// Generate a unique filename using timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const fullPath = path.join(response.path, filename);
// Write the file
fs.writeFileSync(fullPath, base64Data, "base64");
resolve({
path: fullPath,
filename: filename,
});
}
} catch (error) {
reject(error);
} finally {
this.activeConnection?.removeListener("message", messageHandler);
}
};
// Add temporary message handler
this.activeConnection?.on("message", messageHandler);
// Request screenshot
this.activeConnection?.send(
JSON.stringify({ type: "take-screenshot" })
);
// Set timeout
setTimeout(() => {
this.activeConnection?.removeListener("message", messageHandler);
reject(new Error("Screenshot timeout"));
}, 30000); // 30 second timeout
});
res.json(result);
} catch (error: unknown) {
if (error instanceof Error) {
res.status(500).json({ error: error.message });
} else {
res.status(500).json({ error: "An unknown error occurred" });
}
}
}
}
// Move the server creation before BrowserConnector instantiation
const server = app.listen(PORT, () => {
console.log(`Aggregator listening on http://127.0.0.1:${PORT}`);
// Write the port to a file so mcp-server can read it
fs.writeFileSync(".port", PORT.toString());
});
// Initialize the browser connector with the existing app AND server
const browserConnector = new BrowserConnector(app, server);
// Handle shutdown gracefully
process.on("SIGINT", () => {
server.close(() => {
console.log("Server shut down");
process.exit(0);
});
});

View File

@ -1,111 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}