55into the request-scoped state, allowing tools to access it via ctx.get_state("unity_instance").
66"""
77from threading import RLock
8+ import logging
89
910from fastmcp .server .middleware import Middleware , MiddlewareContext
1011
1112from transport .plugin_hub import PluginHub
1213
14+ logger = logging .getLogger ("mcp-for-unity-server" )
15+
1316# Store a global reference to the middleware instance so tools can interact
1417# with it to set or clear the active unity instance.
1518_unity_instance_middleware = None
19+ _middleware_lock = RLock ()
1620
1721
1822def get_unity_instance_middleware () -> 'UnityInstanceMiddleware' :
1923 """Get the global Unity instance middleware."""
24+ global _unity_instance_middleware
2025 if _unity_instance_middleware is None :
21- raise RuntimeError (
22- "UnityInstanceMiddleware not initialized. Call set_unity_instance_middleware first." )
26+ with _middleware_lock :
27+ if _unity_instance_middleware is None :
28+ # Auto-initialize if not set (lazy singleton) to handle import order or test cases
29+ _unity_instance_middleware = UnityInstanceMiddleware ()
30+
2331 return _unity_instance_middleware
2432
2533
@@ -42,37 +50,36 @@ def __init__(self):
4250 self ._active_by_key : dict [str , str ] = {}
4351 self ._lock = RLock ()
4452
45- def _get_session_key (self , ctx ) -> str :
53+ def get_session_key (self , ctx ) -> str :
4654 """
4755 Derive a stable key for the calling session.
4856
49- Uses ctx.session_id if available, falls back to 'global'.
57+ Prioritizes client_id for stability.
58+ If client_id is missing, falls back to 'global' (assuming single-user local mode),
59+ ignoring session_id which can be unstable in some transports/clients.
5060 """
51- session_id = getattr (ctx , "session_id" , None )
52- if isinstance (session_id , str ) and session_id :
53- return session_id
54-
5561 client_id = getattr (ctx , "client_id" , None )
5662 if isinstance (client_id , str ) and client_id :
5763 return client_id
5864
65+ # Fallback to global for local dev stability
5966 return "global"
6067
6168 def set_active_instance (self , ctx , instance_id : str ) -> None :
6269 """Store the active instance for this session."""
63- key = self ._get_session_key (ctx )
70+ key = self .get_session_key (ctx )
6471 with self ._lock :
6572 self ._active_by_key [key ] = instance_id
6673
6774 def get_active_instance (self , ctx ) -> str | None :
6875 """Retrieve the active instance for this session."""
69- key = self ._get_session_key (ctx )
76+ key = self .get_session_key (ctx )
7077 with self ._lock :
7178 return self ._active_by_key .get (key )
7279
7380 def clear_active_instance (self , ctx ) -> None :
7481 """Clear the stored instance for this session."""
75- key = self ._get_session_key (ctx )
82+ key = self .get_session_key (ctx )
7683 with self ._lock :
7784 self ._active_by_key .pop (key , None )
7885
@@ -82,13 +89,32 @@ async def on_call_tool(self, context: MiddlewareContext, call_next):
8289
8390 active_instance = self .get_active_instance (ctx )
8491 if active_instance :
92+ # If using HTTP transport (PluginHub configured), validate session
93+ # But for stdio transport (no PluginHub needed or maybe partially configured),
94+ # we should be careful not to clear instance just because PluginHub can't resolve it.
95+ # The 'active_instance' (Name@hash) might be valid for stdio even if PluginHub fails.
96+
8597 session_id : str | None = None
98+ # Only validate via PluginHub if we are actually using HTTP transport
99+ # OR if we want to support hybrid mode. For now, let's be permissive.
86100 if PluginHub .is_configured ():
87101 try :
102+ # resolving session_id might fail if the plugin disconnected
103+ # We only need session_id for HTTP transport routing.
104+ # For stdio, we just need the instance ID.
88105 session_id = await PluginHub ._resolve_session_id (active_instance )
89- except Exception :
90- self .clear_active_instance (ctx )
91- return await call_next (context )
106+ except Exception as exc :
107+ # If resolution fails, it means the Unity instance is not reachable via HTTP/WS.
108+ # If we are in stdio mode, this might still be fine if the user is just setting state?
109+ # But usually if PluginHub is configured, we expect it to work.
110+ # Let's LOG the error but NOT clear the instance immediately to avoid flickering,
111+ # or at least debug why it's failing.
112+ logger .debug (
113+ "PluginHub session resolution failed for %s: %s; leaving active_instance unchanged" ,
114+ active_instance ,
115+ exc ,
116+ exc_info = True ,
117+ )
92118
93119 ctx .set_state ("unity_instance" , active_instance )
94120 if session_id is not None :
0 commit comments