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.
This commit is contained in:
YueGuobin 2026-06-06 15:21:17 +08:00
parent 13d1563762
commit b1514edf88
No known key found for this signature in database
2 changed files with 67 additions and 0 deletions

View File

@ -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)"
)
# ── Perconnection 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", []))

View File

@ -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:
"""