Skip to content

Commit 962c090

Browse files
committed
Fix port reuse and bind on the main thread before serving requests
1 parent a2d6680 commit 962c090

File tree

1 file changed

+40
-30
lines changed

1 file changed

+40
-30
lines changed

src/zeromcp/mcp.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def send_event(self, event_type: str, data):
5858
return False
5959

6060
class _McpHttpRequestHandler(BaseHTTPRequestHandler):
61-
mcp_server: "McpServer"
61+
def __init__(self, request, client_address, server):
62+
self.mcp_server: "McpServer" = getattr(server, "mcp_server")
63+
super().__init__(request, client_address, server)
6264

6365
def log_message(self, format, *args):
6466
"""Override to suppress default logging or customize"""
@@ -198,8 +200,8 @@ def __init__(self, name: str):
198200
self.name = name
199201
self.tools = McpToolRegistry()
200202

201-
self.http_server = None
202-
self.server_thread = None
203+
self.http_server: ThreadingHTTPServer | None = None
204+
self.server_thread: threading.Thread | None = None
203205
self.running = False
204206
self.sse_connections: dict[str, _McpSseConnection] = {}
205207

@@ -218,10 +220,44 @@ def serve(self, host: str, port: int):
218220
print("[MCP] Server is already running")
219221
return
220222

221-
self.server_thread = threading.Thread(target=self._run_server, daemon=True, args=(host, port))
223+
# Create server with deferred binding
224+
self.http_server = ThreadingHTTPServer(
225+
(host, port),
226+
_McpHttpRequestHandler,
227+
bind_and_activate=False
228+
)
229+
self.http_server.allow_reuse_address = False
230+
231+
# Set the MCPServer instance on the handler class
232+
setattr(self.http_server, "mcp_server", self)
233+
234+
try:
235+
# Bind and activate in main thread - errors propagate synchronously
236+
self.http_server.server_bind()
237+
self.http_server.server_activate()
238+
except OSError:
239+
# Cleanup on binding failure
240+
self.http_server.server_close()
241+
self.http_server = None
242+
raise
243+
244+
# Only start thread after successful bind
222245
self.running = True
246+
def serve_forever():
247+
try:
248+
self.http_server.serve_forever() # type: ignore
249+
except Exception as e:
250+
print(f"[MCP] Server error: {e}")
251+
traceback.print_exc()
252+
finally:
253+
self.running = False
254+
self.server_thread = threading.Thread(target=serve_forever, daemon=True)
223255
self.server_thread.start()
224256

257+
print("[MCP] Server started:")
258+
print(f" Streamable HTTP: http://{host}:{port}/mcp")
259+
print(f" SSE: http://{host}:{port}/sse")
260+
225261
def stop(self):
226262
if not self.running:
227263
return
@@ -260,32 +296,6 @@ def stdio(self, stdin: BinaryIO = sys.stdin.buffer, stdout: BinaryIO = sys.stdou
260296
except (BrokenPipeError, KeyboardInterrupt): # Client disconnected
261297
break
262298

263-
def _run_server(self, host: str, port: int):
264-
"""Run the HTTP server main loop using ThreadingHTTPServer"""
265-
# Set the MCPServer instance on the handler class
266-
_McpHttpRequestHandler.mcp_server = self
267-
268-
269-
# Create HTTP server with threading support and exclusive binding
270-
self.http_server = ThreadingHTTPServer(
271-
(host, port),
272-
_McpHttpRequestHandler
273-
)
274-
self.http_server.allow_reuse_address = False
275-
276-
print("[MCP] Server started:")
277-
print(f" Streamable HTTP: http://{host}:{port}/mcp")
278-
print(f" SSE: http://{host}:{port}/sse")
279-
280-
try:
281-
# Serve until shutdown() is called
282-
self.http_server.serve_forever()
283-
except Exception as e:
284-
print(f"[MCP] Server error: {e}")
285-
traceback.print_exc()
286-
finally:
287-
self.running = False
288-
289299
def _mcp_ping(self, _meta: dict | None = None) -> dict:
290300
"""MCP ping method"""
291301
return {}

0 commit comments

Comments
 (0)