Skip to content

Commit 1c99ed5

Browse files
committed
fix: switch to runtime_id
1 parent eb3dfd9 commit 1c99ed5

File tree

3 files changed

+54
-45
lines changed

3 files changed

+54
-45
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-mcp"
3-
version = "0.0.52"
3+
version = "0.0.53"
44
description = "UiPath MCP SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath_mcp/_cli/_runtime/_runtime.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import sys
55
import tempfile
6+
import uuid
67
from typing import Any, Dict, Optional
78

89
import 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"\nServer 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

src/uipath_mcp/_cli/_utils/_config.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
class McpServer:
99
"""Model representing an MCP server configuration."""
1010

11-
def __init__(self, name: str, server_config: Dict[str, Any], session_id: Optional[str] = None):
11+
def __init__(self, name: str, server_config: Dict[str, Any], ):
1212
self.name = name
13-
self.session_id = session_id
1413
self.type = server_config.get("type")
1514
self.command = server_config.get("command")
1615
self.args = server_config.get("args", [])
@@ -57,9 +56,8 @@ def _load_config(self) -> None:
5756
self._raw_config = json.load(f)
5857

5958
servers_config = self._raw_config.get("servers", {})
60-
self._session_id = self._raw_config.get("sessionId")
6159
self._servers = {
62-
name: McpServer(name, config, self._session_id)
60+
name: McpServer(name, config)
6361
for name, config in servers_config.items()
6462
}
6563

0 commit comments

Comments
 (0)