Skip to content

Commit 46e6cf3

Browse files
committed
Add Windows compatibility features: path conversion and configurable host/port settings
1 parent c72e277 commit 46e6cf3

File tree

2 files changed

+225
-31
lines changed

2 files changed

+225
-31
lines changed

browser-tools-mcp/mcp-server.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,61 @@
22

33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5+
import path from "path";
6+
import fs from "fs";
7+
import os from "os";
58

69
// Create the MCP server
710
const server = new McpServer({
8-
name: "Browsert Tools MCP",
11+
name: "Browser Tools MCP",
912
version: "1.0.9",
1013
});
1114

12-
// Function to get the port from the .port file
13-
// function getPort(): number {
14-
// try {
15-
// const port = parseInt(fs.readFileSync(".port", "utf8"));
16-
// return port;
17-
// } catch (err) {
18-
// console.error("Could not read port file, defaulting to 3000");
19-
// return 3025;
20-
// }
21-
// }
15+
// Function to get the port from environment variable or default
16+
function getServerPort(): number {
17+
// Check environment variable first
18+
if (process.env.BROWSER_TOOLS_PORT) {
19+
const envPort = parseInt(process.env.BROWSER_TOOLS_PORT, 10);
20+
if (!isNaN(envPort) && envPort > 0) {
21+
return envPort;
22+
}
23+
}
24+
25+
// Try to read from .port file
26+
try {
27+
const portFilePath = path.join(__dirname, ".port");
28+
if (fs.existsSync(portFilePath)) {
29+
const port = parseInt(fs.readFileSync(portFilePath, "utf8").trim(), 10);
30+
if (!isNaN(port) && port > 0) {
31+
return port;
32+
}
33+
}
34+
} catch (err) {
35+
console.error("Error reading port file:", err);
36+
}
37+
38+
// Default port if no configuration found
39+
return 3025;
40+
}
41+
42+
// Function to get server host from environment variable or default
43+
function getServerHost(): string {
44+
// Check environment variable first
45+
if (process.env.BROWSER_TOOLS_HOST) {
46+
return process.env.BROWSER_TOOLS_HOST;
47+
}
2248

23-
// const PORT = getPort();
49+
// Default to localhost
50+
return "127.0.0.1";
51+
}
2452

25-
const PORT = 3025;
53+
const PORT = getServerPort();
54+
const HOST = getServerHost();
2655

2756
// We'll define four "tools" that retrieve data from the aggregator at localhost:3000
2857

2958
server.tool("getConsoleLogs", "Check our browser logs", async () => {
30-
const response = await fetch(`http://127.0.0.1:${PORT}/console-logs`);
59+
const response = await fetch(`http://${HOST}:${PORT}/console-logs`);
3160
const json = await response.json();
3261
return {
3362
content: [
@@ -43,7 +72,7 @@ server.tool(
4372
"getConsoleErrors",
4473
"Check our browsers console errors",
4574
async () => {
46-
const response = await fetch(`http://127.0.0.1:${PORT}/console-errors`);
75+
const response = await fetch(`http://${HOST}:${PORT}/console-errors`);
4776
const json = await response.json();
4877
return {
4978
content: [
@@ -58,7 +87,7 @@ server.tool(
5887

5988
// Return all HTTP errors (4xx/5xx)
6089
server.tool("getNetworkErrorLogs", "Check our network ERROR logs", async () => {
61-
const response = await fetch(`http://127.0.0.1:${PORT}/network-errors`);
90+
const response = await fetch(`http://${HOST}:${PORT}/network-errors`);
6291
const json = await response.json();
6392
return {
6493
content: [
@@ -90,7 +119,7 @@ server.tool(
90119
"getNetworkSuccessLogs",
91120
"Check our network SUCCESS logs",
92121
async () => {
93-
const response = await fetch(`http://127.0.0.1:${PORT}/network-success`);
122+
const response = await fetch(`http://${HOST}:${PORT}/network-success`);
94123
const json = await response.json();
95124
return {
96125
content: [
@@ -110,7 +139,7 @@ server.tool(
110139
async () => {
111140
try {
112141
const response = await fetch(
113-
`http://127.0.0.1:${PORT}/capture-screenshot`,
142+
`http://${HOST}:${PORT}/capture-screenshot`,
114143
{
115144
method: "POST",
116145
}
@@ -161,7 +190,7 @@ server.tool(
161190
"getSelectedElement",
162191
"Get the selected element from the browser",
163192
async () => {
164-
const response = await fetch(`http://127.0.0.1:${PORT}/selected-element`);
193+
const response = await fetch(`http://${HOST}:${PORT}/selected-element`);
165194
const json = await response.json();
166195
return {
167196
content: [
@@ -176,7 +205,7 @@ server.tool(
176205

177206
// Add new tool for wiping logs
178207
server.tool("wipeLogs", "Wipe all browser logs from memory", async () => {
179-
const response = await fetch(`http://127.0.0.1:${PORT}/wipelogs`, {
208+
const response = await fetch(`http://${HOST}:${PORT}/wipelogs`, {
180209
method: "POST",
181210
});
182211
const json = await response.json();

browser-tools-server/browser-connector.ts

Lines changed: 176 additions & 11 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
@@ -50,7 +162,7 @@ interface ScreenshotCallback {
50162
const screenshotCallbacks = new Map<string, ScreenshotCallback>();
51163

52164
const app = express();
53-
const PORT = 3025;
165+
const PORT = parseInt(process.env.PORT || "3025", 10);
54166

55167
app.use(cors());
56168
// Increase JSON body parser limit to 50MB to handle large screenshots
@@ -627,29 +739,62 @@ export class BrowserConnector {
627739
console.log("Browser Connector: Received screenshot data, saving...");
628740
console.log("Browser Connector: Custom path from extension:", customPath);
629741

630-
// Determine target path
631-
const targetPath =
632-
customPath ||
633-
currentSettings.screenshotPath ||
634-
getDefaultDownloadsFolder();
742+
// Always prioritize the path from the Chrome extension
743+
let targetPath = customPath;
744+
745+
// If no path provided by extension, fall back to defaults
746+
if (!targetPath) {
747+
targetPath =
748+
currentSettings.screenshotPath || getDefaultDownloadsFolder();
749+
}
750+
751+
// Convert the path for the current platform
752+
targetPath = convertPathForCurrentPlatform(targetPath);
753+
635754
console.log(`Browser Connector: Using path: ${targetPath}`);
636755

637756
if (!base64Data) {
638757
throw new Error("No screenshot data received from Chrome extension");
639758
}
640759

641-
fs.mkdirSync(targetPath, { recursive: true });
760+
try {
761+
fs.mkdirSync(targetPath, { recursive: true });
762+
console.log(`Browser Connector: Created directory: ${targetPath}`);
763+
} catch (err) {
764+
console.error(
765+
`Browser Connector: Error creating directory: ${targetPath}`,
766+
err
767+
);
768+
throw new Error(
769+
`Failed to create screenshot directory: ${
770+
err instanceof Error ? err.message : String(err)
771+
}`
772+
);
773+
}
642774

643775
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
644776
const filename = `screenshot-${timestamp}.png`;
645777
const fullPath = path.join(targetPath, filename);
778+
console.log(`Browser Connector: Full screenshot path: ${fullPath}`);
646779

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

650783
// Save the file
651-
fs.writeFileSync(fullPath, cleanBase64, "base64");
652-
console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
784+
try {
785+
fs.writeFileSync(fullPath, cleanBase64, "base64");
786+
console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
787+
} catch (err) {
788+
console.error(
789+
`Browser Connector: Error saving screenshot to: ${fullPath}`,
790+
err
791+
);
792+
throw new Error(
793+
`Failed to save screenshot: ${
794+
err instanceof Error ? err.message : String(err)
795+
}`
796+
);
797+
}
653798

654799
res.json({
655800
path: fullPath,
@@ -670,8 +815,28 @@ export class BrowserConnector {
670815
}
671816

672817
// 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}`);
818+
const server = app.listen(PORT, currentSettings.serverHost, () => {
819+
console.log(`\n=== Browser Tools Server Started ===`);
820+
console.log(
821+
`Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
822+
);
823+
824+
// Log all available network interfaces for easier discovery
825+
const networkInterfaces = os.networkInterfaces();
826+
console.log("\nAvailable on the following network addresses:");
827+
828+
Object.keys(networkInterfaces).forEach((interfaceName) => {
829+
const interfaces = networkInterfaces[interfaceName];
830+
if (interfaces) {
831+
interfaces.forEach((iface) => {
832+
if (!iface.internal && iface.family === "IPv4") {
833+
console.log(` - http://${iface.address}:${PORT}`);
834+
}
835+
});
836+
}
837+
});
838+
839+
console.log(`\nFor local access use: http://localhost:${PORT}`);
675840
});
676841

677842
// Initialize the browser connector with the existing app AND server

0 commit comments

Comments
 (0)