pass roles to mcp
This commit is contained in:
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
4
web/agent/uv.lock
generated
@@ -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" },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user