Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.10.16"
version = "0.10.17"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
58 changes: 34 additions & 24 deletions src/uipath_langchain/agent/tools/mcp/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
)
from uipath.eval.mocks import mockable

from uipath_langchain.agent.tools.base_uipath_structured_tool import (
BaseUiPathStructuredTool,
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
StructuredToolWithArgumentProperties,
)

from ..utils import sanitize_tool_name
Expand Down Expand Up @@ -73,6 +73,8 @@ async def create_mcp_tools(
f"(dynamic_tools={dynamic_tools.value})"
)

config_tools_by_name = {t.name: t for t in config.available_tools}

if dynamic_tools in (DynamicToolsMode.SCHEMA, DynamicToolsMode.ALL):
logger.info(f"Fetching tools from MCP server '{config.slug}' via list_tools")
result = await mcpClient.list_tools()
Expand All @@ -96,37 +98,45 @@ async def create_mcp_tools(
f"Filtered to {len(server_tools)} tools matching availableTools"
)

mcp_tools = [
AgentMcpTool(
name=tool.name,
description=tool.description or "",
input_schema=tool.inputSchema,
output_schema=tool.outputSchema,
mcp_tools = []
for tool in server_tools:
config_tool = config_tools_by_name.get(tool.name)
argument_properties = config_tool.argument_properties if config_tool else {}
mcp_tools.append(
AgentMcpTool(
name=tool.name,
description=tool.description or "",
input_schema=tool.inputSchema,
output_schema=tool.outputSchema,
argument_properties=argument_properties,
)
)
for tool in server_tools
]
else:
mcp_tools = config.available_tools
logger.info(
f"Using {len(mcp_tools)} tools from resource config for "
f"server '{config.slug}'"
)

return [
BaseUiPathStructuredTool(
name=sanitize_tool_name(mcp_tool.name),
description=mcp_tool.description,
args_schema=mcp_tool.input_schema,
coroutine=build_mcp_tool(mcp_tool, mcpClient),
metadata={
"tool_type": "mcp",
"display_name": mcp_tool.name,
"folder_path": config.folder_path,
"slug": config.slug,
},
tools: list[BaseTool] = []
for mcp_tool in mcp_tools:
tools.append(
StructuredToolWithArgumentProperties(
name=sanitize_tool_name(mcp_tool.name),
description=mcp_tool.description,
args_schema=mcp_tool.input_schema,
coroutine=build_mcp_tool(mcp_tool, mcpClient),
output_type=Any,
metadata={
"tool_type": "mcp",
"display_name": mcp_tool.name,
"folder_path": config.folder_path,
"slug": config.slug,
},
argument_properties=mcp_tool.argument_properties,
)
)
for mcp_tool in mcp_tools
]
return tools


def build_mcp_tool(mcp_tool: AgentMcpTool, mcpClient: McpClient) -> Any:
Expand Down
118 changes: 117 additions & 1 deletion tests/agent/tools/test_mcp/test_mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import logging
from typing import Any
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
Expand All @@ -21,6 +21,9 @@
create_mcp_tools_and_clients,
open_mcp_tools,
)
from uipath_langchain.agent.tools.structured_tool_with_argument_properties import (
StructuredToolWithArgumentProperties,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1005,3 +1008,116 @@ async def test_disposes_clients_on_exception(self, mcp_config):
raise RuntimeError("boom")

mock_client.dispose.assert_awaited_once()


class TestMcpToolArgumentProperties:
"""Test that argument_properties from config are applied to MCP tools."""

@pytest.fixture
def mock_mcp_client(self):
return MagicMock(spec=McpClient)

@pytest.mark.asyncio
async def test_none_mode_passes_argument_properties_to_tool(self, mock_mcp_client):
"""In none mode, argument_properties from config must reach the built tool."""
resource = AgentMcpResourceConfig(
name="test_server",
description="Test",
folder_path="/Shared",
slug="test",
available_tools=[
AgentMcpTool(
name="divide",
description="Divide two numbers",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
argument_properties={
"$['a']": {
"variant": "static",
"value": 76,
"isSensitive": False,
}
},
),
],
)

tools = await create_mcp_tools(resource, mock_mcp_client)

assert len(tools) == 1
tool = tools[0]
assert hasattr(tool, "argument_properties")
assert "$['a']" in tool.argument_properties

@pytest.mark.asyncio
async def test_all_mode_carries_over_argument_properties_for_matching_tools(
self, mock_mcp_client
):
"""In all mode, argument_properties from config must attach to matching server tools."""
mock_mcp_client.list_tools = AsyncMock(
return_value=ListToolsResult(
tools=[
Tool(
name="divide",
description="Divide from server",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
},
),
Tool(
name="new_tool",
description="New tool not in config",
inputSchema={"type": "object", "properties": {}},
),
]
)
)

resource = AgentMcpResourceConfig(
name="test_server",
description="Test",
folder_path="/Shared",
slug="test",
dynamic_tools=DynamicToolsMode.ALL,
available_tools=[
AgentMcpTool(
name="divide",
description="Divide (stale)",
input_schema={"type": "object", "properties": {}},
argument_properties={
"$['a']": {
"variant": "static",
"value": 76,
"isSensitive": False,
}
},
),
],
)

tools = await create_mcp_tools(resource, mock_mcp_client)

assert len(tools) == 2
divide_tool = next(
cast(StructuredToolWithArgumentProperties, t)
for t in tools
if t.name == "divide"
)
new_tool = next(
cast(StructuredToolWithArgumentProperties, t)
for t in tools
if t.name == "new_tool"
)

assert "$['a']" in divide_tool.argument_properties
assert not new_tool.argument_properties
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading