11import asyncio
22import logging
3+ import os
34import sys
4- from typing import Optional
5+ from typing import Any , Dict , Optional
56
67from mcp import ClientSession , StdioServerParameters
78from mcp .client .stdio import stdio_client
9+ from pysignalr .client import SignalRClient
810from uipath import UiPath
911from uipath ._cli ._runtime ._contracts import (
1012 UiPathBaseRuntime ,
1517from .._utils ._config import McpServer
1618from ._context import UiPathMcpRuntimeContext
1719from ._exception import UiPathMcpRuntimeError
20+ from ._session import SessionServer
1821
1922logger = logging .getLogger (__name__ )
2023
@@ -29,69 +32,150 @@ def __init__(self, context: UiPathMcpRuntimeContext):
2932 super ().__init__ (context )
3033 self .context : UiPathMcpRuntimeContext = context
3134 self .server : Optional [McpServer ] = None
35+ self .signalr_client : Optional [SignalRClient ] = None
36+ self .session_servers : Dict [str , SessionServer ] = {}
3237
3338 async def execute (self ) -> Optional [UiPathRuntimeResult ]:
3439 """
35- Start the MCP server .
40+ Start the runtime and connect to SignalR .
3641
3742 Returns:
3843 Dictionary with execution results
3944
4045 Raises:
4146 UiPathMcpRuntimeError: If execution fails
4247 """
43-
4448 await self .validate ()
4549
4650 try :
47-
4851 if self .server is None :
4952 return None
5053
51- server_params = StdioServerParameters (
52- command = self .server .command ,
53- args = self .server .args ,
54- env = None ,
54+ # Set up SignalR client
55+ signalr_url = (
56+ f"{ os .environ .get ('UIPATH_URL' )} /mcp_/wsstunnel?slug={ self .server .name } "
5557 )
5658
57- print ( f"Starting MCP server.. { self .server . command } { self . server . args } " )
58- async with stdio_client ( server_params ) as ( read , write ):
59- async with ClientSession (
60- read , write
61- ) as session :
59+ self .signalr_client = SignalRClient ( signalr_url )
60+ self . signalr_client . on ( "MessageReceived" , self . handle_signalr_message )
61+ self . signalr_client . on_error ( self . handle_signalr_error )
62+ self . signalr_client . on_open ( self . handle_signalr_open )
63+ self . signalr_client . on_close ( self . handle_signalr_close )
6264
63- print ("Connected to MCP server" )
64- # Initialize the connection
65- await session .initialize ()
66- print ("MCP server initialized" )
67- # List available prompts
68- #prompts = await session.list_prompts()
65+ # Register the server with UiPath MCP Server
66+ await self ._register ()
6967
70- # Get a prompt
71- #prompt = await session.get_prompt(
72- # "example-prompt", arguments={"arg1": "value"}
73- # )
68+ # Keep the runtime alive
69+ # Start SignalR client and keep it running (this is a blocking call)
70+ logger . info ( "Starting SignalR client..." )
71+ await self . signalr_client . run ( )
7472
75- # List available resources
76- #resources = await session.list_resources()
73+ return UiPathRuntimeResult ()
7774
78- # List available tools
79- toolsResult = await session .list_tools ()
75+ except Exception as e :
76+ if isinstance (e , UiPathMcpRuntimeError ):
77+ raise
8078
81- print ( toolsResult )
79+ detail = f"Error: { str ( e ) } "
8280
83- # Register with UiPath MCP Server
81+ raise UiPathMcpRuntimeError (
82+ "EXECUTION_ERROR" ,
83+ "MCP Runtime execution failed" ,
84+ detail ,
85+ UiPathErrorCategory .USER ,
86+ ) from e
87+
88+ finally :
89+ await self .cleanup ()
90+
91+ async def validate (self ) -> None :
92+ """Validate runtime inputs and load MCP server configuration."""
93+ self .server = self .context .config .get_server (self .context .entrypoint )
94+ if not self .server :
95+ raise UiPathMcpRuntimeError (
96+ "SERVER_NOT_FOUND" ,
97+ "MCP server not found" ,
98+ f"Server '{ self .context .entrypoint } ' not found in configuration" ,
99+ UiPathErrorCategory .DEPLOYMENT ,
100+ )
101+
102+ async def handle_signalr_message (self , args : list ) -> None :
103+ """
104+ Handle incoming SignalR messages.
105+ The SignalR client will call this with the arguments from the server.
106+ """
107+ if len (args ) < 2 :
108+ logger .error (f"Received invalid SignalR message arguments: { args } " )
109+ return
110+
111+ session_id = args [0 ]
112+ message = args [1 ]
113+
114+ logger .info (f"Received message for session { session_id } : { message } " )
115+
116+ try :
117+ # Check if we have a session server for this session_id
118+ if session_id not in self .session_servers :
119+ # Create and start a new session server
120+ session_server = SessionServer (self .server , session_id )
121+ self .session_servers [session_id ] = session_server
122+ await session_server .start (self .signalr_client )
123+
124+ # Get the session server for this session
125+ session_server = self .session_servers [session_id ]
126+
127+ # Forward the message to the session's MCP server
128+ await session_server .send_message (message )
129+
130+ except Exception as e :
131+ logger .error (
132+ f"Error handling SignalR message for session { session_id } : { str (e )} "
133+ )
134+
135+ async def handle_signalr_error (self , error : Any ) -> None :
136+ """Handle SignalR errors."""
137+ logger .error (f"SignalR error: { error } " )
138+
139+ async def handle_signalr_open (self ) -> None :
140+ """Handle SignalR connection open event."""
141+ logger .info ("SignalR connection established" )
142+
143+ async def handle_signalr_close (self ) -> None :
144+ """Handle SignalR connection close event."""
145+ logger .info ("SignalR connection closed" )
146+
147+ # Clean up all session servers when the connection closes
148+ await self .cleanup ()
149+
150+ async def _register (self ) -> None :
151+ """Register the MCP server type with UiPath."""
152+ logger .info (f"Registering MCP server type: { self .server .name } " )
153+
154+ try :
155+ # Create a temporary session to get tools
156+ server_params = StdioServerParameters (
157+ command = self .server .command ,
158+ args = self .server .args ,
159+ env = None ,
160+ )
161+
162+ # Start a temporary stdio client to get tools
163+ async with stdio_client (server_params ) as (read , write ):
164+ async with ClientSession (read , write ) as session :
165+ await session .initialize ()
166+ tools_result = await session .list_tools ()
167+ print (tools_result )
84168 client_info = {
85169 "server" : {
86170 "Name" : self .server .name ,
87171 "Slug" : self .server .name ,
88172 "Version" : "1.0.0" ,
89- "Type" : 1
173+ "Type" : 1 ,
90174 },
91- "tools" : []
175+ "tools" : [],
92176 }
93177
94- for tool in toolsResult .tools :
178+ for tool in tools_result .tools :
95179 tool_info = {
96180 "Type" : 1 ,
97181 "Name" : tool .name ,
@@ -100,59 +184,39 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
100184 }
101185 client_info ["tools" ].append (tool_info )
102186
103- print (client_info )
104- print ("Registering client..." )
105-
187+ # Register with UiPath MCP Server
106188 uipath = UiPath ()
107- sseUrl : str
108- try :
109- response = uipath .api_client .request (
110- "POST" ,
111- f"/mcp_/api/servers-with-tools/{ self .server .name } " ,
112- json = client_info ,
113- )
114- #data = response.json()
115- #sseUrl = data.get("url")
116- print ("Registered client successfully" )
117- except Exception as e :
118- raise UiPathMcpRuntimeError (
119- "NETWORK_ERROR" ,
120- "Failed to register with UiPath MCP Server" ,
121- str (e ),
122- UiPathErrorCategory .SYSTEM ,
123- ) from e
124-
125- return UiPathRuntimeResult ()
189+ uipath .api_client .request (
190+ "POST" ,
191+ f"mcp_/api/servers-with-tools/{ self .server .name } " ,
192+ json = client_info ,
193+ )
194+ logger .info ("Registered MCP Server type successfully" )
126195
127196 except Exception as e :
128- if isinstance (e , UiPathMcpRuntimeError ):
129- raise
130-
131- detail = f"Error: { str (e )} "
132-
133197 raise UiPathMcpRuntimeError (
134- "EXECUTION_ERROR " ,
135- "MCP Server execution failed " ,
136- detail ,
137- UiPathErrorCategory .USER ,
198+ "NETWORK_ERROR " ,
199+ "Failed to register with UiPath MCP Server " ,
200+ str ( e ) ,
201+ UiPathErrorCategory .SYSTEM ,
138202 ) from e
139203
140- finally :
141- # Add a small delay to allow the server to shut down gracefully
142- if sys .platform == 'win32' :
143- await asyncio .sleep (0.1 )
204+ async def cleanup (self ) -> None :
205+ """Clean up all resources."""
206+ logger .info ("Cleaning up all resources" )
144207
145- async def validate ( self ) -> None :
146- """Validate runtime inputs."""
147- """Load and validate the MCP server configuration ."""
148- self . server = self . context . config . get_server ( self . context . entrypoint )
149- if not self . server :
150- raise UiPathMcpRuntimeError (
151- "SERVER_NOT_FOUND" ,
152- "MCP server not found" ,
153- f"Server ' { self . context . entrypoint } ' not found in configuration" ,
154- UiPathErrorCategory . DEPLOYMENT ,
155- )
208+ # Clean up all session servers
209+ for session_id , session_server in list ( self . session_servers . items ()):
210+ try :
211+ await session_server . cleanup ( )
212+ except Exception as e :
213+ logger . error ( f"Error cleaning up session { session_id } : { str ( e ) } " )
214+
215+ self . session_servers . clear ()
216+
217+ # Close SignalR connection
218+ # self.signalr_client
156219
157- async def cleanup (self ):
158- pass
220+ # Add a small delay to allow the server to shut down gracefully
221+ if sys .platform == "win32" :
222+ await asyncio .sleep (0.1 )
0 commit comments