@@ -11,6 +11,116 @@ import { IncomingMessage } from "http";
11
11
import { Socket } from "net" ;
12
12
import os from "os" ;
13
13
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
+
14
124
// Function to get default downloads folder
15
125
function getDefaultDownloadsFolder ( ) : string {
16
126
const homeDir = os . homedir ( ) ;
@@ -36,6 +146,8 @@ let currentSettings = {
36
146
stringSizeLimit : 500 ,
37
147
maxLogSize : 20000 ,
38
148
screenshotPath : getDefaultDownloadsFolder ( ) ,
149
+ // Add server host configuration
150
+ serverHost : process . env . SERVER_HOST || "0.0.0.0" , // Default to all interfaces
39
151
} ;
40
152
41
153
// Add new storage for selected element
@@ -49,9 +161,68 @@ interface ScreenshotCallback {
49
161
50
162
const screenshotCallbacks = new Map < string , ScreenshotCallback > ( ) ;
51
163
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
+ } ) ;
54
199
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 ( ) ;
55
226
app . use ( cors ( ) ) ;
56
227
// Increase JSON body parser limit to 50MB to handle large screenshots
57
228
app . use ( bodyParser . json ( { limit : "50mb" } ) ) ;
@@ -306,6 +477,16 @@ app.get("/.port", (req, res) => {
306
477
res . send ( PORT . toString ( ) ) ;
307
478
} ) ;
308
479
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
+
309
490
// Add function to clear all logs
310
491
function clearAllLogs ( ) {
311
492
console . log ( "Wiping all logs..." ) ;
@@ -627,29 +808,62 @@ export class BrowserConnector {
627
808
console . log ( "Browser Connector: Received screenshot data, saving..." ) ;
628
809
console . log ( "Browser Connector: Custom path from extension:" , customPath ) ;
629
810
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
+
635
823
console . log ( `Browser Connector: Using path: ${ targetPath } ` ) ;
636
824
637
825
if ( ! base64Data ) {
638
826
throw new Error ( "No screenshot data received from Chrome extension" ) ;
639
827
}
640
828
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
+ }
642
843
643
844
const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, "-" ) ;
644
845
const filename = `screenshot-${ timestamp } .png` ;
645
846
const fullPath = path . join ( targetPath , filename ) ;
847
+ console . log ( `Browser Connector: Full screenshot path: ${ fullPath } ` ) ;
646
848
647
849
// Remove the data:image/png;base64, prefix if present
648
850
const cleanBase64 = base64Data . replace ( / ^ d a t a : i m a g e \/ p n g ; b a s e 6 4 , / , "" ) ;
649
851
650
852
// 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
+ }
653
867
654
868
res . json ( {
655
869
path : fullPath ,
@@ -669,18 +883,88 @@ export class BrowserConnector {
669
883
}
670
884
}
671
885
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 } ` ) ;
676
891
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 ) ;
679
895
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 ) ;
686
970
} ) ;
0 commit comments