-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy path_factory.py
More file actions
168 lines (140 loc) · 5.59 KB
/
_factory.py
File metadata and controls
168 lines (140 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""Factory for creating MCP runtime instances."""
import json
import logging
import os
import uuid
from typing import Any
from uipath.runtime import (
UiPathRuntimeContext,
UiPathRuntimeFactorySettings,
UiPathRuntimeProtocol,
)
from uipath.runtime.errors import UiPathErrorCategory
from uipath.runtime.storage import UiPathRuntimeStorageProtocol
from uipath_mcp._cli._runtime._exception import McpErrorCode, UiPathMcpRuntimeError
from uipath_mcp._cli._runtime._runtime import UiPathMcpRuntime
from uipath_mcp._cli._utils._config import McpConfig
logger = logging.getLogger(__name__)
class UiPathMcpRuntimeFactory:
"""Factory for creating MCP runtimes from mcp.json configuration."""
def __init__(
self,
context: UiPathRuntimeContext,
):
"""Initialize the factory.
Args:
context: UiPathRuntimeContext to use for runtime creation.
"""
self.context = context
self._mcp_config: McpConfig | None = None
def _load_mcp_config(self) -> McpConfig:
"""Load mcp.json configuration."""
if self._mcp_config is None:
self._mcp_config = McpConfig()
return self._mcp_config
def discover_entrypoints(self) -> list[str]:
"""Discover all MCP server entrypoints.
Returns:
List of server names that can be used as entrypoints.
"""
mcp_config = self._load_mcp_config()
if not mcp_config.exists:
return []
return mcp_config.get_server_names()
def _mcp_slug(self, entrypoint: str) -> str:
"""Loads the mcp slug from the uipath.config if available, otherwise it will use the entrypoint."""
logger.info(f"Loading slug from config {self.context.config_path}")
if os.path.exists(self.context.config_path):
config: dict[str, Any] = {}
with open(self.context.config_path, "r") as f:
config = json.load(f)
server_slug = (
config.get("runtime", {})
.get("fpsContext", {})
.get("mcpServer.slug", None)
)
if server_slug is not None:
logger.info(f"Server slug in config '{server_slug}'.")
return server_slug
logger.info(
f"No server slug found in config, using entrypoint '{entrypoint}' as slug."
)
return entrypoint
async def new_runtime(
self, entrypoint: str, runtime_id: str, **kwargs: Any
) -> UiPathRuntimeProtocol:
"""Create a new MCP runtime instance.
Args:
entrypoint: Server name from mcp.json.
runtime_id: Unique identifier for the runtime instance.
Returns:
Configured UiPathMcpRuntime instance.
Raises:
UiPathMcpRuntimeError: If configuration is invalid or server not found.
"""
mcp_config = self._load_mcp_config()
if not mcp_config.exists:
raise UiPathMcpRuntimeError(
McpErrorCode.CONFIGURATION_ERROR,
"Invalid configuration",
"mcp.json not found",
UiPathErrorCategory.DEPLOYMENT,
)
server = mcp_config.get_server(entrypoint)
logger.info("Creating MCP runtime for entrypoint '%s'", entrypoint)
logger.info(server)
if not server:
available = ", ".join(mcp_config.get_server_names())
raise UiPathMcpRuntimeError(
McpErrorCode.SERVER_NOT_FOUND,
"MCP server not found",
f"Server '{entrypoint}' not found. Available: {available}",
UiPathErrorCategory.USER,
)
# Validate streamable-http configuration
if server.is_streamable_http:
if not server.url:
raise UiPathMcpRuntimeError(
McpErrorCode.CONFIGURATION_ERROR,
"Invalid configuration",
f"Server '{entrypoint}' uses streamable-http transport but 'url' is not specified in mcp.json",
UiPathErrorCategory.USER,
)
if not server.command or server.command == "None":
raise UiPathMcpRuntimeError(
McpErrorCode.CONFIGURATION_ERROR,
"Invalid configuration",
f"Server '{entrypoint}' uses streamable-http transport but 'command' is not specified in mcp.json",
UiPathErrorCategory.USER,
)
# Validate runtime_id is a valid UUID, generate new one if not
try:
uuid.UUID(runtime_id)
except ValueError:
new_id = str(uuid.uuid4())
logger.warning(
"Invalid runtime_id '%s' is not a valid UUID; generated '%s'",
runtime_id,
new_id,
)
runtime_id = new_id
return UiPathMcpRuntime(
server=server,
runtime_id=runtime_id,
entrypoint=entrypoint,
folder_key=self.context.folder_key,
server_id=self.context.mcp_server_id,
server_slug=self._mcp_slug(entrypoint),
)
async def get_storage(self) -> UiPathRuntimeStorageProtocol | None:
"""Get factory storage.
MCP servers are long-running processes and don't need
cross-invocation state persistence.
"""
return None
async def get_settings(self) -> UiPathRuntimeFactorySettings | None:
"""Get factory settings."""
return None
async def dispose(self) -> None:
"""Cleanup factory resources."""
self._mcp_config = None