pass roles to mcp

This commit is contained in:
2025-12-08 19:02:12 +01:00
parent fbb050056f
commit 49ea3c1a99
8 changed files with 84 additions and 59 deletions

View File

@@ -1,10 +1,11 @@
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.dependencies import get_http_headers
from psycopg.rows import dict_row
import cohere
import dotenv
import psycopg
import os
import json
dotenv.load_dotenv('/home/pew/scripts-private/loser/cavepedia-v2/poller.env')
@@ -20,19 +21,21 @@ conn = psycopg.connect(
row_factory=dict_row,
)
# The Auth0Provider utilizes Auth0 OIDC configuration
auth_provider = Auth0Provider(
config_url="https://dev-jao4so0av61ny4mr.us.auth0.com/.well-known/openid-configuration",
client_id="oONcxma5PNFwYLhrDC4o0PUuAmqDekzM",
client_secret="4Z7Wl12ALEtDmNAoERQe7lK2YD9x6jz7H25FiMxRp518dnag-IS2NLLScnmbe4-b",
audience="https://dev-jao4so0av61ny4mr.us.auth0.com/me/",
base_url="https://mcp.caving.dev",
# redirect_path="/auth/callback" # Default value, customize if needed
)
mcp = FastMCP("Cavepedia MCP")
def get_user_roles() -> list[str]:
"""Extract user roles from the X-User-Roles header."""
headers = get_http_headers()
print(f"[MCP] All headers: {dict(headers)}")
roles_header = headers.get("x-user-roles", "")
print(f"[MCP] X-User-Roles header: {roles_header}")
if roles_header:
try:
return json.loads(roles_header)
except json.JSONDecodeError:
return []
return []
def embed(text, input_type):
resp = co.embed(
texts=[text],
@@ -54,25 +57,23 @@ def search(query) -> list[dict]:
@mcp.tool
def get_cave_location(cave: str, state: str, county: str) -> list[dict]:
"""Lookup cave location as coordinates. Returns up to 5 matches, ordered by most to least relevant."""
roles = get_user_roles()
print(f"get_cave_location called with roles: {roles}")
return search(f'{cave} Location, latitude, Longitude. Located in {state} and {county} county.')
@mcp.tool
def general_caving_information(query: str) -> list[dict]:
"""General purpose endpoint for any topic related to caves. Returns up to 5 mates, orderd by most to least relevant."""
"""General purpose endpoint for any topic related to caves. Returns up to 5 matches, ordered by most to least relevant."""
roles = get_user_roles()
print(f"general_caving_information called with roles: {roles}")
return search(query)
# Add a protected tool to test authentication
@mcp.tool
async def get_token_info() -> dict:
"""Returns information about the Auth0 token."""
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
def get_user_info() -> dict:
"""Get information about the current user's roles."""
roles = get_user_roles()
return {
"issuer": token.claims.get("iss"),
"audience": token.claims.get("aud"),
"scope": token.claims.get("scope")
"roles": roles,
}
if __name__ == "__main__":

View File

@@ -3,7 +3,8 @@ This is the main entry point for the agent.
It defines the workflow graph, state, tools, nodes and edges.
"""
from typing import Any, List
from typing import Any, List, Callable, Awaitable
import json
from langchain.tools import tool
from langchain_core.messages import BaseMessage, SystemMessage
@@ -13,6 +14,7 @@ from langgraph.graph import END, MessagesState, StateGraph, START
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.types import Command
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest, MCPToolCallResult
class AgentState(MessagesState):
@@ -37,41 +39,55 @@ backend_tools = [
# your_tool_here
]
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}"
class RolesHeaderInterceptor:
"""Interceptor that injects user roles header into MCP tool calls."""
def __init__(self, user_roles: list = None):
self.user_roles = user_roles or []
async def __call__(
self,
request: MCPToolCallRequest,
handler: Callable[[MCPToolCallRequest], Awaitable[MCPToolCallResult]]
) -> MCPToolCallResult:
headers = dict(request.headers or {})
if self.user_roles:
headers["X-User-Roles"] = json.dumps(self.user_roles)
modified_request = request.override(headers=headers)
return await handler(modified_request)
def get_mcp_client(user_roles: list = None):
"""Create MCP client with user roles header."""
return MultiServerMCPClient(
{
"cavepedia": {
"transport": "streamable_http",
"url": "https://mcp.caving.dev/mcp",
"timeout": 10.0,
"headers": headers,
}
}
},
tool_interceptors=[RolesHeaderInterceptor(user_roles)]
)
# Cache for MCP tools per access token
_mcp_tools_cache = {}
async def get_mcp_tools(access_token: str = None):
"""Lazy load MCP tools with authentication."""
cache_key = access_token or "default"
async def get_mcp_tools(user_roles: list = None):
"""Lazy load MCP tools with user roles."""
roles_key = ",".join(sorted(user_roles)) if user_roles else "default"
if cache_key not in _mcp_tools_cache:
if roles_key not in _mcp_tools_cache:
try:
mcp_client = get_mcp_client(access_token)
mcp_client = get_mcp_client(user_roles)
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)}")
_mcp_tools_cache[roles_key] = tools
print(f"Loaded {len(tools)} tools from MCP server with roles: {user_roles}")
except Exception as e:
print(f"Warning: Failed to load MCP tools: {e}")
_mcp_tools_cache[cache_key] = []
_mcp_tools_cache[roles_key] = []
return _mcp_tools_cache[cache_key]
return _mcp_tools_cache[roles_key]
async def chat_node(state: AgentState, config: RunnableConfig) -> dict:
@@ -86,18 +102,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
# 0. Extract user roles from config.configurable.context
configurable = config.get("configurable", {})
access_token = configurable.get("auth0_access_token")
user_roles = configurable.get("auth0_user_roles", [])
context = configurable.get("context", {})
user_roles = context.get("auth0_user_roles", [])
print(f"Chat node invoked with auth token: {bool(access_token)}, roles: {user_roles}")
print(f"Chat node invoked with roles: {user_roles}")
# 1. Define the model
model = ChatAnthropic(model="claude-sonnet-4-5-20250929")
# 1.5 Load MCP tools from the cavepedia server with authentication
mcp_tools = await get_mcp_tools(access_token)
# 1.5 Load MCP tools from the cavepedia server with roles
mcp_tools = await get_mcp_tools(user_roles)
# 2. Bind the tools to the model
model_with_tools = model.bind_tools(
@@ -135,12 +151,13 @@ async def tool_node_wrapper(state: AgentState, config: RunnableConfig) -> dict:
"""
Custom tool node that handles both backend tools and MCP tools.
"""
# Extract Auth0 access token from config
# Extract user roles from config.configurable.context
configurable = config.get("configurable", {})
access_token = configurable.get("auth0_access_token")
context = configurable.get("context", {})
user_roles = context.get("auth0_user_roles", [])
# Load MCP tools with authentication and combine with backend tools
mcp_tools = await get_mcp_tools(access_token)
# Load MCP tools with roles
mcp_tools = await get_mcp_tools(user_roles)
all_tools = [*backend_tools, *mcp_tools]
# Use the standard ToolNode with all tools

View File

@@ -14,4 +14,6 @@ dependencies = [
"langgraph-cli[inmem]>=0.4.7",
"langchain-anthropic>=0.3.3",
"langchain-mcp-adapters>=0.1.0",
"docstring-parser>=0.17.0",
"jsonschema>=4.25.1",
]

4
web/agent/uv.lock generated
View File

@@ -1362,7 +1362,9 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "anthropic" },
{ name = "docstring-parser" },
{ name = "fastapi" },
{ name = "jsonschema" },
{ name = "langchain" },
{ name = "langchain-anthropic" },
{ name = "langchain-mcp-adapters" },
@@ -1376,7 +1378,9 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "anthropic", specifier = ">=0.40.0" },
{ name = "docstring-parser", specifier = ">=0.17.0" },
{ name = "fastapi", specifier = ">=0.115.5,<1.0.0" },
{ name = "jsonschema", specifier = ">=4.25.1" },
{ name = "langchain", specifier = "==1.1.0" },
{ name = "langchain-anthropic", specifier = ">=0.3.3" },
{ name = "langchain-mcp-adapters", specifier = ">=0.1.0" },

View File

@@ -6,9 +6,9 @@
"dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
"dev:debug": "LOG_LEVEL=debug npm run dev",
"dev:agent": "cd agent && npx @langchain/langgraph-cli dev --port 8123 --no-browser",
"dev:ui": "next dev --turbopack",
"dev:ui": "next dev --turbopack -H 127.0.0.1",
"build": "next build",
"start": "next start",
"start": "next start -H 127.0.0.1",
"lint": "eslint .",
"install:agent": "sh ./scripts/setup-agent.sh || scripts\\setup-agent.bat",
"postinstall": "npm run install:agent"

View File

@@ -17,10 +17,12 @@ 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;
// Extract roles from session
const userRoles = session?.user?.roles || [];
console.log("[copilotkit] session exists:", !!session);
console.log("[copilotkit] userRoles:", userRoles);
// 2. Create the CopilotRuntime instance with Auth0 configuration
const runtime = new CopilotRuntime({
agents: {
@@ -28,9 +30,8 @@ export const POST = async (req: NextRequest) => {
deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
graphId: "sample_agent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
langgraphConfig: {
configurable: {
auth0_access_token: accessToken,
assistantConfig: {
context: {
auth0_user_roles: userRoles,
}
}

View File

@@ -95,7 +95,7 @@ export default function CopilotKitPage() {
<div className="flex-1 flex justify-center py-8 px-2 overflow-hidden">
<div className="h-full w-full max-w-5xl flex flex-col">
<CopilotChat
instructions={"You are a knowledgeable caving assistant. Help users with all aspects of caving including cave exploration, safety, surveying techniques, cave locations, geology, equipment, history, conservation, and any other caving-related topics. Provide accurate, helpful, and safety-conscious information."}
instructions={"You are a knowledgeable caving assistant. Help users with all aspects of caving including cave exploration, safety, surveying techniques, cave locations, geology, equipment, history, conservation, and any other caving-related topics. Provide accurate, helpful, and safety-conscious information. CRITICAL: Always cite sources at the end of each response."}
labels={{
title: "AI Cartwright",
initial: "Hello! I'm here to help with anything related to caving. Ask me about caves, techniques, safety, equipment, or anything else caving-related!",

View File

@@ -1,7 +1,7 @@
import { Auth0Client, filterDefaultIdTokenClaims } from '@auth0/nextjs-auth0/server';
export const auth0 = new Auth0Client({
async beforeSessionSaved(session, idToken) {
async beforeSessionSaved(session) {
return {
...session,
user: {