mirror of
https://github.com/AgentDeskAI/browser-tools-mcp.git
synced 2025-12-25 14:14:11 +00:00
fixed windows paths and added host + port autodiscovery
This commit is contained in:
parent
60248f9fca
commit
bc4629db97
@ -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
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user