3737from .._utils ._config import McpServer
3838from ._context import UiPathServerType
3939from ._exception import McpErrorCode , UiPathMcpRuntimeError
40- from ._session import BaseSessionServer , StdioSessionServer , StreamableHttpSessionServer
40+ from ._session import (
41+ BaseSessionServer ,
42+ SessionHealthInfo ,
43+ StdioSessionServer ,
44+ StreamableHttpSessionServer ,
45+ )
4146from ._token_refresh import TokenRefresher
47+ from ._watchdog import SessionWatchdog
4248
4349logger = logging .getLogger (__name__ )
4450tracer = trace .get_tracer (__name__ )
@@ -86,6 +92,7 @@ def __init__(
8692 self ._http_stderr_drain_task : asyncio .Task [None ] | None = None
8793 self ._http_server_stderr_lines : list [str ] = []
8894 self ._uipath = UiPath ()
95+ self ._watchdog : SessionWatchdog | None = None
8996 self ._token_refresher : TokenRefresher | None = None
9097 self ._cleanup_done = False
9198
@@ -118,6 +125,46 @@ def _validate_auth(self) -> None:
118125 UiPathErrorCategory .SYSTEM ,
119126 )
120127
128+ def get_sessions (self ) -> dict [str , SessionHealthInfo ]:
129+ """Return health info for all active sessions (SessionProvider protocol)."""
130+ return {
131+ sid : session .get_health_info ()
132+ for sid , session in self ._session_servers .items ()
133+ }
134+
135+ async def remove_session (self , session_id : str , reason : str ) -> None :
136+ """Remove and stop a session by ID (SessionProvider protocol)."""
137+ session_server = self ._session_servers .pop (session_id , None )
138+ if session_server is not None :
139+ logger .warning (
140+ f"Removing session { session_id } : { reason } "
141+ )
142+ try :
143+ await session_server .stop ()
144+ except Exception :
145+ logger .error (
146+ f"Error stopping session { session_id } during watchdog removal" ,
147+ exc_info = True ,
148+ )
149+ await self ._close_session_on_server (session_id )
150+
151+ async def _close_session_on_server (self , session_id : str ) -> None :
152+ """Notify the UiPath server to remove a session so it stops sending messages."""
153+ try :
154+ await self ._uipath .api_client .request_async (
155+ "DELETE" ,
156+ f"agenthub_/mcp/{ self ._folder_key } /{ self .slug } " ,
157+ headers = {"mcp-session-id" : session_id },
158+ )
159+ logger .info (f"Notified server of session closure: { session_id } " )
160+ except HTTPStatusError as e :
161+ if e .response .status_code == 404 :
162+ logger .info (f"Session { session_id } already removed server-side" )
163+ else :
164+ logger .error (f"Error closing session { session_id } on server: { e } " )
165+ except Exception as e :
166+ logger .error (f"Error closing session { session_id } on server: { e } " )
167+
121168 async def get_schema (self ) -> UiPathRuntimeSchema :
122169 """Get schema for this MCP runtime.
123170
@@ -240,6 +287,9 @@ async def _run_server(self) -> UiPathRuntimeResult:
240287 run_task = asyncio .create_task (self ._signalr_client .run ())
241288 cancel_task = asyncio .create_task (self ._cancel_event .wait ())
242289 self ._keep_alive_task = asyncio .create_task (self ._keep_alive ())
290+
291+ self ._watchdog = SessionWatchdog (self )
292+ self ._watchdog .start ()
243293 self ._token_refresher .start ()
244294
245295 try :
@@ -312,6 +362,10 @@ async def _cleanup(self) -> None:
312362 except asyncio .CancelledError :
313363 pass
314364
365+ if self ._watchdog :
366+ await self ._watchdog .stop ()
367+ self ._watchdog = None
368+
315369 for session_id , session_server in list (self ._session_servers .items ()):
316370 try :
317371 await session_server .stop ()
@@ -367,6 +421,10 @@ async def _handle_signalr_message(self, args: list[str]) -> None:
367421 """
368422 Handle incoming SignalR messages.
369423 """
424+
425+ if self ._cleanup_done :
426+ return
427+
370428 if len (args ) < 2 :
371429 logger .error (f"Received invalid websocket message arguments: { args } " )
372430 return
@@ -769,7 +827,15 @@ async def on_keep_alive_response(
769827 logger .error (f"Error during keep-alive: { response .error } " )
770828 return
771829 session_ids = response .result
772- logger .info (f"Active sessions: { session_ids } " )
830+ logger .info (f"Server active sessions: { session_ids } " )
831+ runtime_sessions = {}
832+ for sid , s in self ._session_servers .items ():
833+ health = s .get_health_info ()
834+ runtime_sessions [sid ] = {
835+ "task_done" : health .task_done ,
836+ "active_requests" : health .active_request_count ,
837+ }
838+ logger .info (f"Runtime active sessions: { runtime_sessions } " )
773839 # If there are no active sessions and this is a sandbox environment
774840 # We need to cancel the runtime
775841 # eg: when user kills the agent that triggered the runtime, before we subscribe to events
@@ -783,6 +849,7 @@ async def on_keep_alive_response(
783849 )
784850 self ._cancel_event .set ()
785851
852+
786853 if self ._signalr_client :
787854 logger .info ("Sending keep-alive ping..." )
788855 await self ._signalr_client .send (
0 commit comments