33from collections import Counter , defaultdict
44from contextlib import AsyncExitStack , asynccontextmanager
55from itertools import chain
6+ from typing import Any , AsyncGenerator
67
78import httpx
8- from langchain_core .tools import BaseTool
9+ from langchain_core .tools import BaseTool , StructuredTool
910from uipath ._utils ._ssl_context import get_httpx_client_kwargs
1011from uipath .agent .models .agent import AgentMcpResourceConfig
12+ from uipath .platform import UiPath
13+ from uipath .platform .orchestrator .mcp import McpServer
14+
15+ from uipath_langchain .agent .react .jsonschema_pydantic_converter import create_model
16+
17+ from .utils import sanitize_tool_name
1118
1219
1320def _deduplicate_tools (tools : list [BaseTool ]) -> list [BaseTool ]:
@@ -33,7 +40,7 @@ def _filter_tools(tools: list[BaseTool], cfg: AgentMcpResourceConfig) -> list[Ba
3340async def create_mcp_tools (
3441 config : AgentMcpResourceConfig | list [AgentMcpResourceConfig ],
3542 max_concurrency : int = 5 ,
36- ):
43+ ) -> AsyncGenerator [ list [ BaseTool ], None ] :
3744 """Connect to UiPath MCP server(s) and yield LangChain-compatible tools."""
3845 if not (base_url := os .getenv ("UIPATH_URL" )):
3946 raise ValueError ("UIPATH_URL environment variable is not set" )
@@ -88,3 +95,79 @@ async def create_session(
8895 sessions = [(await create_session (stack , cfg ), cfg ) for cfg in enabled ]
8996 results = await asyncio .gather (* [init_session (s , cfg ) for s , cfg in sessions ])
9097 yield _deduplicate_tools (list (chain .from_iterable (results )))
98+
99+
100+ async def create_mcp_tools_from_metadata (
101+ config : AgentMcpResourceConfig ,
102+ ) -> list [BaseTool ]:
103+ """Create individual StructuredTool instances for each MCP tool in the resource config.
104+
105+ Each tool manages its own session lifecycle - creating, using, and cleaning up
106+ the MCP connection within the tool invocation.
107+ """
108+
109+ if config .is_enabled is False :
110+ return []
111+
112+ sdk = UiPath ()
113+ mcpServer : McpServer = await sdk .mcp .retrieve_async (
114+ slug = config .slug , folder_path = config .folder_path
115+ )
116+
117+ default_client_kwargs = get_httpx_client_kwargs ()
118+ client_kwargs = {
119+ ** default_client_kwargs ,
120+ "headers" : {"Authorization" : f"Bearer { sdk ._config .secret } " },
121+ "timeout" : httpx .Timeout (600 ),
122+ }
123+
124+ tools : list [BaseTool ] = []
125+
126+ for mcp_tool in config .available_tools :
127+ tool_name = sanitize_tool_name (mcp_tool .name )
128+ input_model : Any = create_model (mcp_tool .input_schema )
129+
130+ def get_tool_coroutine (mcp_tool : Any ) -> Any :
131+ async def tool_fn (** kwargs : Any ) -> Any :
132+ """Execute MCP tool call with ephemeral session."""
133+ async with AsyncExitStack () as stack :
134+ # Create HTTP client
135+ http_client = await stack .enter_async_context (
136+ httpx .AsyncClient (** client_kwargs )
137+ )
138+
139+ # Create streamable connection
140+ read , write , _ = await stack .enter_async_context (
141+ streamable_http_client (
142+ url = f"{ mcpServer .mcp_url } " , http_client = http_client
143+ )
144+ )
145+
146+ # Create and initialize session
147+ session = await stack .enter_async_context (
148+ ClientSession (read , write )
149+ )
150+ await session .initialize ()
151+
152+ # Call the tool
153+ result = await session .call_tool (mcp_tool .name , arguments = kwargs )
154+ return result .content if hasattr (result , "content" ) else result
155+
156+ return tool_fn
157+
158+ tool = StructuredTool (
159+ name = tool_name ,
160+ description = mcp_tool .description ,
161+ args_schema = input_model ,
162+ coroutine = get_tool_coroutine (mcp_tool ),
163+ metadata = {
164+ "tool_type" : "mcp" ,
165+ "display_name" : mcp_tool .name ,
166+ "folder_path" : config .folder_path ,
167+ "slug" : config .slug ,
168+ },
169+ )
170+
171+ tools .append (tool )
172+
173+ return tools
0 commit comments