Coverage for src/mcp_internal/webserver_manager.py: 77%
79 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-08 05:40 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-08 05:40 +0000
1"""
2Webserver Manager Module
4Handles web server lifecycle management, port finding, and browser opening.
5"""
7from typing import Dict, Any
8import sys
9import socket
10import threading
11import webbrowser
12import uvicorn
15class WebserverManager:
16 """Manages web server lifecycle and configuration"""
18 def __init__(self, server: 'MCPDocumentationServer'):
19 """
20 Initialize WebserverManager with reference to server instance
22 Args:
23 server: MCPDocumentationServer instance for accessing shared state
24 """
25 self.server = server
26 self.webserver_url = None
27 self.webserver_port = None
28 self.webserver_thread = None
29 self.webserver_started = False
31 def find_free_port(self, start_port: int = 8080) -> int:
32 """Find first available port using OS-assigned port for tests"""
33 import os
35 # For tests, always use OS-assigned port to avoid conflicts
36 if 'pytest' in os.environ.get('_', '') or 'PYTEST_CURRENT_TEST' in os.environ:
37 try:
38 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39 sock.bind(('localhost', 0)) # Let OS assign port
40 port = sock.getsockname()[1]
41 sock.close()
42 return port
43 except OSError:
44 pass
46 # For normal operation, try preferred range first
47 for port in range(start_port, start_port + 20):
48 try:
49 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
50 sock.bind(('localhost', port))
51 sock.close()
52 return port
53 except OSError:
54 continue
56 # Final fallback: OS-assigned port
57 try:
58 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
59 sock.bind(('localhost', 0))
60 port = sock.getsockname()[1]
61 sock.close()
62 return port
63 except OSError:
64 return start_port
66 def start_webserver_thread(self):
67 """Start webserver in background thread (like Serena)"""
68 # Find free port
69 self.webserver_port = self.find_free_port()
70 self.webserver_url = f"http://localhost:{self.webserver_port}"
72 # Start uvicorn in daemon thread
73 def run_server():
74 import sys
75 import os
76 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
77 import web_server
78 # Set the global doc_server
79 web_server.doc_server = self.server
81 # Configure uvicorn (disable logging config for MCP compatibility)
82 config = uvicorn.Config(
83 web_server.app,
84 host="127.0.0.1",
85 port=self.webserver_port,
86 log_config=None, # Disable default logging (incompatible with MCP stdio)
87 access_log=False
88 )
89 server = uvicorn.Server(config)
90 server.run()
92 self.webserver_thread = threading.Thread(target=run_server, daemon=True)
93 self.webserver_thread.start()
95 # Don't wait - let it start in background
96 print(f"✅ Webserver starting at {self.webserver_url}", file=sys.stderr)
98 # Open browser after short delay (non-blocking)
99 def open_browser_delayed():
100 import time
101 time.sleep(1.5)
102 try:
103 webbrowser.open(self.webserver_url)
104 print(f"🚀 Browser opened", file=sys.stderr)
105 except Exception as e:
106 print(f"⚠️ Could not open browser: {e}", file=sys.stderr)
108 browser_thread = threading.Thread(target=open_browser_delayed, daemon=True)
109 browser_thread.start()
111 self.webserver_started = True
113 def get_webserver_status(self) -> Dict[str, Any]:
114 """Get current webserver status"""
115 if not self.server.enable_webserver:
116 return {
117 'enabled': False,
118 'running': False,
119 'port': None,
120 'url': None,
121 'browser_opened': False,
122 'warnings': []
123 }
125 return {
126 'enabled': True,
127 'running': self.webserver_url is not None,
128 'port': self.webserver_port,
129 'url': self.webserver_url,
130 'browser_opened': self.webserver_url is not None, # Assume success if URL exists
131 'warnings': []
132 }
134 def restart_webserver(self) -> bool:
135 """Restart webserver on potentially different port"""
136 if not self.server.enable_webserver:
137 return False
139 try:
140 # Note: Current implementation doesn't support graceful shutdown
141 # This would need to be implemented if restart is needed
142 print("Webserver restart not fully implemented - requires graceful shutdown", file=sys.stderr)
143 return False
145 except Exception as e:
146 print(f"Error restarting webserver: {e}", file=sys.stderr)
147 return False