Skip to content

Commit 3b944c3

Browse files
committed
fix: add keep alive heartbeat
1 parent 4a9a421 commit 3b944c3

File tree

2 files changed

+37
-5
lines changed

2 files changed

+37
-5
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.64"
3+
version = "0.0.65"
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: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import mcp.types as types
1010
from mcp import ClientSession, StdioServerParameters
1111
from opentelemetry import trace
12-
from pysignalr.client import SignalRClient
12+
from pysignalr.client import CompletionMessage, SignalRClient
1313
from uipath import UiPath
1414
from uipath._cli._runtime._contracts import (
1515
UiPathBaseRuntime,
@@ -42,6 +42,7 @@ def __init__(self, context: UiPathMcpRuntimeContext):
4242
self._session_servers: Dict[str, SessionServer] = {}
4343
self._session_output: Optional[str] = None
4444
self._cancel_event = asyncio.Event()
45+
self._keep_alive_task: Optional[asyncio.Task] = None
4546
self._uipath = UiPath()
4647

4748
async def execute(self) -> Optional[UiPathRuntimeResult]:
@@ -91,6 +92,8 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
9192
# Set up a task to wait for cancellation
9293
cancel_task = asyncio.create_task(self._cancel_event.wait())
9394

95+
self._keep_alive_task = asyncio.create_task(self._keep_alive())
96+
9497
# Keep the runtime alive
9598
# Wait for either the run to complete or cancellation
9699
done, pending = await asyncio.wait(
@@ -138,6 +141,13 @@ async def cleanup(self) -> None:
138141

139142
await self._on_runtime_abort()
140143

144+
if self._keep_alive_task:
145+
self._keep_alive_task.cancel()
146+
try:
147+
await self._keep_alive_task
148+
except asyncio.CancelledError:
149+
pass
150+
141151
for session_id, session_server in self._session_servers.items():
142152
try:
143153
await session_server.stop()
@@ -276,9 +286,6 @@ async def _register(self) -> None:
276286
stderr_temp.seek(0)
277287
server_stderr_output = stderr_temp.read().decode('utf-8', errors='replace')
278288
# We'll handle this after exiting the context managers
279-
logger.info("Exiting client session context")
280-
logger.info("Exiting stdio client context")
281-
logger.info("Exiting temporary file context")
282289
# We don't continue with registration here - we'll do it after the context managers
283290

284291
except BaseException as e:
@@ -369,6 +376,31 @@ async def _on_session_start_error(self, session_id: str) -> None:
369376
f"Error sending session dispose signal to UiPath MCP Server: {e}"
370377
)
371378

379+
async def _keep_alive(self) -> None:
380+
"""
381+
Heartbeat to keep the runtime available.
382+
"""
383+
while True:
384+
try:
385+
async def on_keep_alive_response(response: CompletionMessage) -> None:
386+
session_ids = response.result
387+
logger.info(f"Active sessions: {session_ids}")
388+
# If there are no active sessions and this is a sandbox environment
389+
# We need to cancel the runtime
390+
# eg: when user kills the agent that triggered the runtime, before we subscribe to events
391+
if not session_ids and self.sandboxed and not self._cancel_event.is_set():
392+
logger.error("No active sessions, cancelling sandboxed runtime...")
393+
self._cancel_event.set()
394+
await self._signalr_client.send(
395+
method="OnKeepAlive",
396+
arguments=[],
397+
on_invocation=on_keep_alive_response
398+
)
399+
except Exception as e:
400+
logger.error(f"Error during keep-alive: {e}")
401+
await asyncio.sleep(60)
402+
403+
372404
async def _on_runtime_abort(self) -> None:
373405
"""
374406
Sends a runtime abort signalr to terminate all connected sessions.

0 commit comments

Comments
 (0)