Skip to content

Commit 851f30b

Browse files
committed
chore: fix types and linting errors
1 parent b02fbea commit 851f30b

File tree

10 files changed

+199
-113
lines changed

10 files changed

+199
-113
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-mcp"
3-
version = "0.0.96"
3+
version = "0.0.97"
44
description = "UiPath MCP SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -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
@@ -25,10 +25,10 @@ class UiPathServerType(Enum):
2525
SelfHosted (3): Tunnel to externally hosted server
2626
"""
2727

28-
UiPath = 0 # type: int # Processes, Agents, Activities
29-
Command = 1 # type: int # npx, uvx
30-
Coded = 2 # type: int # PackageType.MCPServer
31-
SelfHosted = 3 # type: int # tunnel to externally hosted server
28+
UiPath = 0 # Processes, Agents, Activities
29+
Command = 1 # npx, uvx
30+
Coded = 2 # PackageType.MCPServer
31+
SelfHosted = 3 # tunnel to externally hosted server
3232

3333
@classmethod
3434
def from_string(cls, name: str) -> "UiPathServerType":

src/uipath_mcp/_cli/_runtime/_runtime.py

Lines changed: 125 additions & 45 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,23 +141,32 @@ 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
with tracer.start_as_current_span(self._server.name) as root_span:
80154
root_span.set_attribute("runtime_id", self._runtime_id)
81-
root_span.set_attribute("command", self._server.command)
82-
root_span.set_attribute("args", self._server.args)
155+
root_span.set_attribute("command", str(self._server.command))
156+
root_span.set_attribute("args", json.dumps(self._server.args))
83157
root_span.set_attribute("span_type", "MCP Server")
158+
trace_context = cast(UiPathTraceContext, self.context.trace_context)
159+
tenant_id = str(trace_context.tenant_id)
160+
org_id = str(trace_context.org_id)
161+
84162
self._signalr_client = SignalRClient(
85163
signalr_url,
86164
headers={
87-
"X-UiPath-Internal-TenantId": self.context.trace_context.tenant_id,
88-
"X-UiPath-Internal-AccountId": self.context.trace_context.org_id,
165+
"X-UiPath-Internal-TenantId": tenant_id,
166+
"X-UiPath-Internal-AccountId": org_id,
89167
},
90168
)
169+
91170
self._signalr_client.on("MessageReceived", self._handle_signalr_message)
92171
self._signalr_client.on(
93172
"SessionClosed", self._handle_signalr_session_closed
@@ -145,20 +224,9 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
145224
) from e
146225
finally:
147226
await self.cleanup()
148-
if hasattr(self, "trace_provider") and self.trace_provider:
227+
if self.trace_provider:
149228
self.trace_provider.shutdown()
150229

151-
async def validate(self) -> None:
152-
"""Validate runtime inputs and load MCP server configuration."""
153-
self._server = self.context.config.get_server(self.context.entrypoint)
154-
if not self._server:
155-
raise UiPathMcpRuntimeError(
156-
"SERVER_NOT_FOUND",
157-
"MCP server not found",
158-
f"Server '{self.context.entrypoint}' not found in configuration",
159-
UiPathErrorCategory.DEPLOYMENT,
160-
)
161-
162230
async def cleanup(self) -> None:
163231
"""Clean up all resources."""
164232

@@ -189,7 +257,7 @@ async def cleanup(self) -> None:
189257
if sys.platform == "win32":
190258
await asyncio.sleep(0.5)
191259

192-
async def _handle_signalr_session_closed(self, args: list) -> None:
260+
async def _handle_signalr_session_closed(self, args: list[Any]) -> None:
193261
"""
194262
Handle session closed by server.
195263
"""
@@ -219,7 +287,7 @@ async def _handle_signalr_session_closed(self, args: list) -> None:
219287
except Exception as e:
220288
logger.error(f"Error terminating session {session_id}: {str(e)}")
221289

222-
async def _handle_signalr_message(self, args: list) -> None:
290+
async def _handle_signalr_message(self, args: list[Any]) -> None:
223291
"""
224292
Handle incoming SignalR messages.
225293
"""
@@ -233,10 +301,12 @@ async def _handle_signalr_message(self, args: list) -> None:
233301
logger.info(f"Received websocket notification... {session_id}")
234302

235303
try:
304+
server = cast(McpServer, self._server)
305+
236306
# Check if we have a session server for this session_id
237307
if session_id not in self._session_servers:
238308
# Create and start a new session server
239-
session_server = SessionServer(self._server, session_id)
309+
session_server = SessionServer(server, session_id)
240310
try:
241311
await session_server.start()
242312
except Exception as e:
@@ -283,10 +353,11 @@ async def _register(self) -> None:
283353
)
284354
logger.info(f"Folder key: {folder_key}")
285355

356+
server = cast(McpServer, self._server)
286357
initialization_successful = False
287-
tools_result = None
358+
tools_result: Optional[ListToolsResult] = None
288359
server_stderr_output = ""
289-
env_vars = self._server.env
360+
env_vars = dict(server.env)
290361

291362
# if server is Coded, include environment variables
292363
if self.server_type is UiPathServerType.Coded:
@@ -298,14 +369,15 @@ async def _register(self) -> None:
298369
try:
299370
# Create a temporary session to get tools
300371
server_params = StdioServerParameters(
301-
command=self._server.command,
302-
args=self._server.args,
372+
command=server.command,
373+
args=server.args,
303374
env=env_vars,
304375
)
305376

306377
# Start a temporary stdio client to get tools
307378
# Use a temporary file to capture stderr
308-
with tempfile.TemporaryFile(mode="w+b") as stderr_temp:
379+
with tempfile.TemporaryFile(mode="w+b") as stderr_temp_binary:
380+
stderr_temp = io.TextIOWrapper(stderr_temp_binary, encoding="utf-8")
309381
async with stdio_client(server_params, errlog=stderr_temp) as (
310382
read,
311383
write,
@@ -325,11 +397,7 @@ async def _register(self) -> None:
325397
logger.error("Initialization timed out")
326398
# Capture stderr output here, after the timeout
327399
stderr_temp.seek(0)
328-
server_stderr_output = stderr_temp.read().decode(
329-
"utf-8", errors="replace"
330-
)
331-
# We'll handle this after exiting the context managers
332-
# We don't continue with registration here - we'll do it after the context managers
400+
server_stderr_output = stderr_temp.read()
333401

334402
except* Exception as eg:
335403
for e in eg.exceptions:
@@ -355,14 +423,24 @@ async def _register(self) -> None:
355423
# Now continue with registration
356424
logger.info("Registering server runtime ...")
357425
try:
426+
if not tools_result:
427+
raise UiPathMcpRuntimeError(
428+
"INITIALIZATION_ERROR",
429+
"Server initialization failed",
430+
"Failed to get tools list from server",
431+
UiPathErrorCategory.DEPLOYMENT,
432+
)
433+
434+
server = cast(McpServer, self._server)
435+
tools_list: List[Dict[str, str | None]] = []
358436
client_info = {
359437
"server": {
360-
"Name": self._server.name,
361-
"Slug": self._server.name,
438+
"Name": server.name,
439+
"Slug": server.name,
362440
"Version": "1.0.0",
363441
"Type": self.server_type.value,
364442
},
365-
"tools": [],
443+
"tools": tools_list,
366444
}
367445

368446
for tool in tools_result.tools:
@@ -374,12 +452,12 @@ async def _register(self) -> None:
374452
if tool.inputSchema
375453
else "{}",
376454
}
377-
client_info["tools"].append(tool_info)
455+
tools_list.append(tool_info)
378456

379457
# Register with UiPath MCP Server
380458
await self._uipath.api_client.request_async(
381459
"POST",
382-
f"agenthub_/mcp/{self._server.name}/runtime/start?runtimeId={self._runtime_id}",
460+
f"agenthub_/mcp/{server.name}/runtime/start?runtimeId={self._runtime_id}",
383461
json=client_info,
384462
headers={"X-UIPATH-FolderKey": folder_key},
385463
)
@@ -406,14 +484,14 @@ async def _on_session_start_error(self, session_id: str) -> None:
406484
try:
407485
response = await self._uipath.api_client.request_async(
408486
"POST",
409-
f"agenthub_/mcp/{self._server.name}/out/message?sessionId={session_id}",
487+
f"agenthub_/mcp/{self._server.name}/out/message?sessionId={session_id}", # type: ignore
410488
json=JSONRPCResponse(
411489
jsonrpc="2.0",
412490
id=0,
413491
result={
414492
"protocolVersion": "initialize-failure",
415493
"capabilities": {},
416-
"serverInfo": {"name": self._server.name, "version": "1.0"},
494+
"serverInfo": {"name": self._server.name, "version": "1.0"}, # type: ignore
417495
},
418496
).model_dump(),
419497
)
@@ -463,7 +541,7 @@ async def on_keep_alive_response(
463541
await self._signalr_client.send(
464542
method="OnKeepAlive",
465543
arguments=[],
466-
on_invocation=on_keep_alive_response,
544+
on_invocation=on_keep_alive_response, # type: ignore
467545
)
468546
except Exception as e:
469547
if not self._cancel_event.is_set():
@@ -485,7 +563,7 @@ async def _on_runtime_abort(self) -> None:
485563
try:
486564
response = await self._uipath.api_client.request_async(
487565
"POST",
488-
f"agenthub_/mcp/{self._server.name}/runtime/abort?runtimeId={self._runtime_id}",
566+
f"agenthub_/mcp/{self._server.name}/runtime/abort?runtimeId={self._runtime_id}", # type: ignore
489567
)
490568
if response.status_code == 202:
491569
logger.info(
@@ -518,7 +596,9 @@ def packaged(self) -> bool:
518596
Returns:
519597
bool: True if this is a packaged runtime (has a process), False otherwise.
520598
"""
521-
process_key = self.context.trace_context.process_key
599+
process_key = None
600+
if self.context.trace_context is not None:
601+
process_key = self.context.trace_context.process_key
522602

523603
return (
524604
process_key is not None

0 commit comments

Comments
 (0)