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
3 changes: 3 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ This sample shows how to create an agent using LangGraph with multiple tool serv
## [Simple remote MCP](simple-remote-mcp)
This sample demonstrates the creation of an agent using LangGraph, which connects to a remote MCP (Model Context Protocol) Server.

## [OAuth external apps agent](oauth-external-apps-agent)
This sample shows how to build a LangGraph agent that connects to a remote UiPath MCP server using OAuth authentication through an external UiPath application for dynamic access token management.

## [Ticket classification](ticket-classification)
This sample demonstrates automatic classification of support tickets into categories. It includes a human approval step via UiPath Action Center.
Empty file.
84 changes: 84 additions & 0 deletions samples/oauth-external-apps-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# LangGraph Agent with OAuth and MCP Servers

This project demonstrates how to build a LangGraph agent that connects to a remote UiPath MCP server, using an external UiPath application to handle OAuth authentication.

## Overview

The agent uses:
- UiPathChat with Claude 3.5 Sonnet as the language model
- LangGraph for orchestration
- Remote tool execution via UiPath MCP server
- Dynamic access token refresh through client credentials grant (OAuth)

## Architecture

```mermaid
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
fetch_new_access_token(fetch_new_access_token)
connect_to_mcp(connect_to_mcp)
__end__([<p>__end__</p>]):::last
__start__ -.-> connect_to_mcp;
__start__ -.-> fetch_new_access_token;
fetch_new_access_token --> connect_to_mcp;
connect_to_mcp --> __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
```

The workflow follows a ReAct pattern:
1. Query is sent to the agent
2. The agent first attempts to connect to the remote MCP server using the current `UIPATH_ACCESS_TOKEN`.
3. If the token is missing or expired, the workflow triggers a call to the UiPath external application endpoint to fetch a fresh token and stores it in the environment.
4. Once connected, the MCP tools are loaded into the ReAct agent.
5. The agent receives both system context and the human task, deciding whether to invoke tools or provide a direct answer.
6. The process repeats until the agent has enough information to produce a final response.
7. The response is collected and returned as the workflow output.

## Prerequisites

- Python 3.10+
- `uipath-langchain`
- `langchain-mcp-adapters`
- `langgraph`
- `httpx`
- `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)

## Installation

```bash
uv venv -p 3.11 .venv
.venv\Scripts\activate
uv sync
```

Set your MCP Remote Server URL and client secret as environment variables in .env

```bash
UIPATH_CLIENT_SECRET=your_client_secret
UIPATH_MCP_SERVER_URL=https://your-uipath-instance/account/tenant/mcp_/mcp/server_slug
```

Configure the following constants in `main.py`:
- `UIPATH_CLIENT_ID` - Your external application client ID
- `UIPATH_URL` - Your UiPath instance base URL
- `UIPATH_SCOPE` - OAuth scope (default: `OR.Jobs`)

## Debugging

For debugging issues:

1. Check logs for any connection or runtime errors:
```bash
uipath run agent --debug '{"task": "What is 2 + 2?"}'
```


1 change: 1 addition & 0 deletions samples/oauth-external-apps-agent/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "task": "What is 2+2" }
7 changes: 7 additions & 0 deletions samples/oauth-external-apps-agent/langgraph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": ["."],
"graphs": {
"agent": "./main.py:graph"
},
"env": ".env"
}
95 changes: 95 additions & 0 deletions samples/oauth-external-apps-agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import dotenv
import httpx
from contextlib import asynccontextmanager
from typing import Optional, Literal

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
from langgraph.prebuilt import create_react_agent
from langchain.schema import SystemMessage, HumanMessage

from uipath_langchain.chat.models import UiPathChat
from langchain_mcp_adapters.tools import load_mcp_tools
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from uipath import UiPath

dotenv.load_dotenv()

UIPATH_CLIENT_ID = "EXTERNAL_APP_CLIENT_ID_HERE"
UIPATH_CLIENT_SECRET = os.getenv("UIPATH_CLIENT_SECRET")
UIPATH_SCOPE = "OR.Jobs"
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(
base_url=UIPATH_URL,
client_id=UIPATH_CLIENT_ID,
client_secret=UIPATH_CLIENT_SECRET,
scope=UIPATH_SCOPE,
)
return Command(update={"access_token": os.getenv("UIPATH_ACCESS_TOKEN")})

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

@asynccontextmanager
async def agent_mcp(access_token: str):
async with streamablehttp_client(
url=UIPATH_MCP_SERVER_URL,
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_react_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),
],
})
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:
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)

builder.add_conditional_edges(START, route_start)
builder.add_edge("fetch_new_access_token", "connect_to_mcp")
builder.add_conditional_edges("connect_to_mcp", route_after_connect)

graph = builder.compile()
17 changes: 17 additions & 0 deletions samples/oauth-external-apps-agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[project]
name = "oauth-external-apps-agent"
version = "0.0.7"
description = "Agent that connects to an MCP server implementing OAuth using an external application."
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
dependencies = [
"uipath-langchain>=0.0.140",
"langchain>=0.3.26",
"langchain-anthropic>=0.3.17",
"langgraph>=0.5.3",
"python-dotenv>=1.0.0",
"anthropic>=0.57.1",
"langchain-mcp-adapters>=0.1.9",
"mypy>=1.17.1",
"uipath>=2.1.38",
]
requires-python = ">=3.10"
Loading