Skip to content

Commit 1389398

Browse files
committed
chore: fix types and linting errors
1 parent 3ae9762 commit 1389398

File tree

10 files changed

+195
-110
lines changed

10 files changed

+195
-110
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ line-ending = "auto"
6464
plugins = [
6565
"pydantic.mypy"
6666
]
67+
exclude = [
68+
"samples/.*"
69+
]
6770

6871
follow_imports = "silent"
6972
warn_redundant_casts = true

samples/github-slack-agent/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def system_prompt(state: AgentState) -> AgentState:
108108
_This review was generated automatically._
109109
"""
110110

111-
return [{"role": "system", "content": system_message}] + state["messages"]
111+
return [{"role": "system", "content": system_message}] + state[
112+
"messages"
113+
]
112114

113115
agent = create_react_agent(
114116
model,

samples/mcp-functions-agent/builder.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,7 @@ async def validator_agent(state: GraphState) -> GraphState:
180180
**Example use case:** If the function reads files from disk, your setup function should create a temporary folder and write some files into it. Then, test the function against that folder path.
181181
"""
182182

183-
seeder = create_react_agent(
184-
model, tools=tools, prompt=test_case_prompt
185-
)
183+
seeder = create_react_agent(model, tools=tools, prompt=test_case_prompt)
186184

187185
test_result = await seeder.ainvoke(state)
188186

samples/mcp-functions-server/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# Initialize the MCP server
66
mcp = FastMCP("Code Functions MCP Server")
77

8+
89
# Functions registry to track dynamically added code functions
910
class FunctionRegistry:
1011
def __init__(self):

src/uipath_mcp/_cli/_runtime/_context.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ class UiPathServerType(Enum):
5353
SelfHosted (3): Tunnel to externally hosted server
5454
"""
5555

56-
UiPath = 0 # type: int # Processes, Agents, Activities
57-
Command = 1 # type: int # npx, uvx
58-
Coded = 2 # type: int # PackageType.MCPServer
59-
SelfHosted = 3 # type: int # tunnel to externally hosted server
56+
UiPath = 0 # Processes, Agents, Activities
57+
Command = 1 # npx, uvx
58+
Coded = 2 # PackageType.MCPServer
59+
SelfHosted = 3 # tunnel to externally hosted server
6060

6161
@classmethod
6262
def from_string(cls, name: str) -> "UiPathServerType":

src/uipath_mcp/_cli/_runtime/_runtime.py

Lines changed: 122 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import asyncio
2+
import io
23
import json
34
import logging
45
import os
56
import sys
67
import tempfile
78
import uuid
8-
from typing import Any, Dict, Optional
9+
from typing import Any, Dict, List, Optional, cast
910

1011
from httpx import HTTPStatusError
1112
from mcp import ClientSession, StdioServerParameters, stdio_client
12-
from mcp.types import JSONRPCResponse
13+
from mcp.types import JSONRPCResponse, ListToolsResult
1314
from opentelemetry import trace
1415
from opentelemetry.sdk.trace import TracerProvider
1516
from opentelemetry.sdk.trace.export import BatchSpanProcessor
16-
from pysignalr.client import CompletionMessage, SignalRClient
17+
from pysignalr.client import SignalRClient
18+
from pysignalr.messages import CompletionMessage
1719
from uipath import UiPath
1820
from uipath._cli._runtime._contracts import (
1921
UiPathBaseRuntime,
2022
UiPathErrorCategory,
2123
UiPathRuntimeResult,
24+
UiPathTraceContext,
2225
)
2326
from uipath.tracing import LlmOpsHttpExporter
2427

@@ -47,8 +50,75 @@ def __init__(self, context: UiPathMcpRuntimeContext):
4750
self._session_servers: Dict[str, SessionServer] = {}
4851
self._session_output: Optional[str] = None
4952
self._cancel_event = asyncio.Event()
50-
self._keep_alive_task: Optional[asyncio.Task] = None
53+
self._keep_alive_task: Optional[asyncio.Task[None]] = None
5154
self._uipath = UiPath()
55+
self.trace_provider: Optional[TracerProvider] = None
56+
57+
async def validate(self) -> None:
58+
"""Validate runtime inputs and load MCP server configuration."""
59+
if self.context.config is None:
60+
raise UiPathMcpRuntimeError(
61+
"CONFIGURATION_ERROR",
62+
"Missing configuration",
63+
"Configuration is required.",
64+
UiPathErrorCategory.SYSTEM,
65+
)
66+
67+
if self.context.entrypoint is None:
68+
raise UiPathMcpRuntimeError(
69+
"CONFIGURATION_ERROR",
70+
"Missing entrypoint",
71+
"Entrypoint is required.",
72+
UiPathErrorCategory.SYSTEM,
73+
)
74+
75+
self._server = self.context.config.get_server(self.context.entrypoint)
76+
if not self._server:
77+
raise UiPathMcpRuntimeError(
78+
"SERVER_NOT_FOUND",
79+
"MCP server not found",
80+
f"Server '{self.context.entrypoint}' not found in configuration",
81+
UiPathErrorCategory.DEPLOYMENT,
82+
)
83+
84+
def _validate_auth(self) -> None:
85+
"""Validate authentication-related configuration.
86+
87+
Raises:
88+
UiPathMcpRuntimeError: If any required authentication values are missing.
89+
"""
90+
uipath_url = os.environ.get("UIPATH_URL")
91+
if not uipath_url:
92+
raise UiPathMcpRuntimeError(
93+
"CONFIGURATION_ERROR",
94+
"Missing UIPATH_URL environment variable",
95+
"Please run 'uipath auth'.",
96+
UiPathErrorCategory.USER,
97+
)
98+
99+
if not self.context.trace_context:
100+
raise UiPathMcpRuntimeError(
101+
"CONFIGURATION_ERROR",
102+
"Missing trace context",
103+
"Trace context is required for SignalR connection.",
104+
UiPathErrorCategory.SYSTEM,
105+
)
106+
107+
if not self.context.trace_context.tenant_id:
108+
raise UiPathMcpRuntimeError(
109+
"CONFIGURATION_ERROR",
110+
"Missing tenant ID",
111+
"Please run 'uipath auth'.",
112+
UiPathErrorCategory.SYSTEM,
113+
)
114+
115+
if not self.context.trace_context.org_id:
116+
raise UiPathMcpRuntimeError(
117+
"CONFIGURATION_ERROR",
118+
"Missing organization ID",
119+
"Please run 'uipath auth'.",
120+
UiPathErrorCategory.SYSTEM,
121+
)
52122

53123
async def execute(self) -> Optional[UiPathRuntimeResult]:
54124
"""
@@ -71,10 +141,14 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
71141
trace.set_tracer_provider(self.trace_provider)
72142
self.trace_provider.add_span_processor(
73143
BatchSpanProcessor(LlmOpsHttpExporter())
74-
) # type: ignore
144+
)
145+
146+
# Validate authentication configuration
147+
self._validate_auth()
75148

76149
# Set up SignalR client
77-
signalr_url = f"{os.environ.get('UIPATH_URL')}/agenthub_/wsstunnel?slug={self._server.name}&runtimeId={self._runtime_id}"
150+
uipath_url = os.environ.get("UIPATH_URL")
151+
signalr_url = f"{uipath_url}/agenthub_/wsstunnel?slug={self._server.name}&runtimeId={self._runtime_id}"
78152

79153
if not self.context.folder_key:
80154
folder_path = os.environ.get("UIPATH_FOLDER_PATH")
@@ -98,17 +172,22 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
98172

99173
with tracer.start_as_current_span(self.slug) as root_span:
100174
root_span.set_attribute("runtime_id", self._runtime_id)
101-
root_span.set_attribute("command", self._server.command)
102-
root_span.set_attribute("args", self._server.args)
175+
root_span.set_attribute("command", str(self._server.command))
176+
root_span.set_attribute("args", json.dumps(self._server.args))
103177
root_span.set_attribute("span_type", "MCP Server")
178+
trace_context = cast(UiPathTraceContext, self.context.trace_context)
179+
tenant_id = str(trace_context.tenant_id)
180+
org_id = str(trace_context.org_id)
181+
104182
self._signalr_client = SignalRClient(
105183
signalr_url,
106184
headers={
107-
"X-UiPath-Internal-TenantId": self.context.trace_context.tenant_id,
108-
"X-UiPath-Internal-AccountId": self.context.trace_context.org_id,
185+
"X-UiPath-Internal-TenantId": tenant_id,
186+
"X-UiPath-Internal-AccountId": org_id,
109187
"X-UIPATH-FolderKey": self.context.folder_key,
110188
},
111189
)
190+
112191
self._signalr_client.on("MessageReceived", self._handle_signalr_message)
113192
self._signalr_client.on(
114193
"SessionClosed", self._handle_signalr_session_closed
@@ -166,20 +245,9 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
166245
) from e
167246
finally:
168247
await self.cleanup()
169-
if hasattr(self, "trace_provider") and self.trace_provider:
248+
if self.trace_provider:
170249
self.trace_provider.shutdown()
171250

172-
async def validate(self) -> None:
173-
"""Validate runtime inputs and load MCP server configuration."""
174-
self._server = self.context.config.get_server(self.context.entrypoint)
175-
if not self._server:
176-
raise UiPathMcpRuntimeError(
177-
"SERVER_NOT_FOUND",
178-
"MCP server not found",
179-
f"Server '{self.context.entrypoint}' not found in configuration",
180-
UiPathErrorCategory.DEPLOYMENT,
181-
)
182-
183251
async def cleanup(self) -> None:
184252
"""Clean up all resources."""
185253

@@ -210,7 +278,7 @@ async def cleanup(self) -> None:
210278
if sys.platform == "win32":
211279
await asyncio.sleep(0.5)
212280

213-
async def _handle_signalr_session_closed(self, args: list) -> None:
281+
async def _handle_signalr_session_closed(self, args: list[Any]) -> None:
214282
"""
215283
Handle session closed by server.
216284
"""
@@ -240,7 +308,7 @@ async def _handle_signalr_session_closed(self, args: list) -> None:
240308
except Exception as e:
241309
logger.error(f"Error terminating session {session_id}: {str(e)}")
242310

243-
async def _handle_signalr_message(self, args: list) -> None:
311+
async def _handle_signalr_message(self, args: list[Any]) -> None:
244312
"""
245313
Handle incoming SignalR messages.
246314
"""
@@ -254,10 +322,12 @@ async def _handle_signalr_message(self, args: list) -> None:
254322
logger.info(f"Received websocket notification... {session_id}")
255323

256324
try:
325+
server = cast(McpServer, self._server)
326+
257327
# Check if we have a session server for this session_id
258328
if session_id not in self._session_servers:
259329
# Create and start a new session server
260-
session_server = SessionServer(self._server, self.slug, session_id)
330+
session_server = SessionServer(server, self.slug, session_id)
261331
try:
262332
await session_server.start()
263333
except Exception as e:
@@ -293,11 +363,12 @@ async def _handle_signalr_close(self) -> None:
293363

294364
async def _register(self) -> None:
295365
"""Register the MCP server with UiPath."""
366+
server = cast(McpServer, self._server)
296367

297368
initialization_successful = False
298-
tools_result = None
369+
tools_result: Optional[ListToolsResult] = None
299370
server_stderr_output = ""
300-
env_vars = self._server.env
371+
env_vars = dict(server.env)
301372

302373
# if server is Coded, include environment variables
303374
if self.server_type is UiPathServerType.Coded:
@@ -309,14 +380,15 @@ async def _register(self) -> None:
309380
try:
310381
# Create a temporary session to get tools
311382
server_params = StdioServerParameters(
312-
command=self._server.command,
313-
args=self._server.args,
383+
command=server.command,
384+
args=server.args,
314385
env=env_vars,
315386
)
316387

317388
# Start a temporary stdio client to get tools
318389
# Use a temporary file to capture stderr
319-
with tempfile.TemporaryFile(mode="w+b") as stderr_temp:
390+
with tempfile.TemporaryFile(mode="w+b") as stderr_temp_binary:
391+
stderr_temp = io.TextIOWrapper(stderr_temp_binary, encoding="utf-8")
320392
async with stdio_client(server_params, errlog=stderr_temp) as (
321393
read,
322394
write,
@@ -336,11 +408,7 @@ async def _register(self) -> None:
336408
logger.error("Initialization timed out")
337409
# Capture stderr output here, after the timeout
338410
stderr_temp.seek(0)
339-
server_stderr_output = stderr_temp.read().decode(
340-
"utf-8", errors="replace"
341-
)
342-
# We'll handle this after exiting the context managers
343-
# We don't continue with registration here - we'll do it after the context managers
411+
server_stderr_output = stderr_temp.read()
344412

345413
except* Exception as eg:
346414
for e in eg.exceptions:
@@ -366,15 +434,24 @@ async def _register(self) -> None:
366434
# Now continue with registration
367435
logger.info("Registering server runtime ...")
368436
try:
437+
if not tools_result:
438+
raise UiPathMcpRuntimeError(
439+
"INITIALIZATION_ERROR",
440+
"Server initialization failed",
441+
"Failed to get tools list from server",
442+
UiPathErrorCategory.DEPLOYMENT,
443+
)
444+
445+
tools_list: List[Dict[str, str | None]] = []
369446
client_info = {
370447
"server": {
371-
"Id": self.context.server_id,
372448
"Name": self.slug,
373449
"Slug": self.slug,
450+
"Id": self.context.server_id,
374451
"Version": "1.0.0",
375452
"Type": self.server_type.value,
376453
},
377-
"tools": [],
454+
"tools": tools_list,
378455
}
379456

380457
for tool in tools_result.tools:
@@ -386,7 +463,7 @@ async def _register(self) -> None:
386463
if tool.inputSchema
387464
else "{}",
388465
}
389-
client_info["tools"].append(tool_info)
466+
tools_list.append(tool_info)
390467

391468
# Register with UiPath MCP Server
392469
await self._uipath.api_client.request_async(
@@ -418,14 +495,14 @@ async def _on_session_start_error(self, session_id: str) -> None:
418495
try:
419496
response = await self._uipath.api_client.request_async(
420497
"POST",
421-
f"agenthub_/mcp/{self.slug}/out/message?sessionId={session_id}",
498+
f"agenthub_/mcp/{self.slug}/out/message?sessionId={session_id}", # type: ignore
422499
json=JSONRPCResponse(
423500
jsonrpc="2.0",
424501
id=0,
425502
result={
426503
"protocolVersion": "initialize-failure",
427504
"capabilities": {},
428-
"serverInfo": {"name": self.slug, "version": "1.0"},
505+
"serverInfo": {"name": self.slug, "version": "1.0"}, # type: ignore
429506
},
430507
).model_dump(),
431508
)
@@ -475,7 +552,7 @@ async def on_keep_alive_response(
475552
await self._signalr_client.send(
476553
method="OnKeepAlive",
477554
arguments=[],
478-
on_invocation=on_keep_alive_response,
555+
on_invocation=on_keep_alive_response, # type: ignore
479556
)
480557
except Exception as e:
481558
if not self._cancel_event.is_set():
@@ -497,7 +574,7 @@ async def _on_runtime_abort(self) -> None:
497574
try:
498575
response = await self._uipath.api_client.request_async(
499576
"POST",
500-
f"agenthub_/mcp/{self.slug}/runtime/abort?runtimeId={self._runtime_id}",
577+
f"agenthub_/mcp/{self.slug}/runtime/abort?runtimeId={self._runtime_id}", # type: ignore
501578
)
502579
if response.status_code == 202:
503580
logger.info(
@@ -530,7 +607,9 @@ def packaged(self) -> bool:
530607
Returns:
531608
bool: True if this is a packaged runtime (has a process), False otherwise.
532609
"""
533-
process_key = self.context.trace_context.process_key
610+
process_key = None
611+
if self.context.trace_context is not None:
612+
process_key = self.context.trace_context.process_key
534613

535614
return (
536615
process_key is not None

0 commit comments

Comments
 (0)