Skip to content
Open
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
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.16.12"
version = "0.17.0"
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 All @@ -18,11 +18,11 @@ dependencies = [
"pydantic-settings>=2.6.0",
"python-dotenv>=1.0.1",
"httpx>=0.27.0",
"httpx2>=2.5.0, <2.10.0",
"openinference-instrumentation-langchain>=0.1.69, <0.2.0",
"jsonschema-pydantic-converter>=0.4.0",
"jsonpath-ng>=1.7.0",
"mcp==1.26.0",
"langchain-mcp-adapters==0.2.1",
"mcp==2.0.0",
"pillow>=12.1.1",
"rdflib>=7.0.0, <8.0.0",
"a2a-sdk>=1.1.2,<2.0.0",
Expand Down Expand Up @@ -90,6 +90,11 @@ dev = [
"pytest_httpx>=0.35.0",
"rust-just>=1.39.0",
"types-protobuf<7",
# tests/agent/tools/test_mcp/real_server.py hosts real MCP servers over real
# HTTP. Both arrive transitively via `mcp`, but the tests import them
# directly, so they are declared here rather than relied on by accident.
"starlette>=0.41.3",
"uvicorn>=0.30.0",
]

[tool.hatch.build.targets.wheel]
Expand Down
5 changes: 2 additions & 3 deletions samples/oauth-external-apps-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ The workflow follows a ReAct pattern:

- Python 3.11+
- `uipath-langchain`
- `langchain-mcp-adapters`
- MCP Python SDK 2.0
- `langgraph`
- `httpx`
- `httpx2`
- `python-dotenv`
- UiPath OAuth credentials and MCP server URL in environment
- UiPath external application configured with `OR.Jobs` scope (or appropriate scope for your MCP server)
Expand Down Expand Up @@ -81,4 +81,3 @@ For debugging issues:
uipath run agent --debug '{"task": "What is 2 + 2?"}'
```


76 changes: 47 additions & 29 deletions samples/oauth-external-apps-agent/main.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import os
import dotenv
import httpx
from contextlib import asynccontextmanager
from typing import Optional, Literal
from typing import Literal, Optional

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
import dotenv
import httpx2
from langchain.agents import create_agent
from langchain.messages import SystemMessage, HumanMessage

from uipath_langchain.chat.models import UiPathChat
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain.messages import HumanMessage, SystemMessage
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.streamable_http import streamable_http_client
from pydantic import BaseModel
from uipath.platform import UiPath

from uipath_langchain.agent.tools.mcp import load_mcp_tools
from uipath_langchain.chat.models import UiPathChat

dotenv.load_dotenv()

UIPATH_CLIENT_ID = "EXTERNAL_APP_CLIENT_ID_HERE"
Expand All @@ -24,17 +24,21 @@
UIPATH_URL = "base_url"
UIPATH_MCP_SERVER_URL = os.getenv("UIPATH_MCP_SERVER_URL")


class GraphInput(BaseModel):
task: str


class GraphOutput(BaseModel):
result: str


class State(BaseModel):
task: str
access_token: Optional[str] = os.getenv("UIPATH_ACCESS_TOKEN")
result: Optional[str] = None


async def fetch_new_access_token(state: State) -> Command:
try:
UiPath(
Expand All @@ -46,44 +50,58 @@ async def fetch_new_access_token(state: State) -> Command:
return Command(update={"access_token": os.getenv("UIPATH_ACCESS_TOKEN")})

except Exception as e:
raise Exception(f"Failed to initialize UiPath SDK: {str(e)}")
raise Exception(f"Failed to initialize UiPath SDK: {str(e)}") from e


@asynccontextmanager
async def agent_mcp(access_token: str):
async with streamablehttp_client(
url=UIPATH_MCP_SERVER_URL,
async with httpx2.AsyncClient(
headers={"Authorization": f"Bearer {access_token}"},
timeout=60,
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
model = UiPathChat(model="anthropic.claude-3-5-sonnet-20240620-v1:0")
agent = create_agent(model, tools=tools)
yield agent
timeout=httpx2.Timeout(60),
follow_redirects=True,
) as http_client:
async with streamable_http_client(
url=UIPATH_MCP_SERVER_URL,
http_client=http_client,
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
model = UiPathChat(model="anthropic.claude-3-5-sonnet-20240620-v1:0")
agent = create_agent(model, tools=tools)
yield agent


async def connect_to_mcp(state: State) -> Command:
try:
async with agent_mcp(state.access_token) as agent:
agent_response = await agent.ainvoke({
"messages": [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content=state.task),
],
})
agent_response = await agent.ainvoke(
{
"messages": [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content=state.task),
],
}
)
return Command(update={"result": agent_response["messages"][-1].content})
except ExceptionGroup as e:
for error in e.exceptions:
if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 401:
if (
isinstance(error, httpx2.HTTPStatusError)
and error.response.status_code == 401
):
return Command(update={"access_token": None})
raise


def route_start(state: State) -> Literal["fetch_new_access_token", "connect_to_mcp"]:
return "fetch_new_access_token" if state.access_token is None else "connect_to_mcp"


def route_after_connect(state: State):
return "fetch_new_access_token" if state.access_token is None else END


builder = StateGraph(State, input=GraphInput, output=GraphOutput)
builder.add_node("fetch_new_access_token", fetch_new_access_token)
builder.add_node("connect_to_mcp", connect_to_mcp)
Expand Down
3 changes: 2 additions & 1 deletion samples/oauth-external-apps-agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ dependencies = [
"langgraph>=1.0.4",
"python-dotenv>=1.0.0",
"anthropic>=0.57.1",
"langchain-mcp-adapters>=0.1.14",
"httpx2>=2.5.0,<2.10.0",
"mcp==2.0.0",
"mypy>=1.17.1",
"uipath",
"uipath-langchain",
Expand Down
4 changes: 2 additions & 2 deletions samples/simple-local-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The workflow follows a ReAct pattern:

- Python 3.11+
- `langchain-anthropic`
- `langchain-mcp-adapters`
- MCP Python SDK 2.0
- `langgraph`
- Anthropic API key set as an environment variable

Expand Down Expand Up @@ -91,5 +91,5 @@ For debugging issues:
To add a new tool:

1. Create a new MCP-compatible server (similar to math_server.py)
2. Add it to the MultiServerMCPClient configuration dictionary
2. Add its script to the server list in `make_graph`
3. The agent will automatically discover and use the new tool's capabilities
4 changes: 1 addition & 3 deletions samples/simple-local-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ description = "Math and Weather Local MCP Server Agent"
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
dependencies = [
"langchain-anthropic>=1.2.0",
"langchain-mcp-adapters>=0.1.14",
"mcp>=1.15.0",
"mcp==2.0.0",
"uipath",
"uipath-langchain",
]
Expand All @@ -16,4 +15,3 @@ requires-python = ">=3.11"
dev = [
"uipath-dev",
]

40 changes: 23 additions & 17 deletions samples/simple-local-mcp/src/simple-local-mcp/graph.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import sys
from contextlib import asynccontextmanager
from contextlib import AsyncExitStack, asynccontextmanager

from langchain_anthropic import ChatAnthropic
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from mcp import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client

from uipath_langchain.agent.tools.mcp import load_mcp_tools

model = ChatAnthropic(model="claude-3-7-sonnet-latest")


@asynccontextmanager
async def make_graph():
client = MultiServerMCPClient({
"math": {
"command": sys.executable,
"args": ["src/simple-local-mcp/math_server.py"],
"transport": "stdio",
},
"weather": {
"command": sys.executable,
"args": ["src/simple-local-mcp/weather_server.py"],
"transport": "stdio",
},
})
agent = create_agent(model, await client.get_tools())
yield agent
async with AsyncExitStack() as stack:
tools = []
for script in ("math_server.py", "weather_server.py"):
read, write = await stack.enter_async_context(
stdio_client(
StdioServerParameters(
command=sys.executable,
args=[f"src/simple-local-mcp/{script}"],
)
)
)
session = await stack.enter_async_context(ClientSession(read, write))
await session.initialize()
tools.extend(await load_mcp_tools(session))

agent = create_agent(model, tools=tools)
yield agent
4 changes: 2 additions & 2 deletions samples/simple-local-mcp/src/simple-local-mcp/math_server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import logging

from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

mcp = FastMCP("Math")
mcp = MCPServer("Math")

@mcp.tool()
def add(a: int, b: int) -> int:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import logging

from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import MCPServer

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

mcp = FastMCP("Weather")
mcp = MCPServer("Weather")

@mcp.tool()
async def get_weather(location: str) -> str:
Expand Down
3 changes: 1 addition & 2 deletions samples/simple-remote-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The workflow follows a ReAct pattern:

- Python 3.11+
- `langchain-anthropic`
- `langchain-mcp-adapters`
- MCP Python SDK 2.0
- `langgraph`
- Anthropic API key set as an environment variable

Expand Down Expand Up @@ -70,4 +70,3 @@ For debugging issues:
uipath run agent --debug '{"messages": [{"type": "human", "content": "What is 2+2"}]}'
```


37 changes: 22 additions & 15 deletions samples/simple-remote-mcp/main.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import os
from typing import Any
from langgraph.graph import StateGraph, MessagesState, START, END

import httpx2
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.graph import END, START, MessagesState, StateGraph
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.streamable_http import streamable_http_client

from uipath_langchain.agent.tools.mcp import load_mcp_tools


async def mcp_client(state: MessagesState) -> dict[str, Any]:
"""Agent node that connects to MCP server and processes messages."""
async with streamablehttp_client(
url=os.getenv("UIPATH_MCP_SERVER_URL"),
async with httpx2.AsyncClient(
headers={"Authorization": f"Bearer {os.getenv('UIPATH_ACCESS_TOKEN')}"},
timeout=60,
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
print(f"Loaded {len(tools)} tools from MCP server")
model = ChatAnthropic(model="claude-3-7-sonnet-latest")
agent = create_agent(model, tools=tools)
result = await agent.ainvoke(state)
return result
timeout=httpx2.Timeout(60),
follow_redirects=True,
) as http_client:
async with streamable_http_client(
url=os.environ["UIPATH_MCP_SERVER_URL"],
http_client=http_client,
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
print(f"Loaded {len(tools)} tools from MCP server")
model = ChatAnthropic(model="claude-3-7-sonnet-latest")
agent = create_agent(model, tools=tools)
result = await agent.ainvoke(state)
return result


builder = StateGraph(MessagesState)
Expand Down
3 changes: 2 additions & 1 deletion samples/simple-remote-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ dependencies = [
"langgraph>=1.0.4",
"python-dotenv>=1.0.0",
"anthropic>=0.57.1",
"langchain-mcp-adapters>=0.1.14",
"httpx2>=2.5.0,<2.10.0",
"mcp==2.0.0",
"uipath",
"uipath-langchain",
]
Expand Down
2 changes: 2 additions & 0 deletions src/uipath_langchain/agent/tools/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
create_mcp_tools_and_clients,
open_mcp_tools,
)
from .session_tools import load_mcp_tools
from .streamable_http import SessionInfo

__all__ = [
Expand All @@ -15,4 +16,5 @@
"create_mcp_tools_and_clients",
"open_mcp_tools",
"create_mcp_tools",
"load_mcp_tools",
]
Loading
Loading