33import os
44import sys
55import tempfile
6+ import uuid
67from typing import Any , Dict , Optional
78
89import mcp .types as types
@@ -36,6 +37,7 @@ def __init__(self, context: UiPathMcpRuntimeContext):
3637 super ().__init__ (context )
3738 self .context : UiPathMcpRuntimeContext = context
3839 self ._server : Optional [McpServer ] = None
40+ self ._runtime_id = self .context .job_id if self .context .job_id else str (uuid .uuid4 ())
3941 self ._signalr_client : Optional [SignalRClient ] = None
4042 self ._session_servers : Dict [str , SessionServer ] = {}
4143 self ._session_output : Optional [str ] = None
@@ -59,13 +61,10 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
5961 return None
6062
6163 # Set up SignalR client
62- signalr_url = f"{ os .environ .get ('UIPATH_URL' )} /mcp_/wsstunnel?slug={ self ._server .name } "
63- if self ._server .session_id :
64- signalr_url += f"&sessionId={ self ._server .session_id } "
64+ signalr_url = f"{ os .environ .get ('UIPATH_URL' )} /mcp_/wsstunnel?slug={ self ._server .name } &runtimeId={ self ._runtime_id } "
6565
6666 with tracer .start_as_current_span (self ._server .name ) as root_span :
67- if self ._server .session_id :
68- root_span .set_attribute ("session_id" , self ._server .session_id )
67+ root_span .set_attribute ("runtime_id" , self ._runtime_id )
6968 root_span .set_attribute ("command" , self ._server .command )
7069 root_span .set_attribute ("args" , self ._server .args )
7170 root_span .set_attribute ("span_type" , "MCP Server" )
@@ -136,13 +135,17 @@ async def validate(self) -> None:
136135
137136 async def cleanup (self ) -> None :
138137 """Clean up all resources."""
138+
139+ self ._on_runtime_abort ()
140+
139141 for session_id , session_server in self ._session_servers .items ():
140142 try :
141143 await session_server .stop ()
142144 except Exception as e :
143145 logger .error (
144146 f"Error cleaning up session server { session_id } : { str (e )} "
145147 )
148+
146149 if self ._signalr_client and hasattr (self ._signalr_client , "_transport" ):
147150 transport = self ._signalr_client ._transport
148151 if transport and hasattr (transport , "_ws" ) and transport ._ws :
@@ -172,14 +175,14 @@ async def _handle_signalr_session_closed(self, args: list) -> None:
172175 if session_server :
173176 await session_server .stop ()
174177 if session_server .output :
175- if self ._is_ephemeral :
178+ if self .sandboxed :
176179 self ._session_output = session_server .output
177180 else :
178181 logger .info (
179182 f"Session { session_id } output: { session_server .output } "
180183 )
181- # If this is an ephemeral runtime for a specific session, cancel the execution
182- if self ._is_ephemeral :
184+ # If this is a sandboxed runtime for a specific session, cancel the execution
185+ if self .sandboxed :
183186 self ._cancel_event .set ()
184187
185188 except Exception as e :
@@ -202,8 +205,15 @@ async def _handle_signalr_message(self, args: list) -> None:
202205 if session_id not in self ._session_servers :
203206 # Create and start a new session server
204207 session_server = SessionServer (self ._server , session_id )
208+ try :
209+ await session_server .start ()
210+ except Exception as e :
211+ logger .error (
212+ f"Error starting session server for session { session_id } : { str (e )} "
213+ )
214+ self ._on_session_start_error (session_id )
215+ raise
205216 self ._session_servers [session_id ] = session_server
206- await session_server .start ()
207217
208218 # Get the session server for this session
209219 session_server = self ._session_servers [session_id ]
@@ -223,23 +233,6 @@ async def _handle_signalr_error(self, error: Any) -> None:
223233 async def _handle_signalr_open (self ) -> None :
224234 """Handle SignalR connection open event."""
225235 logger .info ("Websocket connection established." )
226- # If this is an ephemeral runtime we need to start the local MCP session
227- if self ._is_ephemeral :
228- try :
229- # Check if we have a session server for this session_id
230- # Websocket reconnection may occur, so we need to check if the session server already exists
231- if self ._server .session_id not in self ._session_servers :
232- # Create and start a new session server
233- session_server = SessionServer (self ._server , self ._server .session_id )
234- self ._session_servers [self ._server .session_id ] = session_server
235- await session_server .start ()
236- # Get the session server for this session
237- session_server = self ._session_servers [self ._server .session_id ]
238- # Check for existing messages from the connected client
239- await session_server .on_message_received ()
240- except Exception as e :
241- await self ._on_initialization_failure ()
242- logger .error (f"Error starting session server: { str (e )} " )
243236
244237 async def _handle_signalr_close (self ) -> None :
245238 """Handle SignalR connection close event."""
@@ -293,7 +286,7 @@ async def _register(self) -> None:
293286
294287 # Now that we're outside the context managers, check if initialization succeeded
295288 if not initialization_successful :
296- await self ._on_initialization_failure ()
289+ await self ._on_runtime_abort ()
297290 error_message = "The server process failed to initialize. Verify environment variables are set correctly."
298291 if server_stderr_output :
299292 error_message += f"\n Server error output:\n { server_stderr_output } "
@@ -312,7 +305,7 @@ async def _register(self) -> None:
312305 "Name" : self ._server .name ,
313306 "Slug" : self ._server .name ,
314307 "Version" : "1.0.0" ,
315- "Type" : 1 if self ._server . session_id else 3 ,
308+ "Type" : 1 if self .sandboxed else 3 ,
316309 },
317310 "tools" : [],
318311 }
@@ -342,19 +335,15 @@ async def _register(self) -> None:
342335 UiPathErrorCategory .SYSTEM ,
343336 ) from e
344337
345- async def _on_initialization_failure (self ) -> None :
338+ async def _on_session_start_error (self , session_id : str ) -> None :
346339 """
347340 Sends a dummy initialization failure message to abort the already connected client.
348- Ephemeral runtimes are triggered by new client connections.
341+ Sanboxed runtimes are triggered by new client connections.
349342 """
350-
351- if self ._is_ephemeral is False :
352- return
353-
354343 try :
355344 response = self ._uipath .api_client .request (
356345 "POST" ,
357- f"mcp_/mcp/{ self ._server .name } /out/message?sessionId={ self . _server . session_id } " ,
346+ f"mcp_/mcp/{ self ._server .name } /out/message?sessionId={ session_id } " ,
358347 json = types .JSONRPCResponse (
359348 jsonrpc = "2.0" ,
360349 id = 0 ,
@@ -367,7 +356,7 @@ async def _on_initialization_failure(self) -> None:
367356 )
368357 if response .status_code == 202 :
369358 logger .info (
370- f"Sent outgoing session dispose message to UiPath MCP Server: { self . _server . session_id } "
359+ f"Sent outgoing session dispose message to UiPath MCP Server: { session_id } "
371360 )
372361 else :
373362 logger .error (
@@ -378,12 +367,34 @@ async def _on_initialization_failure(self) -> None:
378367 f"Error sending session dispose signal to UiPath MCP Server: { e } "
379368 )
380369
370+ async def _on_runtime_abort (self ) -> None :
371+ """
372+ Sends a runtime abort signalr to terminate all connected sessions.
373+ """
374+ try :
375+ response = self ._uipath .api_client .request (
376+ "POST" ,
377+ f"mcp_/mcp/{ self ._server .name } /runtime/abort?runtimeId={ self ._runtime_id } "
378+ )
379+ if response .status_code == 202 :
380+ logger .info (
381+ f"Sent runtime abort signal to UiPath MCP Server: { self ._runtime_id } "
382+ )
383+ else :
384+ logger .error (
385+ f"Error sending runtime abort signalr to UiPath MCP Server: { response .status_code } - { response .text } "
386+ )
387+ except Exception as e :
388+ logger .error (
389+ f"Error sending runtime abort signal to UiPath MCP Server: { e } "
390+ )
391+
381392 @property
382- def _is_ephemeral (self ) -> bool :
393+ def sandboxed (self ) -> bool :
383394 """
384- Check if the runtime is ephemeral (created on-demand for a single agent execution).
395+ Check if the runtime is sandboxed (created on-demand for a single agent execution).
385396
386397 Returns:
387- bool: True if this is an ephemeral runtime (has a session_id ), False otherwise.
398+ bool: True if this is an sandboxed runtime (has a job_id ), False otherwise.
388399 """
389- return self ._server . session_id is not None
400+ return self .context . job_id is not None
0 commit comments