From fbb050056fc2938727dc3bfec8a06745a9adb77b Mon Sep 17 00:00:00 2001 From: Paul Walko Date: Sun, 7 Dec 2025 20:25:53 -0700 Subject: [PATCH] mcp auth --- web/agent/main.py | 73 +++++++++++++++++++---------- web/src/app/api/copilotkit/route.ts | 45 +++++++++++------- 2 files changed, 76 insertions(+), 42 deletions(-) diff --git a/web/agent/main.py b/web/agent/main.py index fe3fc30..d4998ac 100644 --- a/web/agent/main.py +++ b/web/agent/main.py @@ -37,31 +37,41 @@ backend_tools = [ # your_tool_here ] -# Initialize MCP client -mcp_client = MultiServerMCPClient( - { - "cavepedia": { - "transport": "streamable_http", - "url": "https://mcp.caving.dev/mcp", - "timeout": 10.0, +def get_mcp_client(access_token: str = None): + """Create MCP client with optional authentication headers.""" + headers = {} + if access_token: + headers["Authorization"] = f"Bearer {access_token}" + + return MultiServerMCPClient( + { + "cavepedia": { + "transport": "streamable_http", + "url": "https://mcp.caving.dev/mcp", + "timeout": 10.0, + "headers": headers, + } } - } -) + ) -# Global variable to hold loaded MCP tools -_mcp_tools = None +# Cache for MCP tools per access token +_mcp_tools_cache = {} -async def get_mcp_tools(): - """Lazy load MCP tools on first access.""" - global _mcp_tools - if _mcp_tools is None: +async def get_mcp_tools(access_token: str = None): + """Lazy load MCP tools with authentication.""" + cache_key = access_token or "default" + + if cache_key not in _mcp_tools_cache: try: - _mcp_tools = await mcp_client.get_tools() - print(f"Loaded {len(_mcp_tools)} tools from MCP server") + mcp_client = get_mcp_client(access_token) + tools = await mcp_client.get_tools() + _mcp_tools_cache[cache_key] = tools + print(f"Loaded {len(tools)} tools from MCP server with auth: {bool(access_token)}") except Exception as e: print(f"Warning: Failed to load MCP tools: {e}") - _mcp_tools = [] - return _mcp_tools + _mcp_tools_cache[cache_key] = [] + + return _mcp_tools_cache[cache_key] async def chat_node(state: AgentState, config: RunnableConfig) -> dict: @@ -76,11 +86,18 @@ async def chat_node(state: AgentState, config: RunnableConfig) -> dict: https://www.perplexity.ai/search/react-agents-NcXLQhreS0WDzpVaS4m9Cg """ + # 0. Extract Auth0 access token from config + configurable = config.get("configurable", {}) + access_token = configurable.get("auth0_access_token") + user_roles = configurable.get("auth0_user_roles", []) + + print(f"Chat node invoked with auth token: {bool(access_token)}, roles: {user_roles}") + # 1. Define the model model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - # 1.5 Load MCP tools from the cavepedia server - mcp_tools = await get_mcp_tools() + # 1.5 Load MCP tools from the cavepedia server with authentication + mcp_tools = await get_mcp_tools(access_token) # 2. Bind the tools to the model model_with_tools = model.bind_tools( @@ -98,7 +115,7 @@ async def chat_node(state: AgentState, config: RunnableConfig) -> dict: # 3. Define the system message by which the chat model will be run system_message = SystemMessage( - content="You are a helpful assistant with access to cave-related information through the Cavepedia MCP server. You can help users find information about caves, caving techniques, and related topics." + content=f"You are a helpful assistant with access to cave-related information through the Cavepedia MCP server. You can help users find information about caves, caving techniques, and related topics. User roles: {', '.join(user_roles) if user_roles else 'none'}" ) # 4. Run the model to generate a response @@ -114,17 +131,21 @@ async def chat_node(state: AgentState, config: RunnableConfig) -> dict: return {"messages": [response]} -async def tool_node_wrapper(state: AgentState) -> dict: +async def tool_node_wrapper(state: AgentState, config: RunnableConfig) -> dict: """ Custom tool node that handles both backend tools and MCP tools. """ - # Load MCP tools and combine with backend tools - mcp_tools = await get_mcp_tools() + # Extract Auth0 access token from config + configurable = config.get("configurable", {}) + access_token = configurable.get("auth0_access_token") + + # Load MCP tools with authentication and combine with backend tools + mcp_tools = await get_mcp_tools(access_token) all_tools = [*backend_tools, *mcp_tools] # Use the standard ToolNode with all tools node = ToolNode(tools=all_tools) - result = await node.ainvoke(state) + result = await node.ainvoke(state, config) return result diff --git a/web/src/app/api/copilotkit/route.ts b/web/src/app/api/copilotkit/route.ts index 4c98186..a1f1757 100644 --- a/web/src/app/api/copilotkit/route.ts +++ b/web/src/app/api/copilotkit/route.ts @@ -6,30 +6,43 @@ import { import { LangGraphAgent } from "@ag-ui/langgraph" import { NextRequest } from "next/server"; - +import { auth0 } from "@/lib/auth0"; + // 1. You can use any service adapter here for multi-agent support. We use // the empty adapter since we're only using one agent. const serviceAdapter = new ExperimentalEmptyAdapter(); - -// 2. Create the CopilotRuntime instance and utilize the LangGraph AG-UI -// integration to setup the connection. -const runtime = new CopilotRuntime({ - agents: { - "sample_agent": new LangGraphAgent({ - deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123", - graphId: "sample_agent", - langsmithApiKey: process.env.LANGSMITH_API_KEY || "", - }), - } -}); - + // 3. Build a Next.js API route that handles the CopilotKit runtime requests. export const POST = async (req: NextRequest) => { + // Get Auth0 session + const session = await auth0.getSession(); + + // Extract access token and roles from session + const accessToken = session?.accessToken; + const userRoles = session?.user?.roles || []; + + // 2. Create the CopilotRuntime instance with Auth0 configuration + const runtime = new CopilotRuntime({ + agents: { + "sample_agent": new LangGraphAgent({ + deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123", + graphId: "sample_agent", + langsmithApiKey: process.env.LANGSMITH_API_KEY || "", + langgraphConfig: { + configurable: { + auth0_access_token: accessToken, + auth0_user_roles: userRoles, + } + } + }), + } + }); + const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ - runtime, + runtime, serviceAdapter, endpoint: "/api/copilotkit", }); - + return handleRequest(req); }; \ No newline at end of file