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