From 6df97a9dcf129778ea930cf41e10a25f742cd7f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Claude4=CE=9Elope?= <xilope@esus.name>
Date: Fri, 27 Jun 2025 21:49:32 +0200
Subject: [PATCH] Fix Claude Desktop timeout issue in server mode
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The server was hanging on initialize requests because:
1. It was calling browser.initialize() at startup which tried to connect to upstream servers
2. It was using blocking stdin.read(4096) instead of line-based reading

Fixed by:
- Removing premature initialization in server mode (let it happen lazily)
- Switching to readline() for proper line-based JSON-RPC handling
- Adding proper error responses for malformed JSON

This resolves the 60-second timeout error when Claude Desktop tries to connect.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
---
 mcp_browser/__main__.py | 172 +++++++++++++++++++++++++++-------------
 1 file changed, 119 insertions(+), 53 deletions(-)

diff --git a/mcp_browser/__main__.py b/mcp_browser/__main__.py
index a23a9f7..ff8fb08 100644
--- a/mcp_browser/__main__.py
+++ b/mcp_browser/__main__.py
@@ -16,7 +16,7 @@ import yaml
 from .proxy import MCPBrowser
 from .config import ConfigLoader
 from .default_configs import ConfigManager
-from .daemon import MCPBrowserDaemon, MCPBrowserClient, get_socket_path, is_daemon_running
+from .daemon import MCPBrowserDaemon, MCPBrowserClient, get_socket_path, is_daemon_running, kill_daemon_with_children
 from .logging_config import setup_logging, get_logger
 
 
@@ -362,8 +362,14 @@ async def start_daemon_background(args):
     socket_path = get_socket_path(args.server)
     
     if is_daemon_running(socket_path):
-        print(f"Daemon already running for server: {args.server or 'default'}")
-        return
+        print(f"Killing existing daemon for server: {args.server or 'default'}")
+        if kill_daemon_with_children(socket_path):
+            print("Existing daemon and children killed successfully")
+            # Wait a moment for cleanup
+            import time
+            time.sleep(0.5)
+        else:
+            print("Warning: Failed to kill existing daemon cleanly")
     
     # Fork to background
     pid = os.fork()
@@ -376,29 +382,49 @@ async def start_daemon_background(args):
     # Detach from terminal
     os.setsid()
     
+    # Second fork to prevent zombie processes
+    try:
+        pid = os.fork()
+        if pid > 0:
+            # First child exits, orphaning the daemon
+            sys.exit(0)
+    except OSError as e:
+        print(f"Fork #2 failed: {e}", file=sys.stderr)
+        sys.exit(1)
+    
+    # Close file descriptors
+    sys.stdin.close()
+    sys.stdout.close()
+    sys.stderr.close()
+    
     # Redirect stdout/stderr to log file
     log_dir = socket_path.parent / "logs"
     log_dir.mkdir(parents=True, exist_ok=True)
     log_file = log_dir / f"mcp-browser-{args.server or 'default'}.log"
     
-    with open(log_file, 'a') as log:
-        sys.stdout = log
-        sys.stderr = log
-        
-        # Create browser
-        config_path = Path(args.config) if args.config else None
-        browser = MCPBrowser(
-            server_name=args.server,
-            config_path=config_path,
-            enable_builtin_servers=not args.no_builtin
-        )
-        
-        # Run daemon
+    # Open new file descriptors
+    sys.stdin = open(os.devnull, 'r')
+    sys.stdout = open(log_file, 'a', buffering=1)
+    sys.stderr = sys.stdout
+    
+    # Create browser
+    config_path = Path(args.config) if args.config else None
+    browser = MCPBrowser(
+        server_name=args.server,
+        config_path=config_path,
+        enable_builtin_servers=not args.no_builtin
+    )
+    
+    # Run daemon
+    try:
         asyncio.run(run_daemon_mode(browser, socket_path))
+    except Exception as e:
+        print(f"Daemon error: {e}", file=sys.stderr)
+        sys.exit(1)
 
 
 def stop_daemon(args):
-    """Stop running daemon."""
+    """Stop running daemon and all child processes."""
     socket_path = get_socket_path(args.server)
     
     if not is_daemon_running(socket_path):
@@ -408,8 +434,12 @@ def stop_daemon(args):
     pid_file = socket_path.with_suffix('.pid')
     try:
         pid = int(pid_file.read_text().strip())
-        os.kill(pid, signal.SIGTERM)
-        print(f"Sent SIGTERM to daemon (PID: {pid})")
+        print(f"Stopping daemon (PID: {pid}) and all child processes...")
+        
+        if kill_daemon_with_children(socket_path):
+            print("Daemon and all children stopped successfully")
+        else:
+            print("Warning: Daemon may not have been stopped cleanly")
     except Exception as e:
         print(f"Error stopping daemon: {e}")
 
@@ -431,28 +461,39 @@ async def run_server_mode(browser: MCPBrowser):
     """Run MCP Browser as an MCP server (stdin/stdout)."""
     import sys
     
-    await browser.initialize()
+    # Don't initialize here - let it happen lazily when Claude sends initialize request
+    # This prevents timeout issues when the browser tries to connect to upstream servers
     
-    # Read JSON-RPC from stdin, write to stdout
-    buffer = ""
+    # Read JSON-RPC from stdin line by line
     while True:
         try:
-            chunk = sys.stdin.read(4096)
-            if not chunk:
+            line = sys.stdin.readline()
+            if not line:  # EOF
                 break
                 
-            buffer += chunk
-            while '\n' in buffer:
-                line, buffer = buffer.split('\n', 1)
-                if line.strip():
-                    try:
-                        request = json.loads(line)
-                        response = await browser.call(request)
-                        print(json.dumps(response))
-                        sys.stdout.flush()
-                    except json.JSONDecodeError:
-                        pass
-                        
+            line = line.strip()
+            if not line:  # Empty line
+                continue
+                
+            try:
+                request = json.loads(line)
+                response = await browser.call(request)
+                print(json.dumps(response))
+                sys.stdout.flush()
+            except json.JSONDecodeError as e:
+                # Send error response for malformed JSON
+                error_response = {
+                    "jsonrpc": "2.0",
+                    "id": None,
+                    "error": {
+                        "code": -32700,
+                        "message": "Parse error",
+                        "data": str(e)
+                    }
+                }
+                print(json.dumps(error_response))
+                sys.stdout.flush()
+                
         except KeyboardInterrupt:
             break
         except EOFError:
@@ -464,25 +505,35 @@ async def run_server_mode_with_daemon(socket_path: Path):
     import sys
     
     async with MCPBrowserClient(socket_path) as client:
-        # Read JSON-RPC from stdin, forward to daemon, write to stdout
-        buffer = ""
+        # Read JSON-RPC from stdin line by line, forward to daemon
         while True:
             try:
-                chunk = sys.stdin.read(4096)
-                if not chunk:
+                line = sys.stdin.readline()
+                if not line:  # EOF
                     break
                     
-                buffer += chunk
-                while '\n' in buffer:
-                    line, buffer = buffer.split('\n', 1)
-                    if line.strip():
-                        try:
-                            request = json.loads(line)
-                            response = await client.call(request)
-                            print(json.dumps(response))
-                            sys.stdout.flush()
-                        except json.JSONDecodeError:
-                            pass
+                line = line.strip()
+                if not line:  # Empty line
+                    continue
+                    
+                try:
+                    request = json.loads(line)
+                    response = await client.call(request)
+                    print(json.dumps(response))
+                    sys.stdout.flush()
+                except json.JSONDecodeError as e:
+                    # Send error response for malformed JSON
+                    error_response = {
+                        "jsonrpc": "2.0",
+                        "id": None,
+                        "error": {
+                            "code": -32700,
+                            "message": "Parse error",
+                            "data": str(e)
+                        }
+                    }
+                    print(json.dumps(error_response))
+                    sys.stdout.flush()
                             
             except KeyboardInterrupt:
                 break
@@ -552,16 +603,28 @@ def main():
     early_args, _ = args_parser.parse_known_args()
     
     # Setup logging before anything else
+    # IMPORTANT: In server mode, we must not log to stderr as it may interfere
+    # Determine if we're in server mode early
+    is_server_mode = "--mode" in sys.argv and "server" in sys.argv
+    
     log_file = Path(early_args.log_file) if early_args.log_file else None
+    
+    # In server mode, use syslog unless a log file is specified
+    use_syslog = is_server_mode and not log_file
+    
     setup_logging(
         debug=early_args.debug,
         log_file=log_file,
-        log_level=early_args.log_level
+        log_level=early_args.log_level,
+        use_syslog=use_syslog
     )
     
+    # Import version
+    from . import __version__
+    
     # Now create the full parser
     parser = argparse.ArgumentParser(
-        description="MCP Browser - Universal Model Context Protocol Interface",
+        description=f"MCP Browser v{__version__} - Universal Model Context Protocol Interface",
         epilog="""
 Examples:
   # Interactive mode
@@ -624,6 +687,9 @@ Environment:
                        help="Stop running daemon")
     parser.add_argument("--daemon-status", action="store_true",
                        help="Check daemon status")
+    parser.add_argument("--version", "-v", action="version",
+                       version=f"%(prog)s {__version__}",
+                       help="Show program version and exit")
     
     # MCP method commands
     subparsers = parser.add_subparsers(dest="command", help="MCP methods")
