fixed windows paths and added host + port autodiscovery

This commit is contained in:
Ted Werbel 2025-03-02 03:10:38 -05:00
parent 60248f9fca
commit bc4629db97
8 changed files with 1494 additions and 330 deletions

View File

@ -4,16 +4,20 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import path from "path";
import fs from "fs";
import os from "os";
// Create the MCP server
const server = new McpServer({
name: "Browser Tools MCP",
version: "1.0.9",
version: "1.1.1",
});
// Function to get the port from environment variable or default
function getServerPort(): number {
// Track the discovered server connection
let discoveredHost = "127.0.0.1";
let discoveredPort = 3025;
let serverDiscovered = false;
// Function to get the default port from environment variable or default
function getDefaultServerPort(): number {
// Check environment variable first
if (process.env.BROWSER_TOOLS_PORT) {
const envPort = parseInt(process.env.BROWSER_TOOLS_PORT, 10);
@ -39,8 +43,8 @@ function getServerPort(): number {
return 3025;
}
// Function to get server host from environment variable or default
function getServerHost(): string {
// Function to get default server host from environment variable or default
function getDefaultServerHost(): string {
// Check environment variable first
if (process.env.BROWSER_TOOLS_HOST) {
return process.env.BROWSER_TOOLS_HOST;
@ -50,147 +54,188 @@ function getServerHost(): string {
return "127.0.0.1";
}
const PORT = getServerPort();
const HOST = getServerHost();
// Server discovery function - similar to what you have in the Chrome extension
async function discoverServer(): Promise<boolean> {
console.log("Starting server discovery process");
// We'll define four "tools" that retrieve data from the aggregator at localhost:3000
// Common hosts to try
const hosts = [getDefaultServerHost(), "127.0.0.1", "localhost"];
// Ports to try (start with default, then try others)
const defaultPort = getDefaultServerPort();
const ports = [defaultPort];
// Add additional ports (fallback range)
for (let p = 3025; p <= 3035; p++) {
if (p !== defaultPort) {
ports.push(p);
}
}
console.log(`Will try hosts: ${hosts.join(", ")}`);
console.log(`Will try ports: ${ports.join(", ")}`);
// Try to find the server
for (const host of hosts) {
for (const port of ports) {
try {
console.log(`Checking ${host}:${port}...`);
// Use the identity endpoint for validation
const response = await fetch(`http://${host}:${port}/.identity`, {
signal: AbortSignal.timeout(1000), // 1 second timeout
});
if (response.ok) {
const identity = await response.json();
// Verify this is actually our server by checking the signature
if (identity.signature === "mcp-browser-connector-24x7") {
console.log(`Successfully found server at ${host}:${port}`);
// Save the discovered connection
discoveredHost = host;
discoveredPort = port;
serverDiscovered = true;
return true;
}
}
} catch (error: any) {
// Ignore connection errors during discovery
console.error(`Error checking ${host}:${port}: ${error.message}`);
}
}
}
console.error("No server found during discovery");
return false;
}
// Wrapper function to ensure server connection before making requests
async function withServerConnection<T>(
apiCall: () => Promise<T>
): Promise<T | any> {
// Attempt to discover server if not already discovered
if (!serverDiscovered) {
const discovered = await discoverServer();
if (!discovered) {
return {
content: [
{
type: "text",
text: "Failed to discover browser connector server. Please ensure it's running.",
},
],
isError: true,
};
}
}
// Now make the actual API call with discovered host/port
try {
return await apiCall();
} catch (error: any) {
// If the request fails, try rediscovering the server once
console.error(
`API call failed: ${error.message}. Attempting rediscovery...`
);
serverDiscovered = false;
if (await discoverServer()) {
console.error("Rediscovery successful. Retrying API call...");
try {
// Retry the API call with the newly discovered connection
return await apiCall();
} catch (retryError: any) {
console.error(`Retry failed: ${retryError.message}`);
return {
content: [
{
type: "text",
text: `Error after reconnection attempt: ${retryError.message}`,
},
],
isError: true,
};
}
} else {
console.error("Rediscovery failed. Could not reconnect to server.");
return {
content: [
{
type: "text",
text: `Failed to reconnect to server: ${error.message}`,
},
],
isError: true,
};
}
}
}
// We'll define our tools that retrieve data from the browser connector
server.tool("getConsoleLogs", "Check our browser logs", async () => {
const response = await fetch(`http://${HOST}:${PORT}/console-logs`);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
return await withServerConnection(async () => {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/console-logs`
);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
});
});
server.tool(
"getConsoleErrors",
"Check our browsers console errors",
async () => {
const response = await fetch(`http://${HOST}:${PORT}/console-errors`);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
}
);
// Return all HTTP errors (4xx/5xx)
server.tool("getNetworkErrorLogs", "Check our network ERROR logs", async () => {
const response = await fetch(`http://${HOST}:${PORT}/network-errors`);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
});
// // Return all XHR/fetch requests
// // DEPRECATED: Use getNetworkSuccessLogs and getNetworkErrorLogs instead
// 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 network success logs
server.tool(
"getNetworkSuccessLogs",
"Check our network SUCCESS logs",
async () => {
const response = await fetch(`http://${HOST}:${PORT}/network-success`);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
}
);
// Add new tool for taking screenshots
server.tool(
"takeScreenshot",
"Take a screenshot of the current browser tab",
async () => {
try {
return await withServerConnection(async () => {
const response = await fetch(
`http://${HOST}:${PORT}/capture-screenshot`,
{
method: "POST",
}
`http://${discoveredHost}:${discoveredPort}/console-errors`
);
const result = await response.json();
if (response.ok) {
// Removed path due to bug... will change later anyways
// const message = `Screenshot saved to: ${
// result.path
// }\nFilename: ${path.basename(result.path)}`;
return {
content: [
{
type: "text",
text: "Successfully saved screenshot",
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Error taking screenshot: ${result.error}`,
},
],
};
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const json = await response.json();
return {
content: [
{
type: "text",
text: `Failed to take screenshot: ${errorMessage}`,
text: JSON.stringify(json, null, 2),
},
],
};
}
});
}
);
// Add new tool for getting selected element
server.tool(
"getSelectedElement",
"Get the selected element from the browser",
async () => {
const response = await fetch(`http://${HOST}:${PORT}/selected-element`);
server.tool("getNetworkErrors", "Check our network ERROR logs", async () => {
return await withServerConnection(async () => {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/network-errors`
);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
isError: true,
};
});
});
server.tool("getNetworkLogs", "Check ALL our network logs", async () => {
return await withServerConnection(async () => {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/network-success`
);
const json = await response.json();
return {
content: [
@ -200,28 +245,116 @@ server.tool(
},
],
};
});
});
server.tool(
"takeScreenshot",
"Take a screenshot of the current browser tab",
async () => {
return await withServerConnection(async () => {
try {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/capture-screenshot`,
{
method: "POST",
}
);
const result = await response.json();
if (response.ok) {
return {
content: [
{
type: "text",
text: "Successfully saved screenshot",
},
],
};
} else {
return {
content: [
{
type: "text",
text: `Error taking screenshot: ${result.error}`,
},
],
};
}
} catch (error: any) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Failed to take screenshot: ${errorMessage}`,
},
],
};
}
});
}
);
server.tool(
"getSelectedElement",
"Get the selected element from the browser",
async () => {
return await withServerConnection(async () => {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/selected-element`
);
const json = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(json, null, 2),
},
],
};
});
}
);
// Add new tool for wiping logs
server.tool("wipeLogs", "Wipe all browser logs from memory", async () => {
const response = await fetch(`http://${HOST}:${PORT}/wipelogs`, {
method: "POST",
});
const json = await response.json();
return {
content: [
return await withServerConnection(async () => {
const response = await fetch(
`http://${discoveredHost}:${discoveredPort}/wipelogs`,
{
type: "text",
text: json.message,
},
],
};
method: "POST",
}
);
const json = await response.json();
return {
content: [
{
type: "text",
text: json.message,
},
],
};
});
});
// Start receiving messages on stdio
(async () => {
try {
// Attempt initial server discovery
console.error("Attempting initial server discovery on startup...");
await discoverServer();
if (serverDiscovered) {
console.error(
`Successfully discovered server at ${discoveredHost}:${discoveredPort}`
);
} else {
console.error(
"Initial server discovery failed. Will try again when tools are used."
);
}
const transport = new StdioServerTransport();
// Ensure stdout is only used for JSON messages

View File

@ -1,6 +1,6 @@
{
"name": "@agentdeskai/browser-tools-mcp",
"version": "1.1.0",
"version": "1.1.1",
"description": "MCP (Model Context Protocol) server for browser tools integration",
"main": "dist/mcp-server.js",
"bin": {

View File

@ -161,9 +161,68 @@ interface ScreenshotCallback {
const screenshotCallbacks = new Map<string, ScreenshotCallback>();
const app = express();
const PORT = parseInt(process.env.PORT || "3025", 10);
// Function to get available port starting with the given port
async function getAvailablePort(
startPort: number,
maxAttempts: number = 10
): Promise<number> {
let currentPort = startPort;
let attempts = 0;
while (attempts < maxAttempts) {
try {
// Try to create a server on the current port
// We'll use a raw Node.js net server for just testing port availability
await new Promise<void>((resolve, reject) => {
const testServer = require("net").createServer();
// Handle errors (e.g., port in use)
testServer.once("error", (err: any) => {
if (err.code === "EADDRINUSE") {
console.log(`Port ${currentPort} is in use, trying next port...`);
currentPort++;
attempts++;
resolve(); // Continue to next iteration
} else {
reject(err); // Different error, propagate it
}
});
// If we can listen, the port is available
testServer.once("listening", () => {
// Make sure to close the server to release the port
testServer.close(() => {
console.log(`Found available port: ${currentPort}`);
resolve();
});
});
// Try to listen on the current port
testServer.listen(currentPort, currentSettings.serverHost);
});
// If we reach here without incrementing the port, it means the port is available
return currentPort;
} catch (error: any) {
console.error(`Error checking port ${currentPort}:`, error);
// For non-EADDRINUSE errors, try the next port
currentPort++;
attempts++;
}
}
// If we've exhausted all attempts, throw an error
throw new Error(
`Could not find an available port after ${maxAttempts} attempts starting from ${startPort}`
);
}
// Start with requested port and find an available one
const REQUESTED_PORT = parseInt(process.env.PORT || "3025", 10);
let PORT = REQUESTED_PORT;
// Create application and initialize middleware
const app = express();
app.use(cors());
// Increase JSON body parser limit to 50MB to handle large screenshots
app.use(bodyParser.json({ limit: "50mb" }));
@ -824,38 +883,88 @@ export class BrowserConnector {
}
}
// Move the server creation before BrowserConnector instantiation
const server = app.listen(PORT, currentSettings.serverHost, () => {
console.log(`\n=== Browser Tools Server Started ===`);
console.log(
`Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
);
// Use an async IIFE to allow for async/await in the initial setup
(async () => {
try {
console.log(`Starting Browser Tools Server...`);
console.log(`Requested port: ${REQUESTED_PORT}`);
// Log all available network interfaces for easier discovery
const networkInterfaces = os.networkInterfaces();
console.log("\nAvailable on the following network addresses:");
// Find an available port
try {
PORT = await getAvailablePort(REQUESTED_PORT);
Object.keys(networkInterfaces).forEach((interfaceName) => {
const interfaces = networkInterfaces[interfaceName];
if (interfaces) {
interfaces.forEach((iface) => {
if (!iface.internal && iface.family === "IPv4") {
console.log(` - http://${iface.address}:${PORT}`);
if (PORT !== REQUESTED_PORT) {
console.log(`\n====================================`);
console.log(`NOTICE: Requested port ${REQUESTED_PORT} was in use.`);
console.log(`Using port ${PORT} instead.`);
console.log(`====================================\n`);
}
} catch (portError) {
console.error(`Failed to find an available port:`, portError);
process.exit(1);
}
// Create the server with the available port
const server = app.listen(PORT, currentSettings.serverHost, () => {
console.log(`\n=== Browser Tools Server Started ===`);
console.log(
`Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
);
if (PORT !== REQUESTED_PORT) {
console.log(
`NOTE: Using fallback port ${PORT} instead of requested port ${REQUESTED_PORT}`
);
}
// Log all available network interfaces for easier discovery
const networkInterfaces = os.networkInterfaces();
console.log("\nAvailable on the following network addresses:");
Object.keys(networkInterfaces).forEach((interfaceName) => {
const interfaces = networkInterfaces[interfaceName];
if (interfaces) {
interfaces.forEach((iface) => {
if (!iface.internal && iface.family === "IPv4") {
console.log(` - http://${iface.address}:${PORT}`);
}
});
}
});
}
});
console.log(`\nFor local access use: http://localhost:${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);
});
console.log(`\nFor local access use: http://localhost:${PORT}`);
});
// Handle server startup errors
server.on("error", (err: any) => {
if (err.code === "EADDRINUSE") {
console.error(
`ERROR: Port ${PORT} is still in use, despite our checks!`
);
console.error(
`This might indicate another process started using this port after our check.`
);
} else {
console.error(`Server error:`, err);
}
process.exit(1);
});
// 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);
});
});
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
})().catch((err) => {
console.error("Unhandled error during server startup:", err);
process.exit(1);
});

View File

@ -1,6 +1,6 @@
{
"name": "@agentdeskai/browser-tools-server",
"version": "1.1.0",
"version": "1.1.1",
"description": "A browser tools server for capturing and managing browser events, logs, and screenshots",
"main": "dist/browser-connector.js",
"bin": {

View File

@ -65,6 +65,57 @@ async function validateServerIdentity(host, port) {
}
}
// Listen for tab updates to detect page refreshes
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// Check if this is a page refresh (status becoming "complete")
if (changeInfo.status === "complete") {
retestConnectionOnRefresh(tabId);
}
});
// Function to retest connection when a page is refreshed
async function retestConnectionOnRefresh(tabId) {
console.log(`Page refreshed in tab ${tabId}, retesting connection...`);
// Get the saved settings
chrome.storage.local.get(["browserConnectorSettings"], async (result) => {
const settings = result.browserConnectorSettings || {
serverHost: "localhost",
serverPort: 3025,
};
// Test the connection with the last known host and port
const isConnected = await validateServerIdentity(
settings.serverHost,
settings.serverPort
);
// Notify all devtools instances about the connection status
chrome.runtime.sendMessage({
type: "CONNECTION_STATUS_UPDATE",
isConnected: isConnected,
tabId: tabId,
});
// Always notify for page refresh, whether connected or not
// This ensures any ongoing discovery is cancelled and restarted
chrome.runtime.sendMessage({
type: "INITIATE_AUTO_DISCOVERY",
reason: "page_refresh",
tabId: tabId,
forceRestart: true, // Add a flag to indicate this should force restart any ongoing processes
});
if (!isConnected) {
console.log(
"Connection test failed after page refresh, initiating auto-discovery..."
);
} else {
console.log("Connection test successful after page refresh");
}
});
}
// Function to capture and send screenshot
function captureAndSendScreenshot(message, settings, sendResponse) {
// Get the inspected window's tab

View File

@ -42,6 +42,71 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
setupWebSocket();
}
}
// Handle connection status updates from page refreshes
if (message.type === "CONNECTION_STATUS_UPDATE") {
console.log(
`DevTools received connection status update: ${
message.isConnected ? "Connected" : "Disconnected"
}`
);
// If connection is lost, try to reestablish WebSocket only if we had a previous connection
if (!message.isConnected && ws) {
console.log(
"Connection lost after page refresh, will attempt to reconnect WebSocket"
);
// Only reconnect if we actually have a WebSocket that might be stale
if (
ws &&
(ws.readyState === WebSocket.CLOSED ||
ws.readyState === WebSocket.CLOSING)
) {
console.log("WebSocket is already closed or closing, will reconnect");
setupWebSocket();
}
}
}
// Handle auto-discovery requests after page refreshes
if (message.type === "INITIATE_AUTO_DISCOVERY") {
console.log(
`DevTools initiating WebSocket reconnect after page refresh (reason: ${message.reason})`
);
// For page refreshes with forceRestart, we should always reconnect if our current connection is not working
if (
(message.reason === "page_refresh" || message.forceRestart === true) &&
(!ws || ws.readyState !== WebSocket.OPEN)
) {
console.log(
"Page refreshed and WebSocket not open - forcing reconnection"
);
// Close existing WebSocket if any
if (ws) {
console.log("Closing existing WebSocket due to page refresh");
intentionalClosure = true; // Mark as intentional to prevent auto-reconnect
try {
ws.close();
} catch (e) {
console.error("Error closing WebSocket:", e);
}
ws = null;
intentionalClosure = false; // Reset flag
}
// Clear any pending reconnect timeouts
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
// Try to reestablish the WebSocket connection
setupWebSocket();
}
}
});
// Utility to recursively truncate strings in any data structure
@ -284,33 +349,80 @@ async function sendToBrowserConnector(logData) {
});
}
// Validate server identity before connecting
// Validate server identity
async function validateServerIdentity() {
try {
const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/.identity`;
console.log(
`Validating server identity at ${settings.serverHost}:${settings.serverPort}...`
);
// Check if the server is our browser-tools-server
const response = await fetch(serverUrl, {
signal: AbortSignal.timeout(2000), // 2 second timeout
});
// Use fetch with a timeout to prevent long-hanging requests
const response = await fetch(
`http://${settings.serverHost}:${settings.serverPort}/.identity`,
{
signal: AbortSignal.timeout(3000), // 3 second timeout
}
);
if (!response.ok) {
console.error(`Invalid server response: ${response.status}`);
console.error(
`Server identity validation failed: HTTP ${response.status}`
);
// Notify about the connection failure
chrome.runtime.sendMessage({
type: "SERVER_VALIDATION_FAILED",
reason: "http_error",
status: response.status,
serverHost: settings.serverHost,
serverPort: settings.serverPort,
});
return false;
}
const identity = await response.json();
// Validate the server signature
// Validate signature
if (identity.signature !== "mcp-browser-connector-24x7") {
console.error("Invalid server signature - not the browser tools server");
console.error("Server identity validation failed: Invalid signature");
// Notify about the invalid signature
chrome.runtime.sendMessage({
type: "SERVER_VALIDATION_FAILED",
reason: "invalid_signature",
serverHost: settings.serverHost,
serverPort: settings.serverPort,
});
return false;
}
// If reached here, the server is valid
console.log(
`Server identity confirmed: ${identity.name} v${identity.version}`
);
// Notify about successful validation
chrome.runtime.sendMessage({
type: "SERVER_VALIDATION_SUCCESS",
serverInfo: identity,
serverHost: settings.serverHost,
serverPort: settings.serverPort,
});
return true;
} catch (error) {
console.error("Error validating server identity:", error);
console.error("Server identity validation failed:", error);
// Notify about the connection error
chrome.runtime.sendMessage({
type: "SERVER_VALIDATION_FAILED",
reason: "connection_error",
error: error.message,
serverHost: settings.serverHost,
serverPort: settings.serverPort,
});
return false;
}
}
@ -529,14 +641,31 @@ chrome.devtools.panels.create("BrowserToolsMCP", "", "panel.html", (panel) => {
});
});
// Clean up when DevTools window is closed
// Clean up when DevTools closes
window.addEventListener("unload", () => {
// Detach debugger
detachDebugger();
// Set intentional closure flag before closing
intentionalClosure = true;
if (ws) {
ws.close();
try {
ws.close();
} catch (e) {
console.error("Error closing WebSocket during unload:", e);
}
ws = null;
}
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
});
@ -590,97 +719,197 @@ chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
// WebSocket connection management
let ws = null;
let wsReconnectTimeout = null;
let heartbeatInterval = null;
const WS_RECONNECT_DELAY = 5000; // 5 seconds
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
// Add a flag to track if we need to reconnect after identity validation
let reconnectAfterValidation = false;
// Track if we're intentionally closing the connection
let intentionalClosure = false;
// Function to send a heartbeat to keep the WebSocket connection alive
function sendHeartbeat() {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("Chrome Extension: Sending WebSocket heartbeat");
ws.send(JSON.stringify({ type: "heartbeat" }));
}
}
async function setupWebSocket() {
// Clear any pending timeouts
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
// Close existing WebSocket if any
if (ws) {
ws.close();
// Set flag to indicate this is an intentional closure
intentionalClosure = true;
try {
ws.close();
} catch (e) {
console.error("Error closing existing WebSocket:", e);
}
ws = null;
intentionalClosure = false; // Reset flag
}
// Validate server identity before connecting
if (!(await validateServerIdentity())) {
console.log("Validating server identity before WebSocket connection...");
const isValid = await validateServerIdentity();
if (!isValid) {
console.error(
"Cannot establish WebSocket: Not connected to a valid browser tools server"
);
// Set flag to indicate we need to reconnect after a page refresh check
reconnectAfterValidation = true;
// Try again after delay
setTimeout(setupWebSocket, WS_RECONNECT_DELAY);
wsReconnectTimeout = setTimeout(() => {
console.log("Attempting to reconnect WebSocket after validation failure");
setupWebSocket();
}, WS_RECONNECT_DELAY);
return;
}
// Reset reconnect flag since validation succeeded
reconnectAfterValidation = false;
const wsUrl = `ws://${settings.serverHost}:${settings.serverPort}/extension-ws`;
console.log(`Connecting to WebSocket at ${wsUrl}`);
ws = new WebSocket(wsUrl);
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log(`Chrome Extension: WebSocket connected to ${wsUrl}`);
};
ws.onopen = () => {
console.log(`Chrome Extension: WebSocket connected to ${wsUrl}`);
ws.onerror = (error) => {
console.error(`Chrome Extension: WebSocket error for ${wsUrl}:`, error);
};
// Start heartbeat to keep connection alive
heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
ws.onclose = (event) => {
console.log(`Chrome Extension: WebSocket closed for ${wsUrl}:`, event);
// Notify that connection is successful
chrome.runtime.sendMessage({
type: "WEBSOCKET_CONNECTED",
serverHost: settings.serverHost,
serverPort: settings.serverPort,
});
};
// Try to reconnect after delay
setTimeout(() => {
console.log(
`Chrome Extension: Attempting to reconnect WebSocket to ${wsUrl}`
);
setupWebSocket();
}, WS_RECONNECT_DELAY);
};
ws.onerror = (error) => {
console.error(`Chrome Extension: WebSocket error for ${wsUrl}:`, error);
};
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log("Chrome Extension: Received WebSocket message:", message);
ws.onclose = (event) => {
console.log(`Chrome Extension: WebSocket closed for ${wsUrl}:`, event);
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));
});
// Stop heartbeat
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
} catch (error) {
console.error(
"Chrome Extension: Error processing WebSocket message:",
error
);
}
};
// Don't reconnect if this was an intentional closure
if (intentionalClosure) {
console.log(
"Chrome Extension: Intentional WebSocket closure, not reconnecting"
);
return;
}
// Only attempt to reconnect if the closure wasn't intentional
// Code 1000 (Normal Closure) and 1001 (Going Away) are normal closures
// Code 1005 often happens with clean closures in Chrome
const isAbnormalClosure = !(event.code === 1000 || event.code === 1001);
// Check if this was an abnormal closure or if we need to reconnect after validation
if (isAbnormalClosure || reconnectAfterValidation) {
console.log(
`Chrome Extension: Will attempt to reconnect WebSocket (closure code: ${event.code})`
);
// Try to reconnect after delay
wsReconnectTimeout = setTimeout(() => {
console.log(
`Chrome Extension: Attempting to reconnect WebSocket to ${wsUrl}`
);
setupWebSocket();
}, WS_RECONNECT_DELAY);
} else {
console.log(
`Chrome Extension: Normal WebSocket closure, not reconnecting automatically`
);
}
};
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
// Don't log heartbeat responses to reduce noise
if (message.type !== "heartbeat-response") {
console.log("Chrome Extension: Received WebSocket message:", message);
}
if (message.type === "heartbeat-response") {
// Just a heartbeat response, no action needed
// Uncomment the next line for debug purposes only
// console.log("Chrome Extension: Received heartbeat response");
} else 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
);
}
};
} catch (error) {
console.error("Error creating WebSocket:", error);
// Try again after delay
wsReconnectTimeout = setTimeout(setupWebSocket, WS_RECONNECT_DELAY);
}
}
// Initialize WebSocket connection when DevTools opens

View File

@ -1,6 +1,6 @@
{
"name": "BrowserTools MCP",
"version": "1.1.0",
"version": "1.1.1",
"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",

View File

@ -12,14 +12,297 @@ let settings = {
serverPort: 3025,
};
// Track connection status
let serverConnected = false;
let reconnectAttemptTimeout = null;
// Add a flag to track ongoing discovery operations
let isDiscoveryInProgress = false;
// Add an AbortController to cancel fetch operations
let discoveryController = null;
// Load saved settings on startup
chrome.storage.local.get(["browserConnectorSettings"], (result) => {
if (result.browserConnectorSettings) {
settings = { ...settings, ...result.browserConnectorSettings };
updateUIFromSettings();
}
// Create connection status banner at the top
createConnectionBanner();
// Automatically discover server on panel load with quiet mode enabled
discoverServer(true);
});
// Add listener for connection status updates from background script (page refresh events)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "CONNECTION_STATUS_UPDATE") {
console.log(
`Received connection status update: ${
message.isConnected ? "Connected" : "Disconnected"
}`
);
// Update UI based on connection status
if (message.isConnected) {
// If already connected, just maintain the current state
if (!serverConnected) {
// Connection was re-established, update UI
serverConnected = true;
updateConnectionBanner(true, {
name: "Browser Tools Server",
version: "reconnected",
host: settings.serverHost,
port: settings.serverPort,
});
}
} else {
// Connection lost, update UI to show disconnected
serverConnected = false;
updateConnectionBanner(false, null);
}
}
if (message.type === "INITIATE_AUTO_DISCOVERY") {
console.log(
`Initiating auto-discovery after page refresh (reason: ${message.reason})`
);
// For page refreshes or if forceRestart is set to true, always cancel any ongoing discovery and restart
if (message.reason === "page_refresh" || message.forceRestart === true) {
// Cancel any ongoing discovery operation
cancelOngoingDiscovery();
// Update UI to indicate we're starting a fresh scan
if (connectionStatusDiv) {
connectionStatusDiv.style.display = "block";
if (statusIcon) statusIcon.className = "status-indicator";
if (statusText)
statusText.textContent =
"Page refreshed. Restarting server discovery...";
}
// Always update the connection banner when a page refresh occurs
updateConnectionBanner(false, null);
// Start a new discovery process with quiet mode
console.log("Starting fresh discovery after page refresh");
discoverServer(true);
}
// For other types of auto-discovery requests, only start if not already in progress
else if (!isDiscoveryInProgress) {
// Use quiet mode for auto-discovery to minimize UI changes
discoverServer(true);
}
}
// Handle successful server validation
if (message.type === "SERVER_VALIDATION_SUCCESS") {
console.log(
`Server validation successful: ${message.serverHost}:${message.serverPort}`
);
// Update the connection status banner
serverConnected = true;
updateConnectionBanner(true, message.serverInfo);
// If we were showing the connection status dialog, we can hide it now
if (connectionStatusDiv && connectionStatusDiv.style.display === "block") {
connectionStatusDiv.style.display = "none";
}
}
// Handle failed server validation
if (message.type === "SERVER_VALIDATION_FAILED") {
console.log(
`Server validation failed: ${message.reason} - ${message.serverHost}:${message.serverPort}`
);
// Update the connection status
serverConnected = false;
updateConnectionBanner(false, null);
// Start auto-discovery if this was a page refresh validation
if (
message.reason === "connection_error" ||
message.reason === "http_error"
) {
// If we're not already trying to discover the server, start the process
if (!isDiscoveryInProgress) {
console.log("Starting auto-discovery after validation failure");
discoverServer(true);
}
}
}
// Handle successful WebSocket connection
if (message.type === "WEBSOCKET_CONNECTED") {
console.log(
`WebSocket connected to ${message.serverHost}:${message.serverPort}`
);
// Update connection status if it wasn't already connected
if (!serverConnected) {
serverConnected = true;
updateConnectionBanner(true, {
name: "Browser Tools Server",
version: "connected via WebSocket",
host: message.serverHost,
port: message.serverPort,
});
}
}
});
// Create connection status banner
function createConnectionBanner() {
// Check if banner already exists
if (document.getElementById("connection-banner")) {
return;
}
// Create the banner
const banner = document.createElement("div");
banner.id = "connection-banner";
banner.style.cssText = `
padding: 6px 0px;
margin-bottom: 4px;
width: 40%;
display: flex;
flex-direction: column;
align-items: flex-start;
background-color:rgba(0,0,0,0);
border-radius: 11px;
font-size: 11px;
font-weight: 500;
color: #ffffff;
`;
// Create reconnect button (now placed at the top)
const reconnectButton = document.createElement("button");
reconnectButton.id = "banner-reconnect-btn";
reconnectButton.textContent = "Reconnect";
reconnectButton.style.cssText = `
background-color: #333333;
color: #ffffff;
border: 1px solid #444444;
border-radius: 3px;
padding: 2px 8px;
font-size: 10px;
cursor: pointer;
margin-bottom: 6px;
align-self: flex-start;
display: none;
transition: background-color 0.2s;
`;
reconnectButton.addEventListener("mouseover", () => {
reconnectButton.style.backgroundColor = "#444444";
});
reconnectButton.addEventListener("mouseout", () => {
reconnectButton.style.backgroundColor = "#333333";
});
reconnectButton.addEventListener("click", () => {
// Hide the button while reconnecting
reconnectButton.style.display = "none";
reconnectButton.textContent = "Reconnecting...";
// Update UI to show searching state
updateConnectionBanner(false, null);
// Try to discover server
discoverServer(false);
});
// Create a container for the status indicator and text
const statusContainer = document.createElement("div");
statusContainer.style.cssText = `
display: flex;
align-items: center;
width: 100%;
`;
// Create status indicator
const indicator = document.createElement("div");
indicator.id = "banner-status-indicator";
indicator.style.cssText = `
width: 6px;
height: 6px;
position: relative;
top: 1px;
border-radius: 50%;
background-color: #ccc;
margin-right: 8px;
flex-shrink: 0;
transition: background-color 0.3s ease;
`;
// Create status text
const statusText = document.createElement("div");
statusText.id = "banner-status-text";
statusText.textContent = "Searching for server...";
statusText.style.cssText =
"flex-grow: 1; font-weight: 400; letter-spacing: 0.1px; font-size: 11px;";
// Add elements to statusContainer
statusContainer.appendChild(indicator);
statusContainer.appendChild(statusText);
// Add elements to banner - reconnect button first, then status container
banner.appendChild(reconnectButton);
banner.appendChild(statusContainer);
// Add banner to the beginning of the document body
// This ensures it's the very first element
document.body.prepend(banner);
// Set initial state
updateConnectionBanner(false, null);
}
// Update the connection banner with current status
function updateConnectionBanner(connected, serverInfo) {
const indicator = document.getElementById("banner-status-indicator");
const statusText = document.getElementById("banner-status-text");
const banner = document.getElementById("connection-banner");
const reconnectButton = document.getElementById("banner-reconnect-btn");
if (!indicator || !statusText || !banner || !reconnectButton) return;
if (connected && serverInfo) {
// Connected state with server info
indicator.style.backgroundColor = "#4CAF50"; // Green indicator
statusText.style.color = "#ffffff"; // White text for contrast on black
statusText.textContent = `Connected to ${serverInfo.name} v${serverInfo.version} at ${settings.serverHost}:${settings.serverPort}`;
// Hide reconnect button when connected
reconnectButton.style.display = "none";
} else if (connected) {
// Connected without server info
indicator.style.backgroundColor = "#4CAF50"; // Green indicator
statusText.style.color = "#ffffff"; // White text for contrast on black
statusText.textContent = `Connected to server at ${settings.serverHost}:${settings.serverPort}`;
// Hide reconnect button when connected
reconnectButton.style.display = "none";
} else {
// Disconnected state
indicator.style.backgroundColor = "#F44336"; // Red indicator
statusText.style.color = "#ffffff"; // White text for contrast on black
// Only show "searching" message if discovery is in progress
if (isDiscoveryInProgress) {
statusText.textContent = "Not connected to server. Searching...";
// Hide reconnect button while actively searching
reconnectButton.style.display = "none";
} else {
statusText.textContent = "Not connected to server.";
// Show reconnect button above status message when disconnected and not searching
reconnectButton.style.display = "block";
reconnectButton.textContent = "Reconnect";
}
}
}
// Initialize UI elements
const logLimitInput = document.getElementById("log-limit");
const queryLimitInput = document.getElementById("query-limit");
@ -120,20 +403,64 @@ screenshotPathInput.addEventListener("change", (e) => {
serverHostInput.addEventListener("change", (e) => {
settings.serverHost = e.target.value;
saveSettings();
// Automatically test connection when host is changed
testConnection(settings.serverHost, settings.serverPort);
});
serverPortInput.addEventListener("change", (e) => {
settings.serverPort = parseInt(e.target.value, 10);
saveSettings();
// Automatically test connection when port is changed
testConnection(settings.serverHost, settings.serverPort);
});
// Function to cancel any ongoing discovery operations
function cancelOngoingDiscovery() {
if (isDiscoveryInProgress) {
console.log("Cancelling ongoing discovery operation");
// Abort any fetch requests in progress
if (discoveryController) {
try {
discoveryController.abort();
} catch (error) {
console.error("Error aborting discovery controller:", error);
}
discoveryController = null;
}
// Reset the discovery status
isDiscoveryInProgress = false;
// Update UI to indicate the operation was cancelled
if (
statusText &&
connectionStatusDiv &&
connectionStatusDiv.style.display === "block"
) {
statusText.textContent = "Server discovery operation cancelled";
}
// Clear any pending network timeouts that might be part of the discovery process
clearTimeout(reconnectAttemptTimeout);
reconnectAttemptTimeout = null;
console.log("Discovery operation cancelled successfully");
}
}
// Test server connection
testConnectionButton.addEventListener("click", async () => {
// Cancel any ongoing discovery operations before testing
cancelOngoingDiscovery();
await testConnection(settings.serverHost, settings.serverPort);
});
// Function to test server connection
async function testConnection(host, port) {
// Cancel any ongoing discovery operations
cancelOngoingDiscovery();
connectionStatusDiv.style.display = "block";
statusIcon.className = "status-indicator";
statusText.textContent = "Testing connection...";
@ -151,11 +478,22 @@ async function testConnection(host, port) {
if (identity.signature !== "mcp-browser-connector-24x7") {
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent = `Connection failed: Found a server at ${host}:${port} but it's not the Browser Tools server`;
return;
serverConnected = false;
updateConnectionBanner(false, null);
scheduleReconnectAttempt();
return false;
}
statusIcon.className = "status-indicator status-connected";
statusText.textContent = `Connected successfully to ${identity.name} v${identity.version} at ${host}:${port}`;
serverConnected = true;
updateConnectionBanner(true, identity);
// Clear any scheduled reconnect attempts
if (reconnectAttemptTimeout) {
clearTimeout(reconnectAttemptTimeout);
reconnectAttemptTimeout = null;
}
// Update settings if different port was discovered
if (parseInt(identity.port, 10) !== port) {
@ -164,111 +502,415 @@ async function testConnection(host, port) {
serverPortInput.value = settings.serverPort;
saveSettings();
}
return true;
} else {
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent = `Connection failed: Server returned ${response.status}`;
serverConnected = false;
// Make sure isDiscoveryInProgress is false so the reconnect button will show
isDiscoveryInProgress = false;
// Now update the connection banner to show the reconnect button
updateConnectionBanner(false, null);
scheduleReconnectAttempt();
return false;
}
} catch (error) {
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent = `Connection failed: ${error.message}`;
serverConnected = false;
// Make sure isDiscoveryInProgress is false so the reconnect button will show
isDiscoveryInProgress = false;
// Now update the connection banner to show the reconnect button
updateConnectionBanner(false, null);
scheduleReconnectAttempt();
return false;
}
}
// Server discovery function
discoverServerButton.addEventListener("click", async () => {
connectionStatusDiv.style.display = "block";
statusIcon.className = "status-indicator";
statusText.textContent = "Discovering server...";
// Schedule a reconnect attempt if server isn't found
function scheduleReconnectAttempt() {
// Clear any existing reconnect timeout
if (reconnectAttemptTimeout) {
clearTimeout(reconnectAttemptTimeout);
}
// Common IPs to try
const hosts = ["localhost", "127.0.0.1", "0.0.0.0"];
// Schedule a reconnect attempt in 30 seconds
reconnectAttemptTimeout = setTimeout(() => {
console.log("Attempting to reconnect to server...");
// Only show minimal UI during auto-reconnect
discoverServer(true);
}, 30000); // 30 seconds
}
// Get local IP addresses on common networks
const commonLocalIps = ["192.168.0.", "192.168.1.", "10.0.0.", "10.0.1."];
// Add common local networks with last octet from 1 to 10
for (const prefix of commonLocalIps) {
for (let i = 1; i <= 10; i++) {
hosts.push(`${prefix}${i}`);
// Helper function to try connecting to a server
async function tryServerConnection(host, port) {
try {
// Check if the discovery process was cancelled
if (!isDiscoveryInProgress) {
return false;
}
// Create a local timeout that won't abort the entire discovery process
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 500); // 500ms timeout for each connection attempt
try {
// Use identity endpoint for validation
const response = await fetch(`http://${host}:${port}/.identity`, {
// Use a local controller for this specific request timeout
// but also respect the global discovery cancellation
signal: discoveryController
? AbortSignal.any([controller.signal, discoveryController.signal])
: controller.signal,
});
clearTimeout(timeoutId);
// Check again if discovery was cancelled during the fetch
if (!isDiscoveryInProgress) {
return false;
}
if (response.ok) {
const identity = await response.json();
// Verify this is actually our server by checking the signature
if (identity.signature !== "mcp-browser-connector-24x7") {
console.log(
`Found a server at ${host}:${port} but it's not the Browser Tools server`
);
return false;
}
console.log(`Successfully found server at ${host}:${port}`);
// Update settings with discovered server
settings.serverHost = host;
settings.serverPort = parseInt(identity.port, 10);
serverHostInput.value = settings.serverHost;
serverPortInput.value = settings.serverPort;
saveSettings();
statusIcon.className = "status-indicator status-connected";
statusText.textContent = `Discovered ${identity.name} v${identity.version} at ${host}:${identity.port}`;
// Update connection banner with server info
updateConnectionBanner(true, identity);
// Update connection status
serverConnected = true;
// Clear any scheduled reconnect attempts
if (reconnectAttemptTimeout) {
clearTimeout(reconnectAttemptTimeout);
reconnectAttemptTimeout = null;
}
// End the discovery process
isDiscoveryInProgress = false;
// Successfully found server
return true;
}
return false;
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
// Ignore connection errors during discovery
// But check if it was an abort (cancellation)
if (error.name === "AbortError") {
// Check if this was due to the global discovery cancellation
if (discoveryController && discoveryController.signal.aborted) {
console.log("Connection attempt aborted by global cancellation");
return "aborted";
}
// Otherwise it was just a timeout for this specific connection attempt
return false;
}
console.log(`Connection error for ${host}:${port}: ${error.message}`);
return false;
}
}
// Server discovery function (extracted to be reusable)
async function discoverServer(quietMode = false) {
// Cancel any ongoing discovery operations before starting a new one
cancelOngoingDiscovery();
// Create a new AbortController for this discovery process
discoveryController = new AbortController();
isDiscoveryInProgress = true;
// In quiet mode, we don't show the connection status until we either succeed or fail completely
if (!quietMode) {
connectionStatusDiv.style.display = "block";
statusIcon.className = "status-indicator";
statusText.textContent = "Discovering server...";
}
// Common ports to try
const ports = [3025, parseInt(settings.serverPort, 10)];
// Always update the connection banner
updateConnectionBanner(false, null);
// Ensure the current port is in the list
if (!ports.includes(parseInt(settings.serverPort, 10))) {
ports.push(parseInt(settings.serverPort, 10));
}
try {
console.log("Starting server discovery process");
// Create a progress indicator
let progress = 0;
const totalAttempts = hosts.length * ports.length;
statusText.textContent = `Discovering server... (0/${totalAttempts})`;
// Add an early cancellation listener that will respond to page navigation/refresh
discoveryController.signal.addEventListener("abort", () => {
console.log("Discovery aborted via AbortController signal");
isDiscoveryInProgress = false;
});
// Try each host:port combination
for (const host of hosts) {
for (const port of ports) {
try {
// Skip duplicates if current port is in the ports list multiple times
if (
port === parseInt(settings.serverPort, 10) &&
host === settings.serverHost
) {
progress++;
statusText.textContent = `Discovering server... (${progress}/${totalAttempts})`;
continue;
// Common IPs to try (in order of likelihood)
const hosts = ["localhost", "127.0.0.1"];
// Add the current configured host if it's not already in the list
if (
!hosts.includes(settings.serverHost) &&
settings.serverHost !== "0.0.0.0"
) {
hosts.unshift(settings.serverHost); // Put at the beginning for priority
}
// Add common local network IPs
const commonLocalIps = ["192.168.0.", "192.168.1.", "10.0.0.", "10.0.1."];
for (const prefix of commonLocalIps) {
for (let i = 1; i <= 5; i++) {
// Reduced from 10 to 5 for efficiency
hosts.push(`${prefix}${i}`);
}
}
// Build port list in a smart order:
// 1. Start with current configured port
// 2. Add default port (3025)
// 3. Add sequential ports around the default (for fallback detection)
const ports = [];
// Current configured port gets highest priority
const configuredPort = parseInt(settings.serverPort, 10);
ports.push(configuredPort);
// Add default port if it's not the same as configured
if (configuredPort !== 3025) {
ports.push(3025);
}
// Add sequential fallback ports (from default up to default+10)
for (let p = 3026; p <= 3035; p++) {
if (p !== configuredPort) {
// Avoid duplicates
ports.push(p);
}
}
// Remove duplicates
const uniquePorts = [...new Set(ports)];
console.log("Will check ports:", uniquePorts);
// Create a progress indicator
let progress = 0;
let totalChecked = 0;
// Phase 1: Try the most likely combinations first (current host:port and localhost variants)
console.log("Starting Phase 1: Quick check of high-priority hosts/ports");
const priorityHosts = hosts.slice(0, 2); // First two hosts are highest priority
for (const host of priorityHosts) {
// Check if discovery was cancelled
if (!isDiscoveryInProgress) {
console.log("Discovery process was cancelled during Phase 1");
return false;
}
// Try configured port first
totalChecked++;
if (!quietMode) {
statusText.textContent = `Checking ${host}:${uniquePorts[0]}...`;
}
console.log(`Checking ${host}:${uniquePorts[0]}...`);
const result = await tryServerConnection(host, uniquePorts[0]);
// Check for cancellation or success
if (result === "aborted" || !isDiscoveryInProgress) {
console.log("Discovery process was cancelled");
return false;
} else if (result === true) {
console.log("Server found in priority check");
if (quietMode) {
// In quiet mode, only show the connection banner but hide the status box
connectionStatusDiv.style.display = "none";
}
return true; // Successfully found server
}
// Then try default port if different
if (uniquePorts.length > 1) {
// Check if discovery was cancelled
if (!isDiscoveryInProgress) {
console.log("Discovery process was cancelled");
return false;
}
totalChecked++;
if (!quietMode) {
statusText.textContent = `Checking ${host}:${uniquePorts[1]}...`;
}
console.log(`Checking ${host}:${uniquePorts[1]}...`);
const result = await tryServerConnection(host, uniquePorts[1]);
// Check for cancellation or success
if (result === "aborted" || !isDiscoveryInProgress) {
console.log("Discovery process was cancelled");
return false;
} else if (result === true) {
console.log("Server found in priority check");
if (quietMode) {
// In quiet mode, only show the connection banner but hide the status box
connectionStatusDiv.style.display = "none";
}
return true; // Successfully found server
}
}
}
// If we're in quiet mode and the quick checks failed, show the status now
// as we move into more intensive scanning
if (quietMode) {
connectionStatusDiv.style.display = "block";
statusIcon.className = "status-indicator";
statusText.textContent = "Searching for server...";
}
// Phase 2: Systematic scan of all combinations
const totalAttempts = hosts.length * uniquePorts.length;
console.log(
`Starting Phase 2: Full scan (${totalAttempts} total combinations)`
);
statusText.textContent = `Quick check failed. Starting full scan (${totalChecked}/${totalAttempts})...`;
// First, scan through all ports on localhost/127.0.0.1 to find fallback ports quickly
const localHosts = ["localhost", "127.0.0.1"];
for (const host of localHosts) {
// Skip the first two ports on localhost if we already checked them in Phase 1
const portsToCheck = uniquePorts.slice(
localHosts.includes(host) && priorityHosts.includes(host) ? 2 : 0
);
for (const port of portsToCheck) {
// Check if discovery was cancelled
if (!isDiscoveryInProgress) {
console.log("Discovery process was cancelled during local port scan");
return false;
}
// Update progress
progress++;
statusText.textContent = `Discovering server... (${progress}/${totalAttempts}) - Trying ${host}:${port}`;
totalChecked++;
statusText.textContent = `Scanning local ports... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
console.log(`Checking ${host}:${port}...`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1000); // 1 second timeout per attempt
const result = await tryServerConnection(host, port);
// Use identity endpoint instead of .port for more reliable server validation
const response = await fetch(`http://${host}:${port}/.identity`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.ok) {
const identity = await response.json();
// Verify this is actually our server by checking the signature
if (identity.signature !== "mcp-browser-connector-24x7") {
console.log(
`Found a server at ${host}:${port} but it's not the Browser Tools server`
);
continue;
}
// Update settings with discovered server
settings.serverHost = host;
settings.serverPort = parseInt(identity.port, 10);
serverHostInput.value = settings.serverHost;
serverPortInput.value = settings.serverPort;
saveSettings();
statusIcon.className = "status-indicator status-connected";
statusText.textContent = `Discovered ${identity.name} v${identity.version} at ${host}:${identity.port}`;
// Stop searching once found
return;
// Check for cancellation or success
if (result === "aborted" || !isDiscoveryInProgress) {
console.log("Discovery process was cancelled");
return false;
} else if (result === true) {
console.log(`Server found at ${host}:${port}`);
return true; // Successfully found server
}
} catch (error) {
// Ignore connection errors during discovery
}
}
}
// If we get here, no server was found
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent =
"No server found. Please check server is running and try again.";
});
// Then scan all the remaining host/port combinations
for (const host of hosts) {
// Skip hosts we already checked
if (localHosts.includes(host)) {
continue;
}
for (const port of uniquePorts) {
// Check if discovery was cancelled
if (!isDiscoveryInProgress) {
console.log("Discovery process was cancelled during remote scan");
return false;
}
// Update progress
progress++;
totalChecked++;
statusText.textContent = `Scanning remote hosts... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
console.log(`Checking ${host}:${port}...`);
const result = await tryServerConnection(host, port);
// Check for cancellation or success
if (result === "aborted" || !isDiscoveryInProgress) {
console.log("Discovery process was cancelled");
return false;
} else if (result === true) {
console.log(`Server found at ${host}:${port}`);
return true; // Successfully found server
}
}
}
console.log(
`Discovery process completed, checked ${totalChecked} combinations, no server found`
);
// If we get here, no server was found
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent =
"No server found. Please check server is running and try again.";
serverConnected = false;
// End the discovery process first before updating the banner
isDiscoveryInProgress = false;
// Update the connection banner to show the reconnect button
updateConnectionBanner(false, null);
// Schedule a reconnect attempt
scheduleReconnectAttempt();
return false;
} catch (error) {
console.error("Error during server discovery:", error);
statusIcon.className = "status-indicator status-disconnected";
statusText.textContent = `Error discovering server: ${error.message}`;
serverConnected = false;
// End the discovery process first before updating the banner
isDiscoveryInProgress = false;
// Update the connection banner to show the reconnect button
updateConnectionBanner(false, null);
// Schedule a reconnect attempt
scheduleReconnectAttempt();
return false;
} finally {
console.log("Discovery process finished");
// Always clean up, even if there was an error
if (discoveryController) {
discoveryController = null;
}
}
}
// Bind discover server button to the extracted function
discoverServerButton.addEventListener("click", () => discoverServer(false));
// Screenshot capture functionality
captureScreenshotButton.addEventListener("click", () => {