1- import asyncio
21import logging
3- import os
4- from collections import Counter , defaultdict
52from contextlib import AsyncExitStack , asynccontextmanager
6- from itertools import chain
73from typing import Any , AsyncGenerator
84
9- import httpx
105from langchain_core .tools import BaseTool
11- from uipath ._utils ._ssl_context import get_httpx_client_kwargs
126from uipath .agent .models .agent import (
137 AgentMcpResourceConfig ,
148 AgentMcpTool ,
159 DynamicToolsMode ,
16- LowCodeAgentDefinition ,
1710)
1811from uipath .eval .mocks import mockable
19- from uipath .runtime .errors import UiPathErrorCategory
2012
21- from uipath_langchain .agent .exceptions import AgentStartupError , AgentStartupErrorCode
2213from uipath_langchain .agent .tools .base_uipath_structured_tool import (
2314 BaseUiPathStructuredTool ,
2415)
2920logger : logging .Logger = logging .getLogger (__name__ )
3021
3122
32- def _deduplicate_tools (tools : list [BaseTool ]) -> list [BaseTool ]:
33- """Deduplicate tools by appending numeric suffix to duplicate names."""
34- counts = Counter (tool .name for tool in tools )
35- seen : defaultdict [str , int ] = defaultdict (int )
36-
37- for tool in tools :
38- if counts [tool .name ] > 1 :
39- seen [tool .name ] += 1
40- tool .name = f"{ tool .name } _{ seen [tool .name ]} "
41-
42- return tools
43-
44-
45- def _filter_tools (tools : list [BaseTool ], cfg : AgentMcpResourceConfig ) -> list [BaseTool ]:
46- """Filter tools to only include those in available_tools."""
47- allowed = {t .name for t in cfg .available_tools }
48- return [t for t in tools if t .name in allowed ]
49-
50-
5123@asynccontextmanager
52- async def create_mcp_tools (
53- config : AgentMcpResourceConfig | list [AgentMcpResourceConfig ],
54- max_concurrency : int = 5 ,
24+ async def open_mcp_tools (
25+ config : list [AgentMcpResourceConfig ],
5526) -> AsyncGenerator [list [BaseTool ], None ]:
56- """Connect to UiPath MCP server(s) and yield LangChain-compatible tools."""
57- if not (base_url := os .getenv ("UIPATH_URL" )):
58- raise AgentStartupError (
59- code = AgentStartupErrorCode .INVALID_TOOL_CONFIG ,
60- title = "Missing UIPATH_URL" ,
61- detail = "UIPATH_URL environment variable is not set." ,
62- category = UiPathErrorCategory .SYSTEM ,
63- )
64- if not (access_token := os .getenv ("UIPATH_ACCESS_TOKEN" )):
65- raise AgentStartupError (
66- code = AgentStartupErrorCode .INVALID_TOOL_CONFIG ,
67- title = "Missing UIPATH_ACCESS_TOKEN" ,
68- detail = "UIPATH_ACCESS_TOKEN environment variable is not set." ,
69- category = UiPathErrorCategory .SYSTEM ,
70- )
27+ """Connect to UiPath MCP server(s) via McpClient and yield LangChain-compatible tools.
7128
72- configs = config if isinstance (config , list ) else [config ]
73- enabled = [c for c in configs if c .is_enabled is not False ]
74-
75- if not enabled :
76- yield []
77- return
78-
79- base_url = base_url .rstrip ("/" )
80- semaphore = asyncio .Semaphore (max_concurrency )
81-
82- default_client_kwargs = get_httpx_client_kwargs ()
83- client_kwargs = {
84- ** default_client_kwargs ,
85- "headers" : {"Authorization" : f"Bearer { access_token } " },
86- "timeout" : httpx .Timeout (60 ),
87- }
88-
89- # Lazy import to improve cold start time
90- from langchain_mcp_adapters .tools import load_mcp_tools
91- from mcp import ClientSession
92- from mcp .client .streamable_http import streamable_http_client
93-
94- async def init_session (
95- session : ClientSession , cfg : AgentMcpResourceConfig
96- ) -> list [BaseTool ]:
97- async with semaphore :
98- await session .initialize ()
99- tools = await load_mcp_tools (session )
100- for tool in tools :
101- tool .metadata = {"tool_type" : "mcp" , "display_name" : tool .name }
102- return _filter_tools (tools , cfg )
103-
104- async def create_session (
105- stack : AsyncExitStack , cfg : AgentMcpResourceConfig
106- ) -> ClientSession :
107- url = f"{ base_url } /agenthub_/mcp/{ cfg .folder_path } /{ cfg .slug } "
108- http_client = await stack .enter_async_context (
109- httpx .AsyncClient (** client_kwargs )
110- )
111- read , write , _ = await stack .enter_async_context (
112- streamable_http_client (url = url , http_client = http_client )
113- )
114- return await stack .enter_async_context (ClientSession (read , write ))
29+ Wraps create_mcp_tools_and_clients() with automatic client lifecycle management.
30+ Tools are lazily initialized on first call via the UiPath SDK.
11531
32+ Args:
33+ config: List of MCP resource configurations.
34+
35+ Yields:
36+ List of BaseTool instances for all enabled MCP resources.
37+ """
11638 async with AsyncExitStack () as stack :
117- sessions = [(await create_session (stack , cfg ), cfg ) for cfg in enabled ]
118- results = await asyncio .gather (* [init_session (s , cfg ) for s , cfg in sessions ])
119- yield _deduplicate_tools (list (chain .from_iterable (results )))
39+ tools , clients = await create_mcp_tools_and_clients (config )
40+ for client in clients :
41+ stack .push_async_callback (client .dispose )
42+ yield tools
12043
12144
122- async def create_mcp_tools_from_metadata_for_mcp_server (
45+ async def create_mcp_tools (
12346 config : AgentMcpResourceConfig ,
12447 mcpClient : McpClient ,
12548) -> list [BaseTool ]:
@@ -242,24 +165,25 @@ async def tool_fn(**kwargs: Any) -> Any:
242165 return tool_fn
243166
244167
245- async def create_mcp_tools_from_agent (
246- agent : LowCodeAgentDefinition ,
168+ async def create_mcp_tools_and_clients (
169+ resources : list [ AgentMcpResourceConfig ] ,
247170 session_info_factory : SessionInfoFactory | None = None ,
248171 terminate_on_close : bool = True ,
249172) -> tuple [list [BaseTool ], list [McpClient ]]:
250- """Create MCP tools from a LowCodeAgentDefinition .
173+ """Create MCP tools from a list of MCP resource configurations .
251174
252- Iterates over all MCP resources in the agent definition and creates tools
253- for each enabled MCP server. Each MCP server gets its own McpClient instance.
175+ Iterates over all MCP resources and creates tools for each enabled MCP
176+ server. Each MCP server gets its own McpClient instance.
254177
255178 The MCP server URL is loaded lazily on first tool call via the UiPath SDK,
256179 using environment variables (UIPATH_URL, UIPATH_ACCESS_TOKEN).
257180
258181 Args:
259- agent: The agent definition containing MCP resources .
182+ resources: List of MCP resource configurations .
260183 session_info_factory: Factory for creating SessionInfo instances.
261184 Defaults to the base ``SessionInfoFactory``. Pass
262185 ``SessionInfoDebugStateFactory()`` for playground mode.
186+ terminate_on_close: Whether to terminate the MCP session on close.
263187
264188 Returns:
265189 A tuple of (tools, mcp_clients) where:
@@ -273,10 +197,7 @@ async def create_mcp_tools_from_agent(
273197 tools : list [BaseTool ] = []
274198 clients : list [McpClient ] = []
275199
276- for resource in agent .resources :
277- if not isinstance (resource , AgentMcpResourceConfig ):
278- continue
279-
200+ for resource in resources :
280201 if resource .is_enabled is False :
281202 logger .info (f"Skipping disabled MCP resource '{ resource .name } '" )
282203 continue
@@ -290,9 +211,7 @@ async def create_mcp_tools_from_agent(
290211 )
291212 clients .append (mcpClient )
292213
293- resource_tools = await create_mcp_tools_from_metadata_for_mcp_server (
294- resource , mcpClient
295- )
214+ resource_tools = await create_mcp_tools (resource , mcpClient )
296215 tools .extend (resource_tools )
297216 logger .info (
298217 f"Created { len (resource_tools )} tools for MCP resource '{ resource .name } '"
0 commit comments