@madappgang/brop-mcp
Version:
Model Context Protocol server for Browser Remote Operations Protocol (BROP) - Control Chrome browser remotely via MCP
1,034 lines (903 loc) • 28.3 kB
JavaScript
/**
* BROP Unified Bridge Server (Node.js)
*
* Unified architecture that combines:
* 1. BROP multiplexing for native BROP commands (port 9225)
* 2. CDP server for 3rd party tools like Playwright (port 9222)
* 3. Chrome extension connection (port 9224)
* 4. CDP discovery endpoints
*
* NO REAL CHROME DEPENDENCY - Everything routes through Chrome Extension APIs
*
* Key Features:
* - Clean protocol separation: BROP vs CDP
* - Multiplexed BROP clients for native commands
* - CDP compatibility for Playwright/Puppeteer
* - Proper session management for Target.* commands
* - HTTP discovery endpoints for CDP clients
*/
import http from "node:http";
import url from "node:url";
import WebSocket, { WebSocketServer } from "ws";
class TableLogger {
constructor(options = {}) {
this.tsWidth = 19;
this.statusWidth = 3;
this.typeWidth = 6;
this.commandWidth = 20;
this.connWidth = 50;
this.errorWidth = 20;
this.outputStream = options.outputStream || "stdout";
this.mcpMode = options.mcpMode || false;
}
getTimestamp() {
return new Date().toISOString().replace("T", " ").slice(0, 19);
}
formatField(text, width, align = "left") {
const str = String(text || "").slice(0, width);
if (str === "✅" || str === "❌" || str === "🔗" || str === "🔌") {
return str.padEnd(1) + " ".repeat(width - 1);
}
return align === "right" ? str.padStart(width) : str.padEnd(width);
}
formatRow(status, type, command, connection, error = "") {
const timestamp = this.getTimestamp();
const parts = [
this.formatField(timestamp, this.tsWidth),
this.formatField(status, this.statusWidth),
this.formatField(type, this.typeWidth),
this.formatField(command, this.commandWidth),
this.formatField(connection, this.connWidth),
this.formatField(error, this.errorWidth),
];
return parts.join(" │ ");
}
log(message) {
if (this.outputStream === "stderr" || this.mcpMode) {
console.error(message);
} else {
console.log(message);
}
}
printHeader() {
const header = this.formatRow(
"STS",
"TYPE",
"COMMAND/EVENT",
"CONNECTION",
"ERROR/DETAILS",
);
this.log("─".repeat(header.length));
this.log(header);
this.log("─".repeat(header.length));
}
logConnect(type, connection) {
this.log(this.formatRow("🔗", type, "connect", connection));
}
logDisconnect(type, connection) {
this.log(this.formatRow("🔌", type, "disconnect", connection));
}
logSuccess(type, command, connection, details = "") {
this.log(this.formatRow("✅", type, command, connection, details));
}
logError(type, command, connection, error) {
this.log(this.formatRow("❌", type, command, connection, error));
}
logSystem(message) {
this.log(`[${this.getTimestamp()}] ${message}`);
}
}
class UnifiedBridgeServer {
constructor(options = {}) {
this.startTime = Date.now();
// Extension connection (single point of truth)
this.extensionClient = null;
// BROP client multiplexing
this.bropClients = new Set();
this.bropConnections = new Map(); // client -> connection info
// CDP client multiplexing with session management
this.cdpClients = new Map(); // clientId -> client info
this.cdpClientCounter = 0;
this.sessionChannels = new Map(); // sessionId -> session info
this.targetToSession = new Map(); // targetId -> sessionId
this.sessionToTarget = new Map(); // sessionId -> targetId
this.targetToClient = new Map(); // targetId -> clientId
// Message routing
this.pendingBropRequests = new Map(); // messageId -> bropClient
this.pendingCdpRequests = new Map(); // messageId -> requestInfo
this.pendingCommandInfo = new Map(); // messageId -> { command, connection } for response logging
this.messageCounter = 0;
this.connectionCounter = 0;
// Default browser context ID (consistent across session)
this.defaultBrowserContextId = this.generateBrowserContextId();
// Server instances
this.bropServer = null;
this.extensionServer = null;
this.cdpServer = null;
this.httpServer = null;
this.running = false;
// Chrome-compatible browser info for CDP discovery
this.browserInfo = {
Browser: "Chrome/138.0.7204.15",
"Protocol-Version": "1.3",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"V8-Version": "13.8.258.9",
"WebKit-Version": "537.36 (@9f1120d029eadbc8ecc5c3d9b298c16d08aabf9f)",
webSocketDebuggerUrl:
"ws://localhost:9222/devtools/browser/brop-bridge-uuid-12345678",
};
// Logs for debugging
this.logs = [];
this.maxLogs = 1000;
// CDP message logging
this.cdpLogs = [];
this.maxCdpLogs = 5000;
this.cdpLoggingEnabled = true;
// Table logger
this.logger = new TableLogger({
outputStream: options.logToStderr ? "stderr" : "stdout",
mcpMode: options.mcpMode || false,
});
}
log(message, ...args) {
// Store all logs for debugging endpoint
const logEntry = {
timestamp: this.logger.getTimestamp(),
message: message,
args: args,
fullMessage: args.length > 0 ? `${message} ${args.join(" ")}` : message,
level: "info",
};
this.logs.push(logEntry);
// Keep only the last maxLogs entries
if (this.logs.length > this.maxLogs) {
this.logs.splice(0, this.logs.length - this.maxLogs);
}
// Use system logging for non-structured messages
this.logger.logSystem(message);
}
getNextMessageId() {
this.messageCounter++;
return `bridge_${this.messageCounter}`;
}
getNextConnectionId() {
this.connectionCounter++;
return `conn_${this.connectionCounter}`;
}
// Helper to format connection display with name
getConnectionDisplay(client) {
const clientInfo = this.bropConnections.get(client);
if (!clientInfo) return "unknown";
return clientInfo.name
? `${clientInfo.id}:${clientInfo.name}`
: clientInfo.id;
}
async startServers() {
this.running = true;
this.logger.printHeader();
try {
// Start BROP server (port 9225 - BROP clients)
this.bropServer = new WebSocketServer({
port: 9225,
host: "127.0.0.1",
perMessageDeflate: false,
});
this.bropServer.on("connection", (ws, req) =>
this.handleBropClient(ws, req),
);
this.log("🔧 BROP Server started on ws://localhost:9225");
// Start Extension server (port 9224 - extension connects here)
this.extensionServer = new WebSocketServer({
port: 9224,
host: "127.0.0.1",
perMessageDeflate: false,
});
this.extensionServer.on("connection", (ws, req) =>
this.handleExtensionClient(ws, req),
);
this.log("🔌 Extension Server started on ws://localhost:9224");
// Start HTTP server for CDP discovery
this.httpServer = http.createServer((req, res) =>
this.handleHttpRequest(req, res),
);
// Start CDP server (port 9222 - CDP clients like Playwright)
this.cdpServer = new WebSocketServer({
server: this.httpServer,
perMessageDeflate: false,
});
this.cdpServer.on("connection", (ws, req) =>
this.handleCdpClient(ws, req),
);
await new Promise((resolve, reject) => {
this.httpServer.on("error", reject);
this.httpServer.listen(9222, "127.0.0.1", () => {
this.log("🎭 CDP Server started on ws://localhost:9222");
this.log(
"🌐 HTTP Server started on http://localhost:9222 (CDP discovery)",
);
resolve();
});
});
this.log("📡 Waiting for Chrome extension to connect...");
} catch (error) {
console.error("Failed to start servers:", error);
throw error;
}
}
handleHttpRequest(req, res) {
const pathname = url.parse(req.url).pathname;
// Enable CORS
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.setHeader("Content-Type", "application/json");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
if (pathname === "/json/version" || pathname === "/json/version/") {
res.writeHead(200);
res.end(JSON.stringify(this.browserInfo));
} else if (
pathname === "/json" ||
pathname === "/json/" ||
pathname === "/json/list" ||
pathname === "/json/list/"
) {
// Return Chrome-compatible target list
const tabs = [
{
description: "",
devtoolsFrontendUrl:
"/devtools/inspector.html?ws=localhost:9222/devtools/browser/brop-bridge-uuid-12345678",
id: "brop-bridge-uuid-12345678",
title: "Chrome",
type: "browser",
url: "",
webSocketDebuggerUrl:
"ws://localhost:9222/devtools/browser/brop-bridge-uuid-12345678",
},
];
res.writeHead(200);
res.end(JSON.stringify(tabs));
} else if (pathname === "/logs") {
// Return bridge server logs for debugging
const urlParams = new URLSearchParams(url.parse(req.url).query);
const limit = Number.parseInt(urlParams.get("limit")) || this.logs.length;
const logsToReturn = this.logs.slice(-limit);
const response = {
total: this.logs.length,
returned: logsToReturn.length,
logs: logsToReturn,
};
res.writeHead(200);
res.end(JSON.stringify(response, null, 2));
} else if (pathname === "/cdp-logs") {
// Return CDP traffic logs
const urlParams = new URLSearchParams(url.parse(req.url).query);
const limit = Number.parseInt(urlParams.get("limit")) || this.cdpLogs.length;
const format = urlParams.get("format") || "json";
const logsToReturn = this.cdpLogs.slice(-limit);
if (format === "jsonl") {
// Return as JSONL format for CDP traffic analyzer
res.setHeader("Content-Type", "application/x-ndjson");
res.writeHead(200);
const jsonlContent = logsToReturn.map(log => JSON.stringify({
direction: log.direction,
timestamp: log.timestamp,
cdp_data: log.data
})).join('\n');
res.end(jsonlContent);
} else {
// Return as JSON
const response = {
total: this.cdpLogs.length,
returned: logsToReturn.length,
logs: logsToReturn,
cdpLoggingEnabled: this.cdpLoggingEnabled
};
res.writeHead(200);
res.end(JSON.stringify(response, null, 2));
}
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
}
}
handleBropClient(ws, req) {
const connectionId = this.getNextConnectionId();
const queryParams = url.parse(req.url, true).query;
const clientName = queryParams.name || null;
const clientInfo = {
id: connectionId,
name: clientName,
connectedAt: Date.now(),
remoteAddress: req.socket.remoteAddress || "unknown",
};
this.bropConnections.set(ws, clientInfo);
const connectionDisplay = clientName
? `${connectionId}:${clientName}`
: connectionId;
this.logger.logConnect("BROP", connectionDisplay);
this.bropClients.add(ws);
ws.on("message", (message) => {
this.processBropMessage(ws, message.toString());
});
ws.on("close", () => {
this.logger.logDisconnect("BROP", connectionDisplay);
this.bropClients.delete(ws);
this.bropConnections.delete(ws);
});
ws.on("error", (error) => {
this.logger.logError(
"BROP",
"connection",
connectionDisplay,
error.message,
);
});
}
handleCdpClient(ws, req) {
const pathname = url.parse(req.url).pathname;
const clientId = `cdp_${++this.cdpClientCounter}`;
this.logger.logConnect("CDP", `${clientId}:${pathname}`);
// Parse connection type from pathname
let isMainBrowser = true;
let clientType = "browser";
let targetSessionId = null;
if (pathname.startsWith("/devtools/browser/")) {
isMainBrowser = true;
clientType = "browser";
} else if (pathname.startsWith("/devtools/page/")) {
const pageId = pathname.substring("/devtools/page/".length);
targetSessionId = pageId;
isMainBrowser = false;
clientType = "page";
} else if (pathname.startsWith("/session/")) {
targetSessionId = pathname.substring("/session/".length);
isMainBrowser = false;
clientType = "session";
} else {
isMainBrowser = this.cdpClients.size === 0;
}
const clientInfo = {
ws: ws,
sessionId: targetSessionId || `session_${clientId}`,
pathname: pathname,
connected: true,
targets: new Set(),
isMainBrowser: isMainBrowser,
isSessionConnection: !isMainBrowser,
targetSessionId: targetSessionId,
clientType: clientType,
created: Date.now(),
};
this.cdpClients.set(clientId, clientInfo);
ws.on("message", (message) => {
this.processCdpMessage(clientId, message.toString());
});
ws.on("close", () => {
this.logger.logDisconnect("CDP", `${clientId}:${pathname}`);
this.cleanupCdpClient(clientId);
});
ws.on("error", (error) => {
this.logger.logError(
"CDP",
"connection",
`${clientId}:${pathname}`,
error.message,
);
this.cleanupCdpClient(clientId);
});
}
cleanupCdpClient(clientId) {
const clientInfo = this.cdpClients.get(clientId);
if (clientInfo) {
clientInfo.connected = false;
// Clean up pending requests
for (const [
messageId,
requestInfo,
] of this.pendingCdpRequests.entries()) {
if (requestInfo.clientId === clientId) {
this.pendingCdpRequests.delete(messageId);
}
}
// Clean up session mappings
for (const targetId of clientInfo.targets) {
this.targetToClient.delete(targetId);
const sessionId = this.targetToSession.get(targetId);
if (sessionId) {
this.sessionChannels.delete(sessionId);
this.targetToSession.delete(targetId);
this.sessionToTarget.delete(sessionId);
}
}
this.cdpClients.delete(clientId);
}
}
handleExtensionClient(ws, req) {
this.logger.logConnect("EXT", "extension");
this.extensionClient = ws;
ws.send(
JSON.stringify({
type: "welcome",
message: "BROP Unified Bridge Server - Extension connected",
timestamp: Date.now(),
}),
);
ws.on("message", (message) => {
this.processExtensionMessage(message.toString());
});
ws.on("close", () => {
this.logger.logDisconnect("EXT", "extension");
this.extensionClient = null;
});
ws.on("error", (error) => {
this.logger.logError("EXT", "connection", "extension", error.message);
});
}
processBropMessage(client, message) {
try {
const data = JSON.parse(message);
const commandType = data.method || data.command?.type;
const messageId = data.id || this.getNextMessageId();
const clientInfo = this.bropConnections.get(client);
const connectionDisplay = clientInfo?.name
? `${clientInfo.id}:${clientInfo.name}`
: clientInfo?.id || "unknown";
if (
!this.extensionClient ||
this.extensionClient.readyState !== WebSocket.OPEN
) {
const errorResponse = {
id: messageId,
success: false,
error: "Chrome extension not connected",
};
client.send(JSON.stringify(errorResponse));
this.logger.logError(
"BROP",
commandType,
connectionDisplay,
"Extension not connected",
);
return;
}
// Add ID and type for extension processing
data.id = messageId;
data.type = "brop_command";
// Store client for response routing
this.pendingBropRequests.set(messageId, client);
// Store command info for response logging
this.pendingCommandInfo.set(messageId, {
command: commandType,
connection: connectionDisplay,
});
// Forward to extension
this.extensionClient.send(JSON.stringify(data));
} catch (error) {
this.logger.logError("BROP", "parse", "unknown", error.message);
}
}
processCdpMessage(clientId, message) {
try {
const data = JSON.parse(message);
const method = data.method;
const messageId = data.id;
const sessionId = data.sessionId;
this.logger.logSuccess("CDP", method, `${clientId}:${messageId}`);
// Log CDP request
if (this.cdpLoggingEnabled) {
this.logCdpMessage({
direction: 'client_to_server',
timestamp: new Date().toISOString(),
clientId: clientId,
messageId: messageId,
method: method,
sessionId: sessionId,
data: data,
type: 'request'
});
}
const clientInfo = this.cdpClients.get(clientId);
if (!clientInfo) {
this.logger.logError(
"CDP",
method,
`${clientId}:${messageId}`,
"Client not found",
);
return;
}
if (
!this.extensionClient ||
this.extensionClient.readyState !== WebSocket.OPEN
) {
const errorResponse = {
id: messageId,
error: { code: -32000, message: "Chrome extension not connected" },
};
// Add sessionId if present in the original request
if (sessionId) {
errorResponse.sessionId = sessionId;
}
clientInfo.ws.send(JSON.stringify(errorResponse));
this.logger.logError(
"CDP",
method,
`${clientId}:${messageId}`,
"Extension not connected",
);
return;
}
// Store request info for response routing
this.pendingCdpRequests.set(messageId, {
clientId: clientId,
originalClient: clientInfo.ws,
method: method,
sessionId: sessionId,
originalParams: data.params,
originalCommand: data,
});
// Track Target.createTarget commands for session management
if (method === "Target.createTarget") {
this.pendingTargetCreations = this.pendingTargetCreations || new Map();
this.pendingTargetCreations.set(messageId, clientId);
}
// Forward CDP command to extension (wrapped as BROP_CDP)
const extensionMessage = {
type: "BROP_CDP",
id: messageId,
method: method,
params: data.params || {},
sessionId: sessionId,
connectionId: clientId,
};
this.extensionClient.send(JSON.stringify(extensionMessage));
} catch (error) {
this.logger.logError("CDP", "parse", clientId, error.message);
}
}
processExtensionMessage(message) {
try {
const data = JSON.parse(message);
const messageType = data.type;
// Handle ping/pong keepalive
if (messageType === "ping") {
// Respond with pong
if (this.extensionClient && this.extensionClient.readyState === WebSocket.OPEN) {
this.extensionClient.send(JSON.stringify({
type: "pong",
timestamp: Date.now(),
originalTimestamp: data.timestamp
}));
}
return;
}
if (messageType === "response") {
const requestId = data.id;
// Handle BROP responses
if (this.pendingBropRequests.has(requestId)) {
const client = this.pendingBropRequests.get(requestId);
this.pendingBropRequests.delete(requestId);
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
// Log BROP command result with connection display using stored command info
const cmdInfo = this.pendingCommandInfo?.get(requestId);
if (cmdInfo) {
this.pendingCommandInfo.delete(requestId);
if (data.success) {
this.logger.logSuccess(
"BROP",
cmdInfo.command,
cmdInfo.connection,
);
} else {
this.logger.logError(
"BROP",
cmdInfo.command,
cmdInfo.connection,
data.error || "Unknown error",
);
}
}
}
return;
}
// Handle CDP responses
if (this.pendingCdpRequests.has(requestId)) {
const requestInfo = this.pendingCdpRequests.get(requestId);
this.pendingCdpRequests.delete(requestId);
// Handle Target.createTarget response - no longer generate attachedToTarget here
if (this.pendingTargetCreations?.has(requestId)) {
const clientId = this.pendingTargetCreations.get(requestId);
this.pendingTargetCreations.delete(requestId);
if (data.result?.targetId) {
const targetId = data.result.targetId;
this.targetToClient.set(targetId, clientId);
const clientInfo = this.cdpClients.get(clientId);
if (clientInfo) {
clientInfo.targets.add(targetId);
// Target.attachedToTarget is now generated by the extension
}
}
}
// Send CDP response back to client
if (requestInfo.originalClient.readyState === WebSocket.OPEN) {
const cdpResponse = {
id: requestId,
result: data.result,
error: data.error,
};
// Add sessionId if the original request had one
if (requestInfo.sessionId) {
cdpResponse.sessionId = requestInfo.sessionId;
}
// Log CDP response
if (this.cdpLoggingEnabled) {
this.logCdpMessage({
direction: 'server_to_client',
timestamp: new Date().toISOString(),
clientId: requestInfo.clientId,
messageId: requestId,
method: requestInfo.method,
sessionId: requestInfo.sessionId,
data: cdpResponse,
type: 'response'
});
}
requestInfo.originalClient.send(JSON.stringify(cdpResponse));
}
}
} else if (messageType === "cdp_event") {
// Extension sending a CDP event
// Special handling for Target.attachedToTarget to set up session mappings
if (data.method === "Target.attachedToTarget") {
const { sessionId, targetInfo } = data.params;
const targetId = targetInfo.targetId;
const connectionId = data.connectionId;
// Create session mapping
this.targetToSession.set(targetId, sessionId);
this.sessionToTarget.set(sessionId, targetId);
// Find the client for this connection
let clientId = null;
for (const [cid, client] of this.cdpClients) {
if (cid.includes(connectionId)) {
clientId = cid;
break;
}
}
if (clientId) {
this.sessionChannels.set(sessionId, {
clientId: clientId,
targetId: targetId,
created: Date.now(),
});
const clientInfo = this.cdpClients.get(clientId);
if (clientInfo) {
clientInfo.targets.add(targetId);
}
}
console.log(`🎭 Created session mapping: ${sessionId} -> ${targetId}`);
}
// Route all CDP events (including Target.attachedToTarget)
this.routeCdpEvent(data);
}
} catch (error) {
this.logger.logError("EXT", "parse", "extension", error.message);
}
}
// No longer needed - Target.attachedToTarget is generated by the extension
// sendTargetAttachedEvent(clientId, targetId) {
// const sessionId = this.generateSessionId();
//
// // Create session mapping
// this.targetToSession.set(targetId, sessionId);
// this.sessionToTarget.set(sessionId, targetId);
// this.sessionChannels.set(sessionId, {
// clientId: clientId,
// targetId: targetId,
// created: Date.now(),
// });
//
// const clientInfo = this.cdpClients.get(clientId);
// if (clientInfo && clientInfo.ws.readyState === WebSocket.OPEN) {
// const attachedEvent = {
// method: "Target.attachedToTarget",
// params: {
// sessionId: sessionId,
// targetInfo: {
// targetId: targetId,
// type: "page",
// title: "",
// url: "about:blank",
// attached: true,
// canAccessOpener: false,
// browserContextId: this.defaultBrowserContextId,
// },
// waitingForDebugger: true,
// },
// };
//
// clientInfo.ws.send(JSON.stringify(attachedEvent));
// this.logger.logSuccess(
// "CDP",
// "event:Target.attachedToTarget",
// `${clientId}:${targetId}`,
// );
// }
// }
routeCdpEvent(eventData) {
const method = eventData.method;
const params = eventData.params;
const tabId = eventData.tabId;
const targetId = eventData.targetId;
// Log the event for debugging
this.logger.logSuccess("CDP", `event:${method}`, `target_${targetId || 'unknown'}`);
const cdpEventMessage = {
method: method,
params: params,
};
// Look up sessionId based on targetId mapping
if (targetId && !method.startsWith("Target.")) {
const sessionId = this.targetToSession.get(targetId);
if (sessionId) {
cdpEventMessage.sessionId = sessionId;
this.logger.logSuccess("CDP", `mapped target ${targetId} to session ${sessionId}`, "");
}
}
const messageStr = JSON.stringify(cdpEventMessage);
// Route to appropriate CDP client(s)
if (method.startsWith("Target.")) {
// Browser-level events go to main browser client
const mainClient = this.getMainBrowserClient();
if (mainClient) {
mainClient.ws.send(messageStr);
// Log CDP event
if (this.cdpLoggingEnabled) {
this.logCdpMessage({
direction: 'server_to_client',
timestamp: new Date().toISOString(),
clientId: 'main',
messageId: null,
method: method,
sessionId: cdpEventMessage.sessionId,
data: cdpEventMessage,
type: 'event'
});
}
}
} else if (targetId) {
// Target-specific events route to session client
const sessionClient = this.getSessionClientForTarget(targetId);
if (sessionClient) {
sessionClient.ws.send(messageStr);
// Log CDP event
if (this.cdpLoggingEnabled) {
this.logCdpMessage({
direction: 'server_to_client',
timestamp: new Date().toISOString(),
clientId: sessionClient.clientId || 'session',
messageId: null,
method: method,
sessionId: cdpEventMessage.sessionId,
data: cdpEventMessage,
type: 'event'
});
}
} else {
// Fallback to main client
const mainClient = this.getMainBrowserClient();
if (mainClient) {
mainClient.ws.send(messageStr);
// Log CDP event fallback
if (this.cdpLoggingEnabled) {
this.logCdpMessage({
direction: 'server_to_client',
timestamp: new Date().toISOString(),
clientId: 'main_fallback',
messageId: null,
method: method,
sessionId: cdpEventMessage.sessionId,
data: cdpEventMessage,
type: 'event'
});
}
}
}
}
}
getMainBrowserClient() {
for (const [clientId, clientInfo] of this.cdpClients) {
if (
clientInfo.isMainBrowser &&
clientInfo.connected &&
clientInfo.ws.readyState === WebSocket.OPEN
) {
return clientInfo;
}
}
return null;
}
getSessionClientForTarget(targetId) {
const sessionId = this.targetToSession.get(targetId);
if (sessionId) {
const sessionInfo = this.sessionChannels.get(sessionId);
if (sessionInfo) {
const clientInfo = this.cdpClients.get(sessionInfo.clientId);
if (
clientInfo?.connected &&
clientInfo.ws.readyState === WebSocket.OPEN
) {
return clientInfo;
}
}
}
return null;
}
generateSessionId() {
return Array.from({ length: 32 }, () =>
Math.floor(Math.random() * 16)
.toString(16)
.toUpperCase(),
).join("");
}
generateBrowserContextId() {
// Generate browser context ID in same format as native Chrome (32 char uppercase hex)
return Array.from({ length: 32 }, () =>
Math.floor(Math.random() * 16)
.toString(16)
.toUpperCase(),
).join("");
}
logCdpMessage(logEntry) {
// Add CDP message to log
this.cdpLogs.push(logEntry);
// Keep only the last maxCdpLogs entries
if (this.cdpLogs.length > this.maxCdpLogs) {
this.cdpLogs.splice(0, this.cdpLogs.length - this.maxCdpLogs);
}
}
async shutdown() {
this.log("🛑 Shutting down unified bridge server...");
this.running = false;
if (this.bropServer) this.bropServer.close();
if (this.extensionServer) this.extensionServer.close();
if (this.cdpServer) this.cdpServer.close();
if (this.httpServer) this.httpServer.close();
}
}
// Main function
async function main() {
console.log("🌉 BROP Unified Bridge Server");
console.log("=".repeat(50));
console.log("🔧 BROP Port: 9225 (BROP clients)");
console.log("🔌 Extension Port: 9224 (extension connects here)");
console.log("🎭 CDP Port: 9222 (Playwright/CDP clients)");
console.log("🌐 NO REAL CHROME DEPENDENCY");
console.log("");
const bridge = new UnifiedBridgeServer();
// Setup signal handlers
process.on("SIGINT", () => {
console.log("🛑 Received SIGINT");
bridge.shutdown().then(() => process.exit(0));
});
process.on("SIGTERM", () => {
console.log("🛑 Received SIGTERM");
bridge.shutdown().then(() => process.exit(0));
});
try {
await bridge.startServers();
} catch (error) {
console.error("💥 Server error:", error);
process.exit(1);
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { UnifiedBridgeServer };