Skip to content

Commit a927510

Browse files
authored
Merge pull request #91 from UiPath/fix/proper_server_type_registration
fix: proper server type registration
2 parents 7af16ff + 9af5594 commit a927510

File tree

3 files changed

+98
-16
lines changed

3 files changed

+98
-16
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.77"
3+
version = "0.0.78"
44
description = "UiPath MCP SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath_mcp/_cli/_runtime/_context.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from typing import Optional
23

34
from uipath._cli._runtime._contracts import UiPathRuntimeContext
@@ -7,4 +8,43 @@
78

89
class UiPathMcpRuntimeContext(UiPathRuntimeContext):
910
"""Context information passed throughout the runtime execution."""
11+
1012
config: Optional[McpConfig] = None
13+
14+
15+
class UiPathServerType(Enum):
16+
"""Defines the different types of UiPath servers used in the MCP ecosystem.
17+
18+
This enum is used to identify and configure the behavior of different server types
19+
during runtime registration and execution.
20+
21+
Attributes:
22+
UiPath (0): Standard UiPath server for Processes, Agents, and Activities
23+
External (1): External server types like npx, uvx
24+
Local (2): Local MCP server (PackageType.MCPServer)
25+
Hosted (3): Tunnel to externally hosted server
26+
"""
27+
28+
UiPath = 0 # type: int # Processes, Agents, Activities
29+
External = 1 # type: int # npx, uvx
30+
Local = 2 # type: int # PackageType.MCPServer
31+
Hosted = 3 # type: int # tunnel to externally hosted server
32+
33+
@classmethod
34+
def from_string(cls, name: str) -> "UiPathServerType":
35+
"""Get enum value from string name."""
36+
try:
37+
return cls[name]
38+
except KeyError as e:
39+
raise ValueError(f"Unknown server type: {name}") from e
40+
41+
@classmethod
42+
def get_description(cls, server_type: "UiPathServerType") -> str:
43+
"""Get description for a server type."""
44+
descriptions = {
45+
cls.UiPath: "Standard UiPath server for Processes, Agents, and Activities",
46+
cls.External: "External server types like npx, uvx",
47+
cls.Local: "Local MCP server (PackageType.MCPServer)",
48+
cls.Hosted: "Tunnel to externally hosted server",
49+
}
50+
return descriptions.get(server_type, "Unknown server type")

src/uipath_mcp/_cli/_runtime/_runtime.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from uipath.tracing import wait_for_tracers
2020

2121
from .._utils._config import McpServer
22-
from ._context import UiPathMcpRuntimeContext
22+
from ._context import UiPathMcpRuntimeContext, UiPathServerType
2323
from ._exception import UiPathMcpRuntimeError
2424
from ._session import SessionServer
2525
from ._stdio_client import stdio_client
@@ -37,7 +37,9 @@ def __init__(self, context: UiPathMcpRuntimeContext):
3737
super().__init__(context)
3838
self.context: UiPathMcpRuntimeContext = context
3939
self._server: Optional[McpServer] = None
40-
self._runtime_id = self.context.job_id if self.context.job_id else str(uuid.uuid4())
40+
self._runtime_id = (
41+
self.context.job_id if self.context.job_id else str(uuid.uuid4())
42+
)
4143
self._signalr_client: Optional[SignalRClient] = None
4244
self._session_servers: Dict[str, SessionServer] = {}
4345
self._session_output: Optional[str] = None
@@ -152,9 +154,7 @@ async def cleanup(self) -> None:
152154
try:
153155
await session_server.stop()
154156
except Exception as e:
155-
logger.error(
156-
f"Error cleaning up session server {session_id}: {str(e)}"
157-
)
157+
logger.error(f"Error cleaning up session server {session_id}: {str(e)}")
158158

159159
if self._signalr_client and hasattr(self._signalr_client, "_transport"):
160160
transport = self._signalr_client._transport
@@ -265,7 +265,7 @@ async def _register(self) -> None:
265265

266266
# Start a temporary stdio client to get tools
267267
# Use a temporary file to capture stderr
268-
with tempfile.TemporaryFile(mode='w+b') as stderr_temp:
268+
with tempfile.TemporaryFile(mode="w+b") as stderr_temp:
269269
async with stdio_client(server_params, errlog=stderr_temp) as (
270270
read,
271271
write,
@@ -285,7 +285,9 @@ async def _register(self) -> None:
285285
logger.error("Initialization timed out")
286286
# Capture stderr output here, after the timeout
287287
stderr_temp.seek(0)
288-
server_stderr_output = stderr_temp.read().decode('utf-8', errors='replace')
288+
server_stderr_output = stderr_temp.read().decode(
289+
"utf-8", errors="replace"
290+
)
289291
# We'll handle this after exiting the context managers
290292
# We don't continue with registration here - we'll do it after the context managers
291293

@@ -314,14 +316,13 @@ async def _register(self) -> None:
314316
"Name": self._server.name,
315317
"Slug": self._server.name,
316318
"Version": "1.0.0",
317-
"Type": 1 if self.sandboxed else 3,
319+
"Type": self.server_type.value,
318320
},
319321
"tools": [],
320322
}
321323

322324
for tool in tools_result.tools:
323325
tool_info = {
324-
"Type": 1,
325326
"Name": tool.name,
326327
"ProcessType": "Tool",
327328
"Description": tool.description,
@@ -347,7 +348,7 @@ async def _register(self) -> None:
347348
async def _on_session_start_error(self, session_id: str) -> None:
348349
"""
349350
Sends a dummy initialization failure message to abort the already connected client.
350-
Sanboxed runtimes are triggered by new client connections.
351+
Sandboxed runtimes are triggered by new client connections.
351352
"""
352353
try:
353354
response = await self._uipath.api_client.request_async(
@@ -382,6 +383,7 @@ async def _keep_alive(self) -> None:
382383
"""
383384
while not self._cancel_event.is_set():
384385
try:
386+
385387
async def on_keep_alive_response(response: CompletionMessage) -> None:
386388
if response.error:
387389
logger.error(f"Error during keep-alive: {response.error}")
@@ -391,27 +393,33 @@ async def on_keep_alive_response(response: CompletionMessage) -> None:
391393
# If there are no active sessions and this is a sandbox environment
392394
# We need to cancel the runtime
393395
# eg: when user kills the agent that triggered the runtime, before we subscribe to events
394-
if not session_ids and self.sandboxed and not self._cancel_event.is_set():
395-
logger.error("No active sessions, cancelling sandboxed runtime...")
396+
if (
397+
not session_ids
398+
and self.sandboxed
399+
and not self._cancel_event.is_set()
400+
):
401+
logger.error(
402+
"No active sessions, cancelling sandboxed runtime..."
403+
)
396404
self._cancel_event.set()
405+
397406
await self._signalr_client.send(
398407
method="OnKeepAlive",
399408
arguments=[],
400-
on_invocation=on_keep_alive_response
409+
on_invocation=on_keep_alive_response,
401410
)
402411
except Exception as e:
403412
logger.error(f"Error during keep-alive: {e}")
404413
await asyncio.sleep(60)
405414

406-
407415
async def _on_runtime_abort(self) -> None:
408416
"""
409417
Sends a runtime abort signalr to terminate all connected sessions.
410418
"""
411419
try:
412420
response = await self._uipath.api_client.request_async(
413421
"POST",
414-
f"mcp_/mcp/{self._server.name}/runtime/abort?runtimeId={self._runtime_id}"
422+
f"mcp_/mcp/{self._server.name}/runtime/abort?runtimeId={self._runtime_id}",
415423
)
416424
if response.status_code == 202:
417425
logger.info(
@@ -435,3 +443,37 @@ def sandboxed(self) -> bool:
435443
bool: True if this is an sandboxed runtime (has a job_id), False otherwise.
436444
"""
437445
return self.context.job_id is not None
446+
447+
@property
448+
def packaged(self) -> bool:
449+
"""
450+
Check if the runtime is packaged (PackageType.MCPServer).
451+
452+
Returns:
453+
bool: True if this is a packaged runtime (has a process), False otherwise.
454+
"""
455+
process_key = self.context.trace_context.process_key
456+
457+
return (
458+
process_key is not None
459+
and process_key != "00000000-0000-0000-0000-000000000000"
460+
)
461+
462+
@property
463+
def server_type(self) -> UiPathServerType:
464+
"""
465+
Determine the correct UiPathServerType for this runtime.
466+
467+
Returns:
468+
UiPathServerType: The appropriate server type enum value based on the runtime configuration.
469+
"""
470+
if self.packaged:
471+
# If it's a packaged runtime (has a process_key), it's a Local server
472+
# Packaged runtimes are also sandboxed
473+
return UiPathServerType.Local
474+
elif self.sandboxed:
475+
# If it's sandboxed but not packaged, it's an External server
476+
return UiPathServerType.External
477+
else:
478+
# If it's neither packaged nor sandboxed, it's a Hosted server
479+
return UiPathServerType.Hosted

0 commit comments

Comments
 (0)