Skip to content

Commit 8aa1aa4

Browse files
authored
Merge pull request #41 from AgentDeskAI/feature/windows-compatibility
Improve Windows compatibility and connection reliability
2 parents c72e277 + bc4629d commit 8aa1aa4

File tree

9 files changed

+1998
-274
lines changed

9 files changed

+1998
-274
lines changed

browser-tools-mcp/mcp-server.ts

Lines changed: 278 additions & 116 deletions
Large diffs are not rendered by default.

browser-tools-mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentdeskai/browser-tools-mcp",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"description": "MCP (Model Context Protocol) server for browser tools integration",
55
"main": "dist/mcp-server.js",
66
"bin": {

browser-tools-server/browser-connector.ts

Lines changed: 306 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,116 @@ import { IncomingMessage } from "http";
1111
import { Socket } from "net";
1212
import os from "os";
1313

14+
/**
15+
* Converts a file path to the appropriate format for the current platform
16+
* Handles Windows, WSL, macOS and Linux path formats
17+
*
18+
* @param inputPath - The path to convert
19+
* @returns The converted path appropriate for the current platform
20+
*/
21+
function convertPathForCurrentPlatform(inputPath: string): string {
22+
const platform = os.platform();
23+
24+
// If no path provided, return as is
25+
if (!inputPath) return inputPath;
26+
27+
console.log(`Converting path "${inputPath}" for platform: ${platform}`);
28+
29+
// Windows-specific conversion
30+
if (platform === "win32") {
31+
// Convert forward slashes to backslashes
32+
return inputPath.replace(/\//g, "\\");
33+
}
34+
35+
// Linux/Mac-specific conversion
36+
if (platform === "linux" || platform === "darwin") {
37+
// Check if this is a Windows UNC path (starts with \\)
38+
if (inputPath.startsWith("\\\\") || inputPath.includes("\\")) {
39+
// Check if this is a WSL path (contains wsl.localhost or wsl$)
40+
if (inputPath.includes("wsl.localhost") || inputPath.includes("wsl$")) {
41+
// Extract the path after the distribution name
42+
// Handle both \\wsl.localhost\Ubuntu\path and \\wsl$\Ubuntu\path formats
43+
const parts = inputPath.split("\\").filter((part) => part.length > 0);
44+
console.log("Path parts:", parts);
45+
46+
// Find the index after the distribution name
47+
const distNames = [
48+
"Ubuntu",
49+
"Debian",
50+
"kali",
51+
"openSUSE",
52+
"SLES",
53+
"Fedora",
54+
];
55+
56+
// Find the distribution name in the path
57+
let distIndex = -1;
58+
for (const dist of distNames) {
59+
const index = parts.findIndex(
60+
(part) => part === dist || part.toLowerCase() === dist.toLowerCase()
61+
);
62+
if (index !== -1) {
63+
distIndex = index;
64+
break;
65+
}
66+
}
67+
68+
if (distIndex !== -1 && distIndex + 1 < parts.length) {
69+
// Reconstruct the path as a native Linux path
70+
const linuxPath = "/" + parts.slice(distIndex + 1).join("/");
71+
console.log(
72+
`Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
73+
);
74+
return linuxPath;
75+
}
76+
77+
// If we couldn't find a distribution name but it's clearly a WSL path,
78+
// try to extract everything after wsl.localhost or wsl$
79+
const wslIndex = parts.findIndex(
80+
(part) =>
81+
part === "wsl.localhost" ||
82+
part === "wsl$" ||
83+
part.toLowerCase() === "wsl.localhost" ||
84+
part.toLowerCase() === "wsl$"
85+
);
86+
87+
if (wslIndex !== -1 && wslIndex + 2 < parts.length) {
88+
// Skip the WSL prefix and distribution name
89+
const linuxPath = "/" + parts.slice(wslIndex + 2).join("/");
90+
console.log(
91+
`Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
92+
);
93+
return linuxPath;
94+
}
95+
}
96+
97+
// For non-WSL Windows paths, just normalize the slashes
98+
const normalizedPath = inputPath
99+
.replace(/\\\\/g, "/")
100+
.replace(/\\/g, "/");
101+
console.log(
102+
`Converted Windows UNC path "${inputPath}" to "${normalizedPath}"`
103+
);
104+
return normalizedPath;
105+
}
106+
107+
// Handle Windows drive letters (e.g., C:\path\to\file)
108+
if (/^[A-Z]:\\/i.test(inputPath)) {
109+
// Convert Windows drive path to Linux/Mac compatible path
110+
const normalizedPath = inputPath
111+
.replace(/^[A-Z]:\\/i, "/")
112+
.replace(/\\/g, "/");
113+
console.log(
114+
`Converted Windows drive path "${inputPath}" to "${normalizedPath}"`
115+
);
116+
return normalizedPath;
117+
}
118+
}
119+
120+
// Return the original path if no conversion was needed or possible
121+
return inputPath;
122+
}
123+
14124
// Function to get default downloads folder
15125
function getDefaultDownloadsFolder(): string {
16126
const homeDir = os.homedir();
@@ -36,6 +146,8 @@ let currentSettings = {
36146
stringSizeLimit: 500,
37147
maxLogSize: 20000,
38148
screenshotPath: getDefaultDownloadsFolder(),
149+
// Add server host configuration
150+
serverHost: process.env.SERVER_HOST || "0.0.0.0", // Default to all interfaces
39151
};
40152

41153
// Add new storage for selected element
@@ -49,9 +161,68 @@ interface ScreenshotCallback {
49161

50162
const screenshotCallbacks = new Map<string, ScreenshotCallback>();
51163

52-
const app = express();
53-
const PORT = 3025;
164+
// Function to get available port starting with the given port
165+
async function getAvailablePort(
166+
startPort: number,
167+
maxAttempts: number = 10
168+
): Promise<number> {
169+
let currentPort = startPort;
170+
let attempts = 0;
171+
172+
while (attempts < maxAttempts) {
173+
try {
174+
// Try to create a server on the current port
175+
// We'll use a raw Node.js net server for just testing port availability
176+
await new Promise<void>((resolve, reject) => {
177+
const testServer = require("net").createServer();
178+
179+
// Handle errors (e.g., port in use)
180+
testServer.once("error", (err: any) => {
181+
if (err.code === "EADDRINUSE") {
182+
console.log(`Port ${currentPort} is in use, trying next port...`);
183+
currentPort++;
184+
attempts++;
185+
resolve(); // Continue to next iteration
186+
} else {
187+
reject(err); // Different error, propagate it
188+
}
189+
});
190+
191+
// If we can listen, the port is available
192+
testServer.once("listening", () => {
193+
// Make sure to close the server to release the port
194+
testServer.close(() => {
195+
console.log(`Found available port: ${currentPort}`);
196+
resolve();
197+
});
198+
});
54199

200+
// Try to listen on the current port
201+
testServer.listen(currentPort, currentSettings.serverHost);
202+
});
203+
204+
// If we reach here without incrementing the port, it means the port is available
205+
return currentPort;
206+
} catch (error: any) {
207+
console.error(`Error checking port ${currentPort}:`, error);
208+
// For non-EADDRINUSE errors, try the next port
209+
currentPort++;
210+
attempts++;
211+
}
212+
}
213+
214+
// If we've exhausted all attempts, throw an error
215+
throw new Error(
216+
`Could not find an available port after ${maxAttempts} attempts starting from ${startPort}`
217+
);
218+
}
219+
220+
// Start with requested port and find an available one
221+
const REQUESTED_PORT = parseInt(process.env.PORT || "3025", 10);
222+
let PORT = REQUESTED_PORT;
223+
224+
// Create application and initialize middleware
225+
const app = express();
55226
app.use(cors());
56227
// Increase JSON body parser limit to 50MB to handle large screenshots
57228
app.use(bodyParser.json({ limit: "50mb" }));
@@ -306,6 +477,16 @@ app.get("/.port", (req, res) => {
306477
res.send(PORT.toString());
307478
});
308479

480+
// Add new identity endpoint with a unique signature
481+
app.get("/.identity", (req, res) => {
482+
res.json({
483+
port: PORT,
484+
name: "browser-tools-server",
485+
version: "1.1.0",
486+
signature: "mcp-browser-connector-24x7",
487+
});
488+
});
489+
309490
// Add function to clear all logs
310491
function clearAllLogs() {
311492
console.log("Wiping all logs...");
@@ -627,29 +808,62 @@ export class BrowserConnector {
627808
console.log("Browser Connector: Received screenshot data, saving...");
628809
console.log("Browser Connector: Custom path from extension:", customPath);
629810

630-
// Determine target path
631-
const targetPath =
632-
customPath ||
633-
currentSettings.screenshotPath ||
634-
getDefaultDownloadsFolder();
811+
// Always prioritize the path from the Chrome extension
812+
let targetPath = customPath;
813+
814+
// If no path provided by extension, fall back to defaults
815+
if (!targetPath) {
816+
targetPath =
817+
currentSettings.screenshotPath || getDefaultDownloadsFolder();
818+
}
819+
820+
// Convert the path for the current platform
821+
targetPath = convertPathForCurrentPlatform(targetPath);
822+
635823
console.log(`Browser Connector: Using path: ${targetPath}`);
636824

637825
if (!base64Data) {
638826
throw new Error("No screenshot data received from Chrome extension");
639827
}
640828

641-
fs.mkdirSync(targetPath, { recursive: true });
829+
try {
830+
fs.mkdirSync(targetPath, { recursive: true });
831+
console.log(`Browser Connector: Created directory: ${targetPath}`);
832+
} catch (err) {
833+
console.error(
834+
`Browser Connector: Error creating directory: ${targetPath}`,
835+
err
836+
);
837+
throw new Error(
838+
`Failed to create screenshot directory: ${
839+
err instanceof Error ? err.message : String(err)
840+
}`
841+
);
842+
}
642843

643844
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
644845
const filename = `screenshot-${timestamp}.png`;
645846
const fullPath = path.join(targetPath, filename);
847+
console.log(`Browser Connector: Full screenshot path: ${fullPath}`);
646848

647849
// Remove the data:image/png;base64, prefix if present
648850
const cleanBase64 = base64Data.replace(/^data:image\/png;base64,/, "");
649851

650852
// Save the file
651-
fs.writeFileSync(fullPath, cleanBase64, "base64");
652-
console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
853+
try {
854+
fs.writeFileSync(fullPath, cleanBase64, "base64");
855+
console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
856+
} catch (err) {
857+
console.error(
858+
`Browser Connector: Error saving screenshot to: ${fullPath}`,
859+
err
860+
);
861+
throw new Error(
862+
`Failed to save screenshot: ${
863+
err instanceof Error ? err.message : String(err)
864+
}`
865+
);
866+
}
653867

654868
res.json({
655869
path: fullPath,
@@ -669,18 +883,88 @@ export class BrowserConnector {
669883
}
670884
}
671885

672-
// Move the server creation before BrowserConnector instantiation
673-
const server = app.listen(PORT, () => {
674-
console.log(`Aggregator listening on http://127.0.0.1:${PORT}`);
675-
});
886+
// Use an async IIFE to allow for async/await in the initial setup
887+
(async () => {
888+
try {
889+
console.log(`Starting Browser Tools Server...`);
890+
console.log(`Requested port: ${REQUESTED_PORT}`);
676891

677-
// Initialize the browser connector with the existing app AND server
678-
const browserConnector = new BrowserConnector(app, server);
892+
// Find an available port
893+
try {
894+
PORT = await getAvailablePort(REQUESTED_PORT);
679895

680-
// Handle shutdown gracefully
681-
process.on("SIGINT", () => {
682-
server.close(() => {
683-
console.log("Server shut down");
684-
process.exit(0);
685-
});
896+
if (PORT !== REQUESTED_PORT) {
897+
console.log(`\n====================================`);
898+
console.log(`NOTICE: Requested port ${REQUESTED_PORT} was in use.`);
899+
console.log(`Using port ${PORT} instead.`);
900+
console.log(`====================================\n`);
901+
}
902+
} catch (portError) {
903+
console.error(`Failed to find an available port:`, portError);
904+
process.exit(1);
905+
}
906+
907+
// Create the server with the available port
908+
const server = app.listen(PORT, currentSettings.serverHost, () => {
909+
console.log(`\n=== Browser Tools Server Started ===`);
910+
console.log(
911+
`Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
912+
);
913+
914+
if (PORT !== REQUESTED_PORT) {
915+
console.log(
916+
`NOTE: Using fallback port ${PORT} instead of requested port ${REQUESTED_PORT}`
917+
);
918+
}
919+
920+
// Log all available network interfaces for easier discovery
921+
const networkInterfaces = os.networkInterfaces();
922+
console.log("\nAvailable on the following network addresses:");
923+
924+
Object.keys(networkInterfaces).forEach((interfaceName) => {
925+
const interfaces = networkInterfaces[interfaceName];
926+
if (interfaces) {
927+
interfaces.forEach((iface) => {
928+
if (!iface.internal && iface.family === "IPv4") {
929+
console.log(` - http://${iface.address}:${PORT}`);
930+
}
931+
});
932+
}
933+
});
934+
935+
console.log(`\nFor local access use: http://localhost:${PORT}`);
936+
});
937+
938+
// Handle server startup errors
939+
server.on("error", (err: any) => {
940+
if (err.code === "EADDRINUSE") {
941+
console.error(
942+
`ERROR: Port ${PORT} is still in use, despite our checks!`
943+
);
944+
console.error(
945+
`This might indicate another process started using this port after our check.`
946+
);
947+
} else {
948+
console.error(`Server error:`, err);
949+
}
950+
process.exit(1);
951+
});
952+
953+
// Initialize the browser connector with the existing app AND server
954+
const browserConnector = new BrowserConnector(app, server);
955+
956+
// Handle shutdown gracefully
957+
process.on("SIGINT", () => {
958+
server.close(() => {
959+
console.log("Server shut down");
960+
process.exit(0);
961+
});
962+
});
963+
} catch (error) {
964+
console.error("Failed to start server:", error);
965+
process.exit(1);
966+
}
967+
})().catch((err) => {
968+
console.error("Unhandled error during server startup:", err);
969+
process.exit(1);
686970
});

0 commit comments

Comments
 (0)