From b1514edf887595d0565da13fa75b84548e4573ef Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 6 Jun 2026 15:21:17 +0800 Subject: [PATCH] fix: add MCP server ready check to prevent initialization errors Add server ready state tracking for MCP service to prevent "Received request before initialization was complete" errors when clients connect before GNS3 server completes startup. Changes: - Add MCP server ready state management with wait/notify mechanism - Modify auth wrapper to wait for server initialization before accepting connections - Set MCP server ready flag after GNS3 startup completes This ensures MCP protocol initialization handshake only occurs after GNS3 server is fully initialized, preventing race conditions during server startup. --- gns3server/api/routes/mcp/__init__.py | 62 +++++++++++++++++++++++++++ gns3server/core/tasks.py | 5 +++ 2 files changed, 67 insertions(+) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index 8cf8d0bcb..5c3e4fe17 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -70,6 +70,65 @@ from .computes import ( log = logging.getLogger(__name__) +# ── Server ready state ──────────────────────────────────────────────── +# Tracks whether GNS3 server has completed initialization. +# MCP connections are rejected until startup completes to prevent +# "Received request before initialization was complete" errors. + +_mcp_server_ready = False +_mcp_ready_lock = asyncio.Lock() + + +def set_mcp_server_ready(ready: bool = True) -> None: + """ + Set MCP server ready state. + + Should be called after GNS3 startup completes (database, controller, etc.) + to allow MCP connections to proceed. + + Args: + ready: True to mark server as ready, False to mark as not ready + """ + global _mcp_server_ready + _mcp_server_ready = ready + if ready: + log.info("MCP server is now ready to accept connections") + + +async def wait_for_mcp_ready() -> None: + """ + Wait until MCP server is ready before accepting connections. + + Returns immediately if already ready. Otherwise waits with a timeout + to prevent indefinite blocking during server startup issues. + """ + global _mcp_server_ready + if _mcp_server_ready: + return + + log.debug("MCP server not ready yet, waiting for initialization to complete...") + + async with _mcp_ready_lock: + # Double-check after acquiring lock + if _mcp_server_ready: + return + + # Wait with a timeout to prevent indefinite blocking + # Timeout: 5 seconds (50 * 0.1s) + for _ in range(50): + if _mcp_server_ready: + log.debug("MCP server is now ready, proceeding with connection") + return + await asyncio.sleep(0.1) + + # Timeout reached - log warning but allow connection anyway + # This prevents complete deadlocks if startup has issues + log.warning( + "MCP server ready check timed out after 5 seconds, " + "allowing connection anyway (may experience errors)" + ) + + # ── Per‑connection JWT token ───────────────────────────────────────── # Set during SSE authentication, read by tool handlers running in the # same asyncio task (contextvars propagate through asyncio.to_thread). @@ -459,6 +518,9 @@ def _make_auth_wrapper(inner_app): """ async def auth_wrapper(scope, receive, send): + # Wait for GNS3 server to complete initialization before accepting MCP connections + await wait_for_mcp_ready() + if scope["type"] == "http" and scope["method"] == "GET": token = None headers = dict(scope.get("headers", [])) diff --git a/gns3server/core/tasks.py b/gns3server/core/tasks.py index 99ab6c122..08afc2198 100644 --- a/gns3server/core/tasks.py +++ b/gns3server/core/tasks.py @@ -84,6 +84,11 @@ async def startup(app: FastAPI) -> None: m = module.instance() m.port_manager = PortManager.instance() + # Mark MCP server as ready to accept connections + from gns3server.api.routes.mcp import set_mcp_server_ready + set_mcp_server_ready(True) + log.info("GNS3 server startup completed") + async def shutdown(app: FastAPI) -> None: """