Skip to content

Commit d6f4ff9

Browse files
committed
fix: refactor, namings, private vars
1 parent 449140d commit d6f4ff9

File tree

4 files changed

+173
-229
lines changed

4 files changed

+173
-229
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.47"
3+
version = "0.0.48"
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: 90 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,22 @@
2929

3030
class UiPathMcpRuntime(UiPathBaseRuntime):
3131
"""
32-
A runtime class implementing the async context manager protocol.
33-
This allows using the class with 'async with' statements.
32+
A runtime class for hosting UiPath MCP servers.
3433
"""
3534

3635
def __init__(self, context: UiPathMcpRuntimeContext):
3736
super().__init__(context)
3837
self.context: UiPathMcpRuntimeContext = context
39-
self.server: Optional[McpServer] = None
40-
self.signalr_client: Optional[SignalRClient] = None
41-
self.session_servers: Dict[str, SessionServer] = {}
38+
self._server: Optional[McpServer] = None
39+
self._signalr_client: Optional[SignalRClient] = None
40+
self._session_servers: Dict[str, SessionServer] = {}
41+
self._session_outputs: Dict[str, str] = {}
42+
self._cancel_event = asyncio.Event()
4243
self._uipath = UiPath()
4344

4445
async def execute(self) -> Optional[UiPathRuntimeResult]:
4546
"""
46-
Start the runtime and connect to SignalR.
47+
Start the MCP Server runtime.
4748
4849
Returns:
4950
Dictionary with execution results
@@ -54,46 +55,41 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
5455
await self.validate()
5556

5657
try:
57-
if self.server is None:
58+
if self._server is None:
5859
return None
5960

6061
# Set up SignalR client
61-
signalr_url = f"{os.environ.get('UIPATH_URL')}/mcp_/wsstunnel?slug={self.server.name}&sessionId={self.server.session_id}"
62+
signalr_url = f"{os.environ.get('UIPATH_URL')}/mcp_/wsstunnel?slug={self._server.name}&sessionId={self._server.session_id}"
6263

63-
self.cancel_event = asyncio.Event()
64-
65-
with tracer.start_as_current_span(self.server.name) as root_span:
66-
root_span.set_attribute("session_id", self.server.session_id)
67-
root_span.set_attribute("command", self.server.command)
68-
root_span.set_attribute("args", self.server.args)
64+
with tracer.start_as_current_span(self._server.name) as root_span:
65+
root_span.set_attribute("session_id", self._server.session_id)
66+
root_span.set_attribute("command", self._server.command)
67+
root_span.set_attribute("args", self._server.args)
6968
root_span.set_attribute("span_type", "MCP Server")
70-
self.signalr_client = SignalRClient(
69+
self._signalr_client = SignalRClient(
7170
signalr_url,
7271
headers={
7372
"X-UiPath-Internal-TenantId": self.context.trace_context.tenant_id,
7473
"X-UiPath-Internal-AccountId": self.context.trace_context.org_id,
7574
},
7675
)
77-
self.signalr_client.on("MessageReceived", self.handle_signalr_message)
78-
self.signalr_client.on(
79-
"SessionClosed", self.handle_signalr_session_closed
76+
self._signalr_client.on("MessageReceived", self._handle_signalr_message)
77+
self._signalr_client.on(
78+
"SessionClosed", self._handle_signalr_session_closed
8079
)
81-
self.signalr_client.on_error(self.handle_signalr_error)
82-
self.signalr_client.on_open(self.handle_signalr_open)
83-
self.signalr_client.on_close(self.handle_signalr_close)
80+
self._signalr_client.on_error(self._handle_signalr_error)
81+
self._signalr_client.on_open(self._handle_signalr_open)
82+
self._signalr_client.on_close(self._handle_signalr_close)
8483

85-
# Register the server with UiPath MCP Server
84+
# Register the local server with UiPath MCP Server
8685
await self._register()
8786

88-
# Keep the runtime alive
89-
# Start SignalR client and keep it running (this is a blocking call)
90-
logger.info("Starting websocket client...")
91-
92-
run_task = asyncio.create_task(self.signalr_client.run())
87+
run_task = asyncio.create_task(self._signalr_client.run())
9388

9489
# Set up a task to wait for cancellation
95-
cancel_task = asyncio.create_task(self.cancel_event.wait())
90+
cancel_task = asyncio.create_task(self._cancel_event.wait())
9691

92+
# Keep the runtime alive
9793
# Wait for either the run to complete or cancellation
9894
done, pending = await asyncio.wait(
9995
[run_task, cancel_task], return_when=asyncio.FIRST_COMPLETED
@@ -103,24 +99,14 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
10399
for task in pending:
104100
task.cancel()
105101

106-
session_outputs = {}
107-
for session_id, session_server in self.session_servers.items():
108-
try:
109-
await session_server.cleanup()
110-
stderr_output = session_server.get_server_stderr()
111-
if stderr_output:
112-
session_outputs[session_id] = stderr_output
113-
except Exception as e:
114-
logger.error(f"Error stopping session {session_id}: {str(e)}")
115-
116102
output_result = {}
117-
if len(session_outputs) == 1:
118-
# If there's only one session, use a single "output" key
119-
first_session_id = next(iter(session_outputs))
120-
output_result["content"] = session_outputs[first_session_id]
121-
elif session_outputs:
122-
# If there are multiple sessions, use the sessionId as the key
123-
output_result = session_outputs
103+
if len(self._session_outputs) == 1:
104+
# If there's only one session, use a single "content" key
105+
single_session_id = next(iter(self._session_outputs))
106+
output_result["content"] = self._session_outputs[single_session_id]
107+
elif self._session_outputs:
108+
# If there are multiple sessions, use the session_id as the key
109+
output_result = self._session_outputs
124110

125111
self.context.result = UiPathRuntimeResult(output=output_result)
126112

@@ -129,55 +115,72 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
129115
except Exception as e:
130116
if isinstance(e, UiPathMcpRuntimeError):
131117
raise
132-
133118
detail = f"Error: {str(e)}"
134-
135119
raise UiPathMcpRuntimeError(
136120
"EXECUTION_ERROR",
137121
"MCP Runtime execution failed",
138122
detail,
139123
UiPathErrorCategory.USER,
140124
) from e
141-
142125
finally:
143126
wait_for_tracers()
144127

145128
async def validate(self) -> None:
146129
"""Validate runtime inputs and load MCP server configuration."""
147-
self.server = self.context.config.get_server(self.context.entrypoint)
148-
if not self.server:
130+
self._server = self.context.config.get_server(self.context.entrypoint)
131+
if not self._server:
149132
raise UiPathMcpRuntimeError(
150133
"SERVER_NOT_FOUND",
151134
"MCP server not found",
152135
f"Server '{self.context.entrypoint}' not found in configuration",
153136
UiPathErrorCategory.DEPLOYMENT,
154137
)
155138

156-
async def handle_signalr_session_closed(self, args: list) -> None:
139+
async def cleanup(self) -> None:
140+
"""Clean up all resources."""
141+
if self._signalr_client and hasattr(self._signalr_client, "_transport"):
142+
transport = self._signalr_client._transport
143+
if transport and hasattr(transport, "_ws") and transport._ws:
144+
try:
145+
await transport._ws.close()
146+
except Exception as e:
147+
logger.error(f"Error closing SignalR WebSocket: {str(e)}")
148+
149+
# Add a small delay to allow the server to shut down gracefully
150+
if sys.platform == "win32":
151+
await asyncio.sleep(0.1)
152+
153+
async def _handle_signalr_session_closed(self, args: list) -> None:
157154
"""
158155
Handle session closed by server.
159156
"""
160157
if len(args) < 1:
161-
logger.error(f"Received invalid SignalR message arguments: {args}")
158+
logger.error(f"Received invalid websocket message arguments: {args}")
162159
return
163160

164161
session_id = args[0]
165162

166163
logger.info(f"Received closed signal for session {session_id}")
167164

168165
try:
169-
self.cancel_event.set()
166+
session_server = self._session_servers.pop(session_id, None)
167+
if session_server:
168+
await session_server.cleanup()
169+
if session_server.output:
170+
self._session_outputs[session_id] = session_server.output
171+
172+
if len(self._session_servers) == 0:
173+
self._cancel_event.set()
170174

171175
except Exception as e:
172176
logger.error(f"Error terminating session {session_id}: {str(e)}")
173177

174-
async def handle_signalr_message(self, args: list) -> None:
178+
async def _handle_signalr_message(self, args: list) -> None:
175179
"""
176180
Handle incoming SignalR messages.
177-
The SignalR client will call this with the arguments from the server.
178181
"""
179182
if len(args) < 1:
180-
logger.error(f"Received invalid SignalR message arguments: {args}")
183+
logger.error(f"Received invalid websocket message arguments: {args}")
181184
return
182185

183186
session_id = args[0]
@@ -186,50 +189,50 @@ async def handle_signalr_message(self, args: list) -> None:
186189

187190
try:
188191
# Check if we have a session server for this session_id
189-
if session_id not in self.session_servers:
192+
if session_id not in self._session_servers:
190193
# Create and start a new session server
191-
session_server = SessionServer(self.server, session_id)
192-
self.session_servers[session_id] = session_server
194+
session_server = SessionServer(self._server, session_id)
195+
self._session_servers[session_id] = session_server
193196
await session_server.start()
194197

195198
# Get the session server for this session
196-
session_server = self.session_servers[session_id]
199+
session_server = self._session_servers[session_id]
197200

198201
# Forward the message to the session's MCP server
199-
await session_server.get_incoming_messages()
202+
await session_server.on_message_received()
200203

201204
except Exception as e:
202205
logger.error(
203206
f"Error handling websocket notification for session {session_id}: {str(e)}"
204207
)
205208

206-
async def handle_signalr_error(self, error: Any) -> None:
209+
async def _handle_signalr_error(self, error: Any) -> None:
207210
"""Handle SignalR errors."""
208-
logger.error(f"SignalR error: {error}")
211+
logger.error(f"Websocket error: {error}")
209212

210-
async def handle_signalr_open(self) -> None:
213+
async def _handle_signalr_open(self) -> None:
211214
"""Handle SignalR connection open event."""
212215

213216
logger.info("Websocket connection established.")
214-
if self.server.session_id:
217+
if self._server.session_id:
215218
try:
216-
session_server = SessionServer(self.server, self.server.session_id)
219+
session_server = SessionServer(self._server, self._server.session_id)
217220
await session_server.start()
218-
self.session_servers[self.server.session_id] = session_server
219-
await session_server.get_incoming_messages()
221+
self._session_servers[self._server.session_id] = session_server
222+
await session_server.on_message_received()
220223
except Exception as e:
221-
await self._dispose_session()
224+
await self._on_initialization_failure()
222225
logger.error(f"Error starting session server: {str(e)}")
223226

224-
async def handle_signalr_close(self) -> None:
227+
async def _handle_signalr_close(self) -> None:
225228
"""Handle SignalR connection close event."""
226-
logger.info("SignalR connection closed.")
229+
logger.info("Websocket connection closed.")
227230
# Clean up all session servers when the connection closes
228231
await self.cleanup()
229232

230233
async def _register(self) -> None:
231-
"""Register the MCP server type with UiPath."""
232-
logger.info(f"Registering MCP server type: {self.server.name}")
234+
"""Register the MCP server with UiPath."""
235+
logger.info(f"Registering MCP server: {self._server.name}")
233236

234237
initialization_successful = False
235238
tools_result = None
@@ -238,9 +241,9 @@ async def _register(self) -> None:
238241
try:
239242
# Create a temporary session to get tools
240243
server_params = StdioServerParameters(
241-
command=self.server.command,
242-
args=self.server.args,
243-
env=self.server.env,
244+
command=self._server.command,
245+
args=self._server.args,
246+
env=self._server.env,
244247
)
245248

246249
# Start a temporary stdio client to get tools
@@ -275,7 +278,7 @@ async def _register(self) -> None:
275278

276279
# Now that we're outside the context managers, check if initialization succeeded
277280
if not initialization_successful:
278-
await self._dispose_session()
281+
await self._on_initialization_failure()
279282
error_message = "The server process failed to initialize. Verify environment variables are set correctly."
280283
if server_stderr_output:
281284
error_message += f"\nServer error output:\n{server_stderr_output}"
@@ -291,8 +294,8 @@ async def _register(self) -> None:
291294
try:
292295
client_info = {
293296
"server": {
294-
"Name": self.server.name,
295-
"Slug": self.server.name,
297+
"Name": self._server.name,
298+
"Slug": self._server.name,
296299
"Version": "1.0.0",
297300
"Type": 1,
298301
},
@@ -311,7 +314,7 @@ async def _register(self) -> None:
311314
# Register with UiPath MCP Server
312315
self._uipath.api_client.request(
313316
"POST",
314-
f"mcp_/api/servers-with-tools/{self.server.name}",
317+
f"mcp_/api/servers-with-tools/{self._server.name}",
315318
json=client_info,
316319
)
317320
logger.info("Registered MCP Server type successfully")
@@ -324,25 +327,28 @@ async def _register(self) -> None:
324327
UiPathErrorCategory.SYSTEM,
325328
) from e
326329

327-
async def _dispose_session(self) -> None:
330+
async def _on_initialization_failure(self) -> None:
328331
"""Dispose of the session on the server."""
332+
if self._server.session_id is None:
333+
return
334+
329335
try:
330336
response = self._uipath.api_client.request(
331337
"POST",
332-
f"mcp_/mcp/{self.server.name}/out/message?sessionId={self.server.session_id}",
338+
f"mcp_/mcp/{self._server.name}/out/message?sessionId={self._server.session_id}",
333339
json=types.JSONRPCResponse(
334340
jsonrpc="2.0",
335341
id=0,
336342
result={
337343
"protocolVersion": "initiliaze-failure",
338344
"capabilities": {},
339-
"serverInfo": {"name": self.server.name, "version": "1.0"},
345+
"serverInfo": {"name": self._server.name, "version": "1.0"},
340346
},
341347
).model_dump(),
342348
)
343349
if response.status_code == 202:
344350
logger.info(
345-
f"Sent outgoing session dispose message to UiPath MCP Server: {self.server.session_id}"
351+
f"Sent outgoing session dispose message to UiPath MCP Server: {self._server.session_id}"
346352
)
347353
else:
348354
logger.error(
@@ -352,21 +358,3 @@ async def _dispose_session(self) -> None:
352358
logger.error(
353359
f"Error sending session dispose signal to UiPath MCP Server: {e}"
354360
)
355-
356-
async def cleanup(self) -> None:
357-
"""Clean up all resources."""
358-
logger.info("Cleaning up all resources")
359-
360-
self.session_servers.clear()
361-
362-
if self.signalr_client and hasattr(self.signalr_client, "_transport"):
363-
transport = self.signalr_client._transport
364-
if transport and hasattr(transport, "_ws") and transport._ws:
365-
try:
366-
await transport._ws.close()
367-
except Exception as e:
368-
logger.error(f"Error closing SignalR WebSocket: {str(e)}")
369-
370-
# Add a small delay to allow the server to shut down gracefully
371-
if sys.platform == "win32":
372-
await asyncio.sleep(0.1)

0 commit comments

Comments
 (0)