1+ import copy
12import logging
23from contextlib import AsyncExitStack , asynccontextmanager
34from typing import Any , AsyncGenerator
45
6+ from langchain_core .messages .tool import ToolCall
57from langchain_core .tools import BaseTool
68from uipath .agent .models .agent import (
79 AgentMcpResourceConfig ,
1012)
1113from uipath .eval .mocks import mockable
1214
13- from uipath_langchain .agent .tools .base_uipath_structured_tool import (
14- BaseUiPathStructuredTool ,
15+ from uipath_langchain .agent .react .types import AgentGraphState
16+ from uipath_langchain .agent .tools .static_args import handle_static_args
17+ from uipath_langchain .agent .tools .structured_tool_with_argument_properties import (
18+ StructuredToolWithArgumentProperties ,
1519)
20+ from uipath_langchain .agent .tools .tool_node import ToolWrapperReturnType
1621
1722from ..utils import sanitize_tool_name
1823from .mcp_client import McpClient , SessionInfoFactory
@@ -73,6 +78,8 @@ async def create_mcp_tools(
7378 f"(dynamic_tools={ dynamic_tools .value } )"
7479 )
7580
81+ config_tools_by_name = {t .name : t for t in config .available_tools }
82+
7683 if dynamic_tools in (DynamicToolsMode .SCHEMA , DynamicToolsMode .ALL ):
7784 logger .info (f"Fetching tools from MCP server '{ config .slug } ' via list_tools" )
7885 result = await mcpClient .list_tools ()
@@ -96,24 +103,34 @@ async def create_mcp_tools(
96103 f"Filtered to { len (server_tools )} tools matching availableTools"
97104 )
98105
99- mcp_tools = [
100- AgentMcpTool (
101- name = tool .name ,
102- description = tool .description or "" ,
103- input_schema = tool .inputSchema ,
104- output_schema = tool .outputSchema ,
106+ mcp_tools = []
107+ for tool in server_tools :
108+ config_tool = config_tools_by_name .get (tool .name )
109+ argument_properties = config_tool .argument_properties if config_tool else {}
110+ server_schema = tool .inputSchema
111+ if config_tool :
112+ server_schema = _merge_descriptions_from_config (
113+ server_schema , config_tool .input_schema
114+ )
115+ mcp_tools .append (
116+ AgentMcpTool (
117+ name = tool .name ,
118+ description = tool .description or "" ,
119+ inputSchema = server_schema ,
120+ outputSchema = tool .outputSchema ,
121+ argumentProperties = argument_properties ,
122+ )
105123 )
106- for tool in server_tools
107- ]
108124 else :
109125 mcp_tools = config .available_tools
110126 logger .info (
111127 f"Using { len (mcp_tools )} tools from resource config for "
112128 f"server '{ config .slug } '"
113129 )
114130
115- return [
116- BaseUiPathStructuredTool (
131+ tools : list [BaseTool ] = []
132+ for mcp_tool in mcp_tools :
133+ tool = StructuredToolWithArgumentProperties (
117134 name = sanitize_tool_name (mcp_tool .name ),
118135 description = mcp_tool .description ,
119136 args_schema = mcp_tool .input_schema ,
@@ -124,9 +141,12 @@ async def create_mcp_tools(
124141 "folder_path" : config .folder_path ,
125142 "slug" : config .slug ,
126143 },
144+ argument_properties = mcp_tool .argument_properties ,
127145 )
128- for mcp_tool in mcp_tools
129- ]
146+ if mcp_tool .argument_properties :
147+ tool .set_tool_wrappers (awrapper = _mcp_tool_wrapper )
148+ tools .append (tool )
149+ return tools
130150
131151
132152def build_mcp_tool (mcp_tool : AgentMcpTool , mcpClient : McpClient ) -> Any :
@@ -218,3 +238,48 @@ async def create_mcp_tools_and_clients(
218238 )
219239
220240 return tools , clients
241+
242+
243+ async def _mcp_tool_wrapper (
244+ tool : BaseTool ,
245+ call : ToolCall ,
246+ state : AgentGraphState ,
247+ ) -> ToolWrapperReturnType :
248+ """Wrapper that injects static argument values before MCP tool execution."""
249+ call ["args" ] = handle_static_args (tool , state , call ["args" ])
250+ return await tool .ainvoke (call )
251+
252+
253+ def _merge_descriptions_from_config (
254+ server_schema : dict [str , Any ],
255+ config_schema : dict [str , Any ],
256+ ) -> dict [str , Any ]:
257+ """Merge property descriptions from config schema into server schema.
258+
259+ For each property that exists in both schemas, if the config schema
260+ has a description, it overwrites the server schema's description.
261+ This preserves prompt overrides that were configured in agent.json.
262+
263+ Args:
264+ server_schema: The fresh schema from the MCP server.
265+ config_schema: The schema from the agent config with prompt overrides.
266+
267+ Returns:
268+ A copy of the server schema with config descriptions merged in.
269+ """
270+ server_props = server_schema .get ("properties" , {})
271+ config_props = config_schema .get ("properties" , {})
272+
273+ if not config_props or not server_props :
274+ return server_schema
275+
276+ merged_schema = copy .deepcopy (server_schema )
277+ merged_props = merged_schema ["properties" ]
278+
279+ for prop_name , config_prop in config_props .items ():
280+ if prop_name not in merged_props :
281+ continue
282+ if "description" in config_prop :
283+ merged_props [prop_name ]["description" ] = config_prop ["description" ]
284+
285+ return merged_schema
0 commit comments