2929logging .basicConfig (level = logging .INFO )
3030logger = logging .getLogger (__name__ )
3131
32+
3233class AsyncStdinReader :
3334 """Async wrapper for stdin."""
34-
35+
3536 async def receive (self ) -> bytes :
3637 line = await asyncio .get_event_loop ().run_in_executor (None , sys .stdin .readline )
3738 return line .encode ()
3839
40+
3941class AsyncStdoutWriter :
4042 """Async wrapper for stdout."""
41-
43+
4244 async def send (self , data : bytes ) -> None :
4345 text = data .decode ()
4446 sys .stdout .write (text )
4547 sys .stdout .flush ()
4648 await asyncio .sleep (0 ) # Yield control back to the event loop
4749
50+
4851@click .group ()
4952def cli ():
5053 """Browser MCP Server CLI."""
@@ -100,9 +103,11 @@ def start(
100103 # Set up browser context and LLM
101104 if chrome_path is None :
102105 chrome_path = os .environ .get ("CHROME_PATH" )
103-
106+
104107 try :
105- logger .info (f"Initializing browser context with Chrome path: { chrome_path or 'default' } " )
108+ logger .info (
109+ f"Initializing browser context with Chrome path: { chrome_path or 'default' } "
110+ )
106111 context = initialize_browser_context (
107112 chrome_path = chrome_path ,
108113 window_width = window_width ,
@@ -112,15 +117,15 @@ def start(
112117 except Exception as e :
113118 logger .error (f"Failed to initialize browser context: { e } " )
114119 return 1
115-
120+
116121 try :
117122 logger .info (f"Initializing LLM with model: { model } " )
118123 llm = ChatOpenAI (model = model , temperature = 0.0 )
119124 logger .info ("LLM initialized successfully" )
120125 except Exception as e :
121126 logger .error (f"Failed to initialize LLM: { e } " )
122127 return 1
123-
128+
124129 try :
125130 # Create MCP server
126131 logger .info ("Creating MCP server" )
@@ -134,70 +139,70 @@ def start(
134139 except Exception as e :
135140 logger .error (f"Failed to create MCP server: { e } " )
136141 return 1
137-
142+
138143 if transport == "stdio" :
139144 # Run the server with stdio transport
140145 logger .info ("Starting browser MCP server with stdio transport" )
141146 return asyncio .run (_run_stdio (app ))
142-
147+
143148 else :
144149 # Set up Starlette app for SSE transport
145150 async def handle_sse (request ):
146151 """Handle SSE connections."""
147152 logger .info (f"New SSE connection from { request .client } " )
148153 logger .info (f"Request headers: { request .headers } " )
149-
154+
150155 # Create a queue for sending messages
151156 send_queue = asyncio .Queue ()
152-
157+
153158 # Define message handlers for MCP server
154159 class SSEReadStream :
155160 async def __aenter__ (self ):
156161 return self
157-
162+
158163 async def __aexit__ (self , exc_type , exc_val , exc_tb ):
159164 pass
160-
165+
161166 async def receive (self ) -> bytes :
162167 # For SSE, we don't receive anything from client
163168 # Just block indefinitely
164169 future = asyncio .Future ()
165170 await future # This will block forever
166171 return b"" # Never reached
167-
172+
168173 class SSEWriteStream :
169174 async def __aenter__ (self ):
170175 return self
171-
176+
172177 async def __aexit__ (self , exc_type , exc_val , exc_tb ):
173178 pass
174-
179+
175180 async def send (self , data : bytes ) -> None :
176181 # Queue the message to be sent over SSE
177182 await send_queue .put (data )
178-
183+
179184 # Create async generator to stream SSE responses
180185 async def stream_response ():
181186 """Stream SSE responses."""
182187 logger .info ("Setting up SSE stream" )
183-
188+
184189 # Start MCP server in background
185190 read_stream = SSEReadStream ()
186191 write_stream = SSEWriteStream ()
187-
192+
188193 server_task = asyncio .create_task (
189194 app .run (
190195 read_stream = read_stream ,
191196 write_stream = write_stream ,
192- initialization_options = {}
197+ initialization_options = {},
193198 )
194199 )
195-
200+
196201 try :
197202 # Send initial connected event
198203 logger .info ("Sending initial connected event" )
199204 yield b"event: connected\n data: {}\n \n "
200-
205+
201206 # Stream messages from the queue
202207 logger .info ("Starting to stream messages" )
203208 while True :
@@ -212,119 +217,126 @@ async def stream_response():
212217 # Clean up
213218 server_task .cancel ()
214219 logger .info ("SSE connection closed" )
215-
220+
216221 return StreamingResponse (
217222 stream_response (),
218223 media_type = "text/event-stream" ,
219224 headers = {"Cache-Control" : "no-cache" , "Connection" : "keep-alive" },
220225 )
221-
226+
222227 async def health_check (request ):
223228 """Health check endpoint."""
224229 try :
225230 # Check browser health
226231 healthy = await check_browser_health (context )
227232 return JSONResponse ({"status" : "healthy" if healthy else "unhealthy" })
228233 except Exception as e :
229- return JSONResponse ({"status" : "error" , "message" : str (e )}, status_code = 500 )
230-
234+ return JSONResponse (
235+ {"status" : "error" , "message" : str (e )}, status_code = 500
236+ )
237+
231238 async def reset_context (request ):
232239 """Reset browser context endpoint."""
233240 try :
234241 # Reset browser context
235242 await reset_browser_context (context )
236- return JSONResponse ({"status" : "success" , "message" : "Browser context reset" })
243+ return JSONResponse (
244+ {"status" : "success" , "message" : "Browser context reset" }
245+ )
237246 except Exception as e :
238- return JSONResponse ({"status" : "error" , "message" : str (e )}, status_code = 500 )
239-
247+ return JSONResponse (
248+ {"status" : "error" , "message" : str (e )}, status_code = 500
249+ )
250+
240251 # Define startup and shutdown events
241252 async def startup_event ():
242253 """Run on server startup."""
243254 logger .info ("Starting server..." )
244-
255+
245256 # Start task cleanup job
246257 asyncio .create_task (cleanup_old_tasks ())
247-
258+
248259 logger .info (f"Server started on port { port } " )
249-
260+
250261 async def shutdown_event ():
251262 """Run on server shutdown."""
252263 logger .info ("Shutting down server..." )
253-
264+
254265 try :
255266 # Close the browser
256267 await context .browser .close ()
257268 logger .info ("Browser closed successfully" )
258269 except Exception as e :
259270 logger .error (f"Error closing browser: { e } " )
260-
271+
261272 logger .info ("Server shut down" )
262-
273+
263274 async def cleanup_old_tasks ():
264275 """Periodically clean up expired tasks."""
265276 from datetime import datetime
266-
277+
267278 while True :
268279 try :
269280 # Check for expired tasks every minute
270281 await asyncio .sleep (60 )
271-
282+
272283 # Get current time
273284 now = datetime .now ()
274-
285+
275286 # Check each task
276287 expired_tasks = []
277288 for task_id , task in task_store .items ():
278289 if "expiry_time" in task :
279290 # Parse expiry time
280291 expiry_time = datetime .fromisoformat (task ["expiry_time" ])
281-
292+
282293 # Check if expired
283294 if now > expiry_time :
284295 expired_tasks .append (task_id )
285-
296+
286297 # Remove expired tasks
287298 for task_id in expired_tasks :
288299 logger .info (f"Removing expired task { task_id } " )
289300 task_store .pop (task_id , None )
290-
301+
291302 except Exception as e :
292303 logger .error (f"Error cleaning up old tasks: { e } " )
293-
304+
294305 # Create Starlette app with routes
295306 routes = [
296307 Route ("/sse" , endpoint = handle_sse , methods = ["GET" ]),
297308 Route ("/health" , endpoint = health_check , methods = ["GET" ]),
298309 Route ("/reset" , endpoint = reset_context , methods = ["POST" ]),
299310 ]
300-
311+
301312 starlette_app = Starlette (
302313 routes = routes ,
303314 on_startup = [startup_event ],
304315 on_shutdown = [shutdown_event ],
305- debug = True
316+ debug = True ,
306317 )
307-
318+
308319 # Run with uvicorn
309320 logger .info (f"Starting browser MCP server with SSE transport on port { port } " )
310321 uvicorn .run (starlette_app , host = "0.0.0.0" , port = port )
311-
322+
312323 return 0
313324
325+
314326async def _run_stdio (app : Server ) -> int :
315327 """Run the server using stdio transport."""
316328 try :
317329 stdin_reader = AsyncStdinReader ()
318330 stdout_writer = AsyncStdoutWriter ()
319-
331+
320332 # Create initialization options
321333 initialization_options = {}
322-
334+
323335 # Run the server
324336 await app .run (
325337 read_stream = stdin_reader ,
326338 write_stream = stdout_writer ,
327- initialization_options = initialization_options
339+ initialization_options = initialization_options ,
328340 )
329341 return 0
330342 except KeyboardInterrupt :
@@ -334,5 +346,6 @@ async def _run_stdio(app: Server) -> int:
334346 logger .error (f"Error running server: { e } " )
335347 return 1
336348
349+
337350if __name__ == "__main__" :
338- cli ()
351+ cli ()
0 commit comments