Skip to content

Commit c66cf4a

Browse files
committed
feat: mcp tool callable
1 parent 34bf679 commit c66cf4a

File tree

2 files changed

+112
-4
lines changed

2 files changed

+112
-4
lines changed

src/uipath_langchain/agent/tools/mcp_tool.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
from collections import Counter, defaultdict
44
from contextlib import AsyncExitStack, asynccontextmanager
55
from itertools import chain
6+
from typing import Any, AsyncGenerator
67

78
import httpx
8-
from langchain_core.tools import BaseTool
9+
from langchain_core.tools import BaseTool, StructuredTool
910
from uipath._utils._ssl_context import get_httpx_client_kwargs
1011
from 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

1320
def _deduplicate_tools(tools: list[BaseTool]) -> list[BaseTool]:
@@ -33,7 +40,7 @@ def _filter_tools(tools: list[BaseTool], cfg: AgentMcpResourceConfig) -> list[Ba
3340
async 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

src/uipath_langchain/agent/tools/tool_factory.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Factory functions for creating tools from agent resources."""
22

3+
from logging import getLogger
4+
35
from langchain_core.language_models import BaseChatModel
46
from langchain_core.tools import BaseTool
57
from uipath.agent.models.agent import (
@@ -8,6 +10,7 @@
810
AgentIntegrationToolResourceConfig,
911
AgentInternalToolResourceConfig,
1012
AgentIxpExtractionResourceConfig,
13+
AgentMcpResourceConfig,
1114
AgentProcessToolResourceConfig,
1215
BaseAgentResourceConfig,
1316
LowCodeAgentDefinition,
@@ -18,25 +21,37 @@
1821
from .extraction_tool import create_ixp_extraction_tool
1922
from .integration_tool import create_integration_tool
2023
from .internal_tools import create_internal_tool
24+
from .mcp_tool import create_mcp_tools_from_metadata
2125
from .process_tool import create_process_tool
2226

27+
logger = getLogger(__name__)
28+
2329

2430
async def create_tools_from_resources(
2531
agent: LowCodeAgentDefinition, llm: BaseChatModel
2632
) -> list[BaseTool]:
2733
tools: list[BaseTool] = []
2834

35+
logger.info("Creating tools for agent '%s' from resources", agent.name)
2936
for resource in agent.resources:
37+
logger.info(
38+
"Creating tool for resource '%s' of type '%s'",
39+
resource.name,
40+
type(resource).__name__,
41+
)
3042
tool = await _build_tool_for_resource(resource, llm)
3143
if tool is not None:
32-
tools.append(tool)
44+
if isinstance(tool, list):
45+
tools.extend(tool)
46+
else:
47+
tools.append(tool)
3348

3449
return tools
3550

3651

3752
async def _build_tool_for_resource(
3853
resource: BaseAgentResourceConfig, llm: BaseChatModel
39-
) -> BaseTool | None:
54+
) -> BaseTool | list[BaseTool] | None:
4055
if isinstance(resource, AgentProcessToolResourceConfig):
4156
return create_process_tool(resource)
4257

@@ -55,4 +70,14 @@ async def _build_tool_for_resource(
5570
elif isinstance(resource, AgentIxpExtractionResourceConfig):
5671
return create_ixp_extraction_tool(resource)
5772

73+
elif isinstance(resource, AgentMcpResourceConfig):
74+
logger.info(
75+
f"Creating MCP tools for MCP server with slug '{resource.slug}' in folder '{resource.folder_path}'"
76+
)
77+
import json
78+
79+
logger.info(json.dumps(resource.model_dump()))
80+
81+
return await create_mcp_tools_from_metadata(resource)
82+
5883
return None

0 commit comments

Comments
 (0)