browser-connect-mcp
Version:
MCP server for browser DevTools and backend debugging - analyze console logs, network requests, and backend logs with AI assistance
286 lines • 11.1 kB
JavaScript
import CDP from 'chrome-remote-interface';
import { spawn, exec } from 'child_process';
import { tmpdir, platform } from 'os';
import { accessSync } from 'fs';
import logger from './logger.js';
export class BrowserManager {
connections = new Map();
consoleMessages = new Map();
networkRequests = new Map();
networkResponses = new Map();
browserProcess = null;
async connectToBrowser(host = 'localhost', port = 9222) {
try {
const targets = await CDP.List({ host, port });
const connections = [];
for (const target of targets) {
if (target.type === 'page' && target.webSocketDebuggerUrl) {
connections.push({
id: target.id,
debuggerUrl: target.webSocketDebuggerUrl,
title: target.title || 'Untitled',
type: target.type,
connected: false,
});
}
}
return connections;
}
catch (error) {
logger.error('Failed to connect to browser:', error);
throw new Error(`Failed to connect to browser: ${error}`);
}
}
async connectToTab(tabId, host = 'localhost', port = 9222) {
try {
const targets = await CDP.List({ host, port });
const target = targets.find(t => t.id === tabId);
if (!target) {
throw new Error(`Tab with id ${tabId} not found`);
}
const client = await CDP({ target: tabId, host, port });
this.connections.set(tabId, client);
// Initialize storage for this tab
this.consoleMessages.set(tabId, []);
this.networkRequests.set(tabId, new Map());
this.networkResponses.set(tabId, new Map());
// Enable necessary domains
await Promise.all([
client.Console.enable(),
client.Network.enable(),
client.Page.enable(),
client.Runtime.enable(),
]);
// Set up event listeners
this.setupConsoleListeners(tabId, client);
this.setupNetworkListeners(tabId, client);
logger.info(`Connected to tab ${tabId}`);
}
catch (error) {
logger.error(`Failed to connect to tab ${tabId}:`, error);
throw error;
}
}
setupConsoleListeners(tabId, client) {
client.Console.messageAdded((params) => {
const message = {
level: params.message.level,
text: params.message.text,
timestamp: Date.now(),
url: params.message.url,
lineNumber: params.message.line,
columnNumber: params.message.column,
stackTrace: params.message.stackTrace,
};
const messages = this.consoleMessages.get(tabId) || [];
messages.push(message);
// Keep only last 10000 messages
if (messages.length > 10000) {
messages.shift();
}
this.consoleMessages.set(tabId, messages);
});
client.Runtime.consoleAPICalled((params) => {
const message = {
level: params.type,
text: params.args.map(arg => this.extractValue(arg)).join(' '),
timestamp: params.timestamp,
args: params.args,
stackTrace: params.stackTrace,
};
const messages = this.consoleMessages.get(tabId) || [];
messages.push(message);
if (messages.length > 10000) {
messages.shift();
}
this.consoleMessages.set(tabId, messages);
});
}
setupNetworkListeners(tabId, client) {
const requests = this.networkRequests.get(tabId) || new Map();
const responses = this.networkResponses.get(tabId) || new Map();
client.Network.requestWillBeSent((params) => {
const request = {
requestId: params.requestId,
url: params.request.url,
method: params.request.method,
headers: params.request.headers,
timestamp: params.timestamp * 1000,
postData: params.request.postData,
hasPostData: params.request.hasPostData,
resourceType: params.type,
};
requests.set(params.requestId, request);
// Keep only last 5000 requests
if (requests.size > 5000) {
const firstKey = requests.keys().next().value;
requests.delete(firstKey);
}
});
client.Network.responseReceived((params) => {
const requestTime = requests.get(params.requestId)?.timestamp;
const response = {
requestId: params.requestId,
url: params.response.url,
status: params.response.status,
statusText: params.response.statusText,
headers: params.response.headers,
mimeType: params.response.mimeType,
timestamp: params.timestamp * 1000,
responseTime: requestTime ? params.timestamp * 1000 - requestTime : undefined,
encodedDataLength: params.response.encodedDataLength,
};
responses.set(params.requestId, response);
// Keep only last 5000 responses
if (responses.size > 5000) {
const firstKey = responses.keys().next().value;
responses.delete(firstKey);
}
});
}
extractValue(arg) {
if (!arg)
return 'undefined';
switch (arg.type) {
case 'string':
case 'number':
case 'boolean':
return String(arg.value);
case 'undefined':
return 'undefined';
case 'null':
return 'null';
case 'object':
if (arg.subtype === 'array') {
return `Array(${arg.description})`;
}
return arg.description || JSON.stringify(arg.value || {});
default:
return arg.description || String(arg.value);
}
}
async launchBrowser(executablePath, url, debugPort = 9222) {
const chromePath = executablePath || this.findChromePath();
if (!chromePath) {
throw new Error('Chrome/Chromium executable not found');
}
const args = [
`--remote-debugging-port=${debugPort}`,
'--no-first-run',
'--no-default-browser-check',
'--user-data-dir=' + tmpdir() + '/browser-connect-mcp',
];
// Add URL if provided
if (url) {
args.push(url);
}
this.browserProcess = spawn(chromePath, args);
this.browserProcess.on('error', (error) => {
logger.error('Browser process error:', error);
});
// Wait for browser to start
await new Promise(resolve => setTimeout(resolve, 2000));
}
findChromePath() {
const paths = [
// macOS
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
// Linux
'/usr/bin/google-chrome',
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
'/usr/bin/brave-browser',
'/usr/bin/microsoft-edge',
// Windows
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
];
for (const path of paths) {
try {
accessSync(path);
return path;
}
catch {
// Continue to next path
}
}
return null;
}
async openUrlInDefaultBrowser(url) {
const currentPlatform = platform();
return new Promise((resolve, reject) => {
let command;
switch (currentPlatform) {
case 'darwin': // macOS
command = `open "${url}"`;
break;
case 'win32': // Windows
command = `start "${url}"`;
break;
default: // Linux and others
command = `xdg-open "${url}"`;
break;
}
exec(command, (error) => {
if (error) {
logger.error('Failed to open URL in default browser:', error);
reject(error);
}
else {
logger.info(`Opened URL in default browser: ${url}`);
resolve();
}
});
});
}
async disconnectTab(tabId) {
const client = this.connections.get(tabId);
if (client) {
await client.close();
this.connections.delete(tabId);
this.consoleMessages.delete(tabId);
this.networkRequests.delete(tabId);
this.networkResponses.delete(tabId);
logger.info(`Disconnected from tab ${tabId}`);
}
}
async disconnectAll() {
for (const [, client] of this.connections) {
await client.close();
}
this.connections.clear();
this.consoleMessages.clear();
this.networkRequests.clear();
this.networkResponses.clear();
if (this.browserProcess) {
this.browserProcess.kill();
this.browserProcess = null;
}
}
getConsoleMessages(tabId) {
return this.consoleMessages.get(tabId) || [];
}
getNetworkRequests(tabId) {
const requests = this.networkRequests.get(tabId);
return requests ? Array.from(requests.values()) : [];
}
getNetworkResponses(tabId) {
const responses = this.networkResponses.get(tabId);
return responses ? Array.from(responses.values()) : [];
}
getConnectedTabs() {
return Array.from(this.connections.keys());
}
isConnected(tabId) {
return this.connections.has(tabId);
}
}
//# sourceMappingURL=browser-manager.js.map