Skip to main content
Glama

API Registry MCP Server

ChatPageAgent.tsx77.8 kB
import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Send, Loader2, Sparkles, Database, Search, TestTube, FileJson, Home, Plus, Wrench, HelpCircle, Activity, Copy, Check, Edit2, AlertCircle, User, Bot, } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; import DOMPurify from "dompurify"; import { marked } from "marked"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DatabaseService } from "@/fastapi_client"; interface Model { id: string; name: string; provider: string; supports_tools: boolean; context_window: number; type: string; } interface Warehouse { id: string; name: string; state: string; size?: string; type?: string; } interface CatalogSchema { catalog_name: string; schema_name: string; full_name: string; comment?: string; } interface Message { role: "user" | "assistant"; content: string; tool_calls?: Array<{ tool: string; args: any; result: any; }>; trace_id?: string; // MLflow trace ID for "View Trace" link } interface ChatPageAgentProps { onViewTrace?: (traceId: string) => void; selectedWarehouse: string; setSelectedWarehouse: (value: string) => void; selectedCatalogSchema: string; setSelectedCatalogSchema: (value: string) => void; } export function ChatPageAgent({ onViewTrace, selectedWarehouse, setSelectedWarehouse, selectedCatalogSchema, setSelectedCatalogSchema, }: ChatPageAgentProps) { const [models, setModels] = useState<Model[]>([]); const [selectedModel, setSelectedModel] = useState<string>(""); const [warehouses, setWarehouses] = useState<Warehouse[]>([]); const [warehouseFilter, setWarehouseFilter] = useState<string>(""); const [debouncedWarehouseFilter, setDebouncedWarehouseFilter] = useState<string>(""); // TWO-DROPDOWN ARCHITECTURE: Separate catalog and schema const [catalogs, setCatalogs] = useState<{name: string; comment?: string}[]>([]); const [schemas, setSchemas] = useState<{name: string; comment?: string}[]>([]); const [selectedCatalog, setSelectedCatalog] = useState<string>(""); const [selectedSchema, setSelectedSchema] = useState<string>(""); const [catalogFilter, setCatalogFilter] = useState<string>(""); const [schemaFilter, setSchemaFilter] = useState<string>(""); const [debouncedCatalogFilter, setDebouncedCatalogFilter] = useState<string>(""); const [debouncedSchemaFilter, setDebouncedSchemaFilter] = useState<string>(""); const [tableValidation, setTableValidation] = useState<{ exists: boolean; error?: string; message?: string; checking: boolean; }>({ exists: true, checking: false }); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [systemPrompt, setSystemPrompt] = useState<string>(""); const [showSystemPrompt, setShowSystemPrompt] = useState(false); const [tempSystemPrompt, setTempSystemPrompt] = useState<string>(""); const [copiedIndex, setCopiedIndex] = useState<number | null>(null); const [editingIndex, setEditingIndex] = useState<number | null>(null); const [editingContent, setEditingContent] = useState<string>(""); const [showCredentialDialog, setShowCredentialDialog] = useState(false); const [credentialType, setCredentialType] = useState<"api_key" | "bearer_token">("api_key"); const [credentialValue, setCredentialValue] = useState<string>(""); const [pendingApiName, setPendingApiName] = useState<string>(""); const [pendingEndpoints, setPendingEndpoints] = useState<Array<{ path: string; description: string; method: string; }>>([]); const [selectedEndpoints, setSelectedEndpoints] = useState<Set<number>>(new Set()); const [endpointRegistrationData, setEndpointRegistrationData] = useState<{ api_name?: string; host?: string; base_path?: string; auth_type?: string; } | null>(null); // Secure credential storage - stored in session, not in messages! const [storedCredentials, setStoredCredentials] = useState<{ api_key?: string; bearer_token?: string; }>({}); const textareaRef = useRef<HTMLTextAreaElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null); const { theme } = useTheme(); // Sync separate catalog + schema to parent's combined state useEffect(() => { if (selectedCatalog && selectedSchema) { const combined = `${selectedCatalog}.${selectedSchema}`; if (combined !== selectedCatalogSchema) { setSelectedCatalogSchema(combined); } } }, [selectedCatalog, selectedSchema]); // Parse parent's combined state on mount/change useEffect(() => { if (selectedCatalogSchema && selectedCatalogSchema.includes('.')) { const [catalog, schema] = selectedCatalogSchema.split('.'); if (catalog !== selectedCatalog) setSelectedCatalog(catalog); if (schema !== selectedSchema) setSelectedSchema(schema); } }, [selectedCatalogSchema]); // Fetch schemas when catalog changes useEffect(() => { if (selectedCatalog) { fetchSchemas(selectedCatalog); } else { setSchemas([]); setSelectedSchema(""); } }, [selectedCatalog]); // Server-side search with debounce - WAREHOUSES useEffect(() => { const timer = setTimeout(() => { if (warehouseFilter !== debouncedWarehouseFilter) { setDebouncedWarehouseFilter(warehouseFilter); fetchWarehouses(warehouseFilter); } }, 750); return () => clearTimeout(timer); }, [warehouseFilter]); // Server-side search with debounce - CATALOGS useEffect(() => { const timer = setTimeout(() => { if (catalogFilter !== debouncedCatalogFilter) { setDebouncedCatalogFilter(catalogFilter); fetchCatalogs(catalogFilter); } }, 750); return () => clearTimeout(timer); }, [catalogFilter]); // Server-side search with debounce - SCHEMAS useEffect(() => { const timer = setTimeout(() => { if (schemaFilter !== debouncedSchemaFilter) { setDebouncedSchemaFilter(schemaFilter); if (selectedCatalog) { fetchSchemas(selectedCatalog, schemaFilter); } } }, 750); return () => clearTimeout(timer); }, [schemaFilter]); // No client-side filtering needed - using server-side filtering const filteredWarehouses = warehouses; const filteredCatalogs = catalogs; const filteredSchemas = schemas; useEffect(() => { fetchModels(); fetchWarehouses(); // Initial load without search fetchCatalogs(); // Load catalogs first }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const fetchModels = async () => { try { const response = await fetch("/api/chat/models"); const data = await response.json(); setModels(data.models); setSelectedModel(data.default); } catch (error) { console.error("Failed to fetch models:", error); } }; const fetchWarehouses = async (search?: string) => { try { console.log(`🔍 Fetching warehouses${search ? ` (search: "${search}")` : ''}...`); const queryParams = new URLSearchParams(); if (search) queryParams.append('search', search); const response = await fetch(`/api/db/warehouses?${queryParams}`); const data = await response.json(); console.log("✅ Warehouses data:", data); setWarehouses(data.warehouses || []); // Set first warehouse as default if available and not already set if (data.warehouses && data.warehouses.length > 0 && !selectedWarehouse) { console.log(`📊 Setting default warehouse: ${data.warehouses[0].name} (${data.warehouses[0].id})`); setSelectedWarehouse(data.warehouses[0].id); } else if (!data.warehouses || data.warehouses.length === 0) { console.warn("⚠️ No warehouses returned from API"); } } catch (error) { console.error("❌ Failed to fetch warehouses:", error); setWarehouses([]); } }; const fetchCatalogs = async (search?: string) => { try { console.log(`🔍 Fetching catalogs${search ? ` (search: "${search}")` : ''}...`); const response = await fetch('/api/db/catalogs'); const data = await response.json(); console.log("✅ Catalogs data:", data); const catalogList = data.catalogs || []; // Apply client-side search filter if provided const filtered = search ? catalogList.filter((c: any) => c.name.toLowerCase().includes(search.toLowerCase())) : catalogList; setCatalogs(filtered); // Set first catalog as default if available and not already set if (filtered.length > 0 && !selectedCatalog) { console.log(`📊 Setting default catalog: ${filtered[0].name}`); setSelectedCatalog(filtered[0].name); } else if (filtered.length === 0) { console.warn("⚠️ No catalogs returned from API"); } } catch (error) { console.error("❌ Failed to fetch catalogs:", error); setCatalogs([]); } }; const fetchSchemas = async (catalogName: string, search?: string) => { try { console.log(`🔍 Fetching schemas for catalog "${catalogName}"${search ? ` (search: "${search}")` : ''}...`); const response = await fetch(`/api/db/schemas/${encodeURIComponent(catalogName)}`); const data = await response.json(); console.log("✅ Schemas data:", data); const schemaList = data.schemas || []; // Apply client-side search filter if provided const filtered = search ? schemaList.filter((s: any) => s.name.toLowerCase().includes(search.toLowerCase())) : schemaList; setSchemas(filtered); // Set first schema as default if available and not already set if (filtered.length > 0 && !selectedSchema) { console.log(`📊 Setting default schema: ${filtered[0].name}`); setSelectedSchema(filtered[0].name); } else if (filtered.length === 0) { console.warn(`⚠️ No schemas found in catalog "${catalogName}"`); } } catch (error) { console.error(`❌ Failed to fetch schemas for catalog "${catalogName}":`, error); setSchemas([]); } }; // DEPRECATED: Old combined fetch - keeping for reference, will remove const fetchCatalogSchemas_OLD = async (search?: string, limit: number = 100) => { try { console.log(`🔍 Fetching catalog schemas${search ? ` (search: "${search}")` : ''} (limit: ${limit})...`); // Use fetch with query params for server-side filtering and limiting const queryParams = new URLSearchParams(); queryParams.append('limit', limit.toString()); if (search) queryParams.append('search', search); const response = await fetch(`/api/db/catalog-schemas?${queryParams}`); const data = await response.json(); console.log("✅ Catalog schemas data:", data); setCatalogSchemas(data.catalog_schemas || []); // Warn if there are more results if (data.has_more) { console.log(`ℹ️ Showing first ${data.count} results. Use search to narrow down.`); } // Set first catalog.schema as default if available and not already set if (data.catalog_schemas && data.catalog_schemas.length > 0 && !selectedCatalogSchema) { console.log(`📊 Setting default catalog.schema: ${data.catalog_schemas[0].full_name}`); setSelectedCatalogSchema(data.catalog_schemas[0].full_name); } else if (!data.catalog_schemas || data.catalog_schemas.length === 0) { console.warn("⚠️ No catalog schemas returned from API"); } } catch (error) { console.error("❌ Failed to fetch catalog schemas:", error); setCatalogSchemas([]); } }; const validateApiRegistryTable = async (catalog: string, schema: string, warehouseId: string) => { if (!catalog || !schema || !warehouseId) { setTableValidation({ exists: false, message: "Please select warehouse, catalog, and schema", checking: false }); return; } setTableValidation({ exists: true, checking: true }); try { const data = await DatabaseService.validateApiRegistryTableApiDbValidateApiRegistryTableGet( catalog, schema, warehouseId ); setTableValidation({ exists: data.exists || false, error: data.error, message: data.message, checking: false, }); } catch (error) { console.error("Failed to validate api_http_registry table:", error); setTableValidation({ exists: false, error: "Validation failed", message: "Could not validate table existence", checking: false, }); } }; // Validate table when warehouse or catalog/schema changes useEffect(() => { if (selectedWarehouse && selectedCatalog && selectedSchema) { validateApiRegistryTable(selectedCatalog, selectedSchema, selectedWarehouse); } }, [selectedWarehouse, selectedCatalog, selectedSchema]); const sendMessage = async () => { if (!input.trim() || loading) return; const userMessage: Message = { role: "user", content: input, }; // Add user message and a temporary "thinking" message setMessages((prev) => [...prev, userMessage, { role: "assistant", content: "Thinking...", }]); setInput(""); setLoading(true); try { // Call the NEW agent endpoint - it does all the orchestration! const response = await fetch("/api/agent/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messages: [...messages.map(m => ({ role: m.role, content: m.content })), userMessage], model: selectedModel, system_prompt: systemPrompt || undefined, // Include custom system prompt if set warehouse_id: selectedWarehouse || undefined, // Pass selected warehouse catalog_schema: selectedCatalogSchema || undefined, // Pass selected catalog.schema // Pass credentials as metadata, NOT in message content! credentials: storedCredentials, }), }); if (!response.ok) { // Handle HTTP errors const errorData = await response.json().catch(() => ({ detail: "Unknown error" })); console.error("❌ [ERROR] API request failed:", response.status, errorData); // Remove the temporary "thinking" message setMessages((prev) => prev.slice(0, -1)); setMessages((prev) => [ ...prev, { role: "assistant", content: `Error: ${errorData.detail || "Request failed"}`, }, ]); return; } const data = await response.json(); // Remove the temporary "thinking" message setMessages((prev) => prev.slice(0, -1)); // Check for API errors in successful response if (data.detail) { console.error("❌ [ERROR] Error in response data:", data.detail); setMessages((prev) => [ ...prev, { role: "assistant", content: `Error: ${data.detail}`, }, ]); return; } // Check if response contains markers const responseText = data.response; console.log("🔍 [DEBUG] Full response text:", responseText); const needsApiKey = responseText.includes("[CREDENTIAL_REQUEST:API_KEY]"); const needsBearerToken = responseText.includes("[CREDENTIAL_REQUEST:BEARER_TOKEN]"); const hasEndpointOptions = responseText.includes("[ENDPOINT_OPTIONS:"); console.log("🔍 [DEBUG] Marker detection:", { needsApiKey, needsBearerToken, hasEndpointOptions }); // Extract API name if mentioned (look for it in the response) let apiName = ""; const apiNameMatch = responseText.match(/for\s+([A-Za-z0-9_\s-]+?)[\.\n\[]|provide your (?:API key|bearer token) for ([A-Za-z0-9_\s-]+)|register.*?([A-Za-z0-9_\s-]+)\s+(?:API|api|endpoint)/i); if (apiNameMatch) { apiName = (apiNameMatch[1] || apiNameMatch[2] || apiNameMatch[3] || "").trim(); } // Extract endpoint options information if present let endpoints: Array<{path: string; description: string; method: string}> = []; let registrationData: any = null; // Extract JSON by finding balanced braces // This handles nested objects/arrays properly const markerStart = responseText.indexOf("[ENDPOINT_OPTIONS:"); if (markerStart !== -1) { const jsonStart = responseText.indexOf("{", markerStart); if (jsonStart !== -1) { // Find the matching closing brace by counting let braceCount = 0; let jsonEnd = jsonStart; for (let i = jsonStart; i < responseText.length; i++) { if (responseText[i] === '{') braceCount++; if (responseText[i] === '}') braceCount--; if (braceCount === 0) { jsonEnd = i + 1; break; } } const jsonStr = responseText.substring(jsonStart, jsonEnd); console.log("🔍 [DEBUG] Extracted JSON (first 200 chars):", jsonStr.substring(0, 200)); try { const optionsData = JSON.parse(jsonStr); console.log("🔍 [DEBUG] Parsed options data:", optionsData); if (optionsData.endpoints && Array.isArray(optionsData.endpoints)) { endpoints = optionsData.endpoints; registrationData = { api_name: optionsData.api_name, host: optionsData.host, base_path: optionsData.base_path, auth_type: optionsData.auth_type, }; console.log("🔍 [DEBUG] Found endpoints:", endpoints.length, "Auth type:", optionsData.auth_type); // Use auth_type from data if API name not found in text if (!apiName && optionsData.api_name) { apiName = optionsData.api_name.replace(/_/g, ' '); } } } catch (e) { console.error("❌ [ERROR] Failed to parse endpoint options data:", e); console.error("❌ [ERROR] Raw JSON:", jsonStr); } } } else { console.log("⚠️ [WARN] No ENDPOINT_OPTIONS marker found in response"); } // Remove the markers from the displayed message let displayedResponse = responseText .replace(/\[CREDENTIAL_REQUEST:API_KEY\]/g, "") .replace(/\[CREDENTIAL_REQUEST:BEARER_TOKEN\]/g, ""); // Remove ENDPOINT_OPTIONS with balanced brace matching const removeMarkerStart = displayedResponse.indexOf("[ENDPOINT_OPTIONS:"); if (removeMarkerStart !== -1) { const removeJsonStart = displayedResponse.indexOf("{", removeMarkerStart); if (removeJsonStart !== -1) { let braceCount = 0; let removeJsonEnd = removeJsonStart; for (let i = removeJsonStart; i < displayedResponse.length; i++) { if (displayedResponse[i] === '{') braceCount++; if (displayedResponse[i] === '}') braceCount--; if (braceCount === 0) { removeJsonEnd = i + 1; break; } } // Check if there's a closing ] after the } if (displayedResponse[removeJsonEnd] === ']') { removeJsonEnd++; } displayedResponse = displayedResponse.substring(0, removeMarkerStart) + displayedResponse.substring(removeJsonEnd); } } displayedResponse = displayedResponse.trim(); // Add the assistant's response setMessages((prev) => [ ...prev, { role: "assistant", content: displayedResponse, tool_calls: data.tool_calls, // Show which tools were used trace_id: data.trace_id, // MLflow trace ID for "View Trace" link }, ]); // Show endpoint selection dialog if endpoints are available // Dialog will show credential input ONLY if authentication is required if (hasEndpointOptions && endpoints.length > 0) { const requiresAuth = needsApiKey || needsBearerToken; setCredentialType(needsBearerToken ? "bearer_token" : "api_key"); setPendingApiName(apiName); setPendingEndpoints(endpoints); setEndpointRegistrationData(registrationData); setSelectedEndpoints(new Set(endpoints.map((_, idx) => idx))); // Select all by default setShowCredentialDialog(true); } } catch (error) { console.error("Failed to send message:", error); // Remove thinking message setMessages((prev) => prev.slice(0, -1)); setMessages((prev) => [ ...prev, { role: "assistant", content: "Sorry, I encountered an error processing your request.", }, ]); } finally { setLoading(false); textareaRef.current?.focus(); } }; const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const resetChat = () => { setMessages([]); setInput(""); }; const handleOpenSystemPrompt = () => { setTempSystemPrompt(systemPrompt); setShowSystemPrompt(true); }; const handleSaveSystemPrompt = () => { setSystemPrompt(tempSystemPrompt); setShowSystemPrompt(false); }; const handleCancelSystemPrompt = () => { setTempSystemPrompt(systemPrompt); setShowSystemPrompt(false); }; const handleResetSystemPrompt = () => { setTempSystemPrompt(""); }; const handleCopyMessage = async (content: string, index: number) => { try { await navigator.clipboard.writeText(content); setCopiedIndex(index); setTimeout(() => setCopiedIndex(null), 2000); } catch (error) { console.error("Failed to copy:", error); } }; const handleEditMessage = (index: number, content: string) => { setEditingIndex(index); setEditingContent(content); }; const handleSaveEdit = async (index: number) => { if (!editingContent.trim()) return; // Remove all messages after the edited one const updatedMessages = messages.slice(0, index); setMessages(updatedMessages); setEditingIndex(null); // Set the edited content as the new input and send it setInput(editingContent); setEditingContent(""); // Trigger send with the new content const userMessage: Message = { role: "user", content: editingContent, }; setMessages((prev) => [...prev, userMessage, { role: "assistant", content: "Thinking...", }]); setLoading(true); try { const response = await fetch("/api/agent/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messages: [...updatedMessages.map(m => ({ role: m.role, content: m.content })), userMessage], model: selectedModel, system_prompt: systemPrompt || undefined, warehouse_id: selectedWarehouse || undefined, // Pass selected warehouse catalog_schema: selectedCatalogSchema || undefined, // Pass selected catalog.schema // Pass credentials as metadata, NOT in message content! credentials: storedCredentials, }), }); const data = await response.json(); setMessages((prev) => prev.slice(0, -1)); if (data.detail) { setMessages((prev) => [ ...prev, { role: "assistant", content: `Error: ${data.detail}`, }, ]); return; } // Check if response contains credential request markers const responseText = data.response; const needsApiKey = responseText.includes("[CREDENTIAL_REQUEST:API_KEY]"); const needsBearerToken = responseText.includes("[CREDENTIAL_REQUEST:BEARER_TOKEN]"); // Extract API name if mentioned (look for it in the response) let apiName = ""; const apiNameMatch = responseText.match(/for\s+([A-Za-z0-9_\s-]+?)[\.\n\[]|provide your (?:API key|bearer token) for ([A-Za-z0-9_\s-]+)/i); if (apiNameMatch) { apiName = (apiNameMatch[1] || apiNameMatch[2] || "").trim(); } // Remove the marker from the displayed message let displayedResponse = responseText .replace(/\[CREDENTIAL_REQUEST:API_KEY\]/g, "") .replace(/\[CREDENTIAL_REQUEST:BEARER_TOKEN\]/g, "") .trim(); setMessages((prev) => [ ...prev, { role: "assistant", content: displayedResponse, tool_calls: data.tool_calls, trace_id: data.trace_id, }, ]); // Show credential dialog if credentials are needed if (needsApiKey || needsBearerToken) { setCredentialType(needsBearerToken ? "bearer_token" : "api_key"); setPendingApiName(apiName); setShowCredentialDialog(true); } } catch (error) { console.error("Failed to send message:", error); setMessages((prev) => prev.slice(0, -1)); setMessages((prev) => [ ...prev, { role: "assistant", content: "Sorry, I encountered an error processing your request.", }, ]); } finally { setLoading(false); setInput(""); } }; const handleCancelEdit = () => { setEditingIndex(null); setEditingContent(""); }; const suggestedActions = [ { icon: <Search className="h-4 w-4" />, label: "Discover", prompt: "Discover the Alpha Vantage API for stock data", }, { icon: <Database className="h-4 w-4" />, label: "Register", prompt: "Help me register a new API in the registry", }, { icon: <FileJson className="h-4 w-4" />, label: "Query", prompt: "Show me all registered APIs in the registry", }, { icon: <TestTube className="h-4 w-4" />, label: "Test", prompt: "Test if my registered API is still healthy", }, { icon: <Wrench className="h-4 w-4" />, label: "Tools", prompt: "What tools do I have available?", }, ]; const isDark = theme === "dark"; return ( <div className={`flex flex-col h-full ${ isDark ? "bg-gradient-to-br from-[#1C3D42] via-[#24494F] to-[#2C555C]" : "bg-gradient-to-br from-gray-50 via-white to-gray-100" } transition-all duration-500`} > {/* Top Bar */} <div className={`flex items-center justify-between p-4 ${ isDark ? "bg-black/20" : "bg-white/60" } backdrop-blur-sm border-b ${ isDark ? "border-white/10" : "border-gray-200" }`}> <div className="flex items-center gap-3"> <Sparkles className={`h-5 w-5 ${isDark ? "text-[#FF8A80]" : "text-[#FF3621]"}`} /> <span className={`font-semibold ${isDark ? "text-white" : "text-gray-900"}`}> API Registry Agent </span> <span className={`text-xs px-2 py-1 rounded ${isDark ? "bg-[#FF3621]/20 text-[#FF8A80]" : "bg-[#FF3621]/10 text-[#FF3621]"}`}> MCP Powered </span> {messages.length > 0 && ( <Button variant="outline" size="sm" onClick={resetChat} className={`gap-2 ${ isDark ? "bg-white/5 border-white/20 text-white hover:bg-white/10" : "bg-white border-gray-300 text-gray-900 hover:bg-gray-100" }`} > <Home className="h-4 w-4" /> New Chat </Button> )} </div> <div className="flex items-center gap-3"> <Select value={selectedModel} onValueChange={setSelectedModel}> <SelectTrigger className={`w-[240px] ${ isDark ? "bg-black/20 border-white/20 text-white" : "bg-white border-gray-300 text-gray-900" } backdrop-blur-sm`}> <SelectValue placeholder="Select model"> {models.find((m) => m.id === selectedModel)?.name || "Select model"} </SelectValue> </SelectTrigger> <SelectContent> {models.map((model) => ( <SelectItem key={model.id} value={model.id} disabled={!model.supports_tools} > <div className="flex flex-col"> <div className="flex items-center gap-2"> <span className={`font-medium ${!model.supports_tools ? 'text-muted-foreground' : ''}`}> {model.name} </span> {!model.supports_tools && ( <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground border border-border"> Not tool-enabled </span> )} </div> <span className="text-xs text-muted-foreground"> {model.provider} • {model.context_window.toLocaleString()} tokens </span> </div> </SelectItem> ))} </SelectContent> </Select> <Select value={selectedWarehouse} onValueChange={(value) => { setSelectedWarehouse(value); setWarehouseFilter(""); // Clear filter on selection }} onOpenChange={(open) => { if (!open) setWarehouseFilter(""); // Clear filter on close }} > <SelectTrigger className={`w-[200px] ${ isDark ? "bg-black/20 border-white/20 text-white" : "bg-white border-gray-300 text-gray-900" } backdrop-blur-sm`}> <SelectValue placeholder="Select warehouse"> {warehouses.find((w) => w.id === selectedWarehouse)?.name || "Select warehouse"} </SelectValue> </SelectTrigger> <SelectContent> <div className="flex items-center px-2 pb-2 sticky top-0 bg-background"> <Search className="h-4 w-4 mr-2 text-muted-foreground" /> <Input placeholder="Search warehouses..." value={warehouseFilter} onChange={(e) => setWarehouseFilter(e.target.value)} className="h-8 text-sm" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} /> </div> <div className="max-h-[300px] overflow-y-auto"> {filteredWarehouses.length === 0 ? ( <div className="px-2 py-6 text-center text-sm text-muted-foreground"> No warehouses found </div> ) : ( filteredWarehouses.map((warehouse) => ( <SelectItem key={warehouse.id} value={warehouse.id} > <div className="flex flex-col"> <span className="font-medium">{warehouse.name}</span> <span className="text-xs text-muted-foreground"> {warehouse.size} • {warehouse.state} </span> </div> </SelectItem> )) )} </div> </SelectContent> </Select> {/* CATALOG DROPDOWN */} <Select value={selectedCatalog} onValueChange={(value) => { setSelectedCatalog(value); setCatalogFilter(""); // Clear filter on selection }} onOpenChange={(open) => { if (!open) setCatalogFilter(""); // Clear filter on close }} > <SelectTrigger className={`w-[180px] ${ isDark ? "bg-black/20 border-white/20 text-white" : "bg-white border-gray-300 text-gray-900" } backdrop-blur-sm`}> <SelectValue placeholder="Select catalog"> {catalogs.find((c) => c.name === selectedCatalog)?.name || "Select catalog"} </SelectValue> </SelectTrigger> <SelectContent> <div className="flex items-center px-2 pb-2 sticky top-0 bg-background"> <Search className="h-4 w-4 mr-2 text-muted-foreground" /> <Input placeholder="Search catalogs..." value={catalogFilter} onChange={(e) => setCatalogFilter(e.target.value)} className="h-8 text-sm" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} /> </div> <div className="max-h-[300px] overflow-y-auto"> {filteredCatalogs.length === 0 ? ( <div className="px-2 py-6 text-center text-sm text-muted-foreground"> No catalogs found </div> ) : ( filteredCatalogs.map((catalog) => ( <SelectItem key={catalog.name} value={catalog.name} > <div className="flex flex-col"> <span className="font-medium">{catalog.name}</span> {catalog.comment && ( <span className="text-xs text-muted-foreground">{catalog.comment}</span> )} </div> </SelectItem> )) )} </div> </SelectContent> </Select> {/* SCHEMA DROPDOWN */} <div className="flex items-center gap-2"> <Select value={selectedSchema} onValueChange={(value) => { setSelectedSchema(value); setSchemaFilter(""); // Clear filter on selection }} onOpenChange={(open) => { if (!open) setSchemaFilter(""); // Clear filter on close }} disabled={!selectedCatalog} > <SelectTrigger className={`w-[180px] ${ isDark ? "bg-black/20 text-white" : "bg-white text-gray-900" } ${ !tableValidation.exists && !tableValidation.checking ? "border-red-500 border-2" : isDark ? "border-white/20" : "border-gray-300" } backdrop-blur-sm`}> <SelectValue placeholder="Select schema"> {schemas.find((s) => s.name === selectedSchema)?.name || "Select schema"} </SelectValue> </SelectTrigger> <SelectContent> <div className="flex items-center px-2 pb-2 sticky top-0 bg-background"> <Search className="h-4 w-4 mr-2 text-muted-foreground" /> <Input placeholder="Search schemas..." value={schemaFilter} onChange={(e) => setSchemaFilter(e.target.value)} className="h-8 text-sm" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} /> </div> <div className="max-h-[300px] overflow-y-auto"> {filteredSchemas.length === 0 ? ( <div className="px-2 py-6 text-center text-sm text-muted-foreground"> {selectedCatalog ? "No schemas found" : "Select a catalog first"} </div> ) : ( filteredSchemas.map((schema) => ( <SelectItem key={schema.name} value={schema.name} > <div className="flex flex-col"> <span className="font-medium">{schema.name}</span> {schema.comment && ( <span className="text-xs text-muted-foreground">{schema.comment}</span> )} </div> </SelectItem> )) )} </div> </SelectContent> </Select> {!tableValidation.exists && !tableValidation.checking && selectedCatalog && selectedSchema && ( <div className="flex items-center gap-1 text-red-500" title={`No api_http_registry table exists in ${selectedCatalog}.${selectedSchema}. Switch to a different catalog.schema or run setup_api_http_registry_table.sql to create it.`}> <AlertCircle className="h-4 w-4" /> <span className="text-xs">No api_http_registry table in this schema</span> </div> )} {tableValidation.checking && ( <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> )} </div> </div> </div> {/* Error Banner for Missing Table */} {!tableValidation.exists && !tableValidation.checking && selectedCatalog && selectedSchema && ( <div className={`mx-6 mt-3 rounded-lg border-2 p-4 ${ isDark ? "bg-red-500/5 border-red-500/30" : "bg-red-50/50 border-red-200" }`}> <div className="flex items-start gap-3"> <AlertCircle className={`h-5 w-5 flex-shrink-0 mt-0.5 ${ isDark ? "text-[#FF8A80]" : "text-[#FF3621]" }`} /> <div className="flex-1"> <h3 className={`font-semibold text-sm mb-1 ${ isDark ? "text-white" : "text-gray-900" }`}> No api_http_registry table exists in {selectedCatalog}.{selectedSchema} </h3> <div className={`text-xs space-y-0.5 ${ isDark ? "text-white/70" : "text-gray-700" }`}> <p>Switch to a different catalog.schema with the api_http_registry table,</p> <p>or run setup_api_http_registry_table.sql to create it in <span className="font-mono font-medium">{selectedCatalog}.{selectedSchema}</span></p> </div> </div> </div> </div> )} {/* Main Content */} <div className="flex-1 overflow-y-auto"> {messages.length === 0 ? ( /* Empty State */ <div className="flex flex-col items-center justify-center min-h-full px-6 py-20"> <div className="max-w-5xl w-full space-y-8"> <div className="text-center space-y-4"> <h1 className={`text-5xl font-bold ${ isDark ? "text-white" : "text-gray-900" }`}> What can I help you with today? </h1> <p className={`text-lg ${ isDark ? "text-white/80" : "text-gray-600" }`}> I can help you discover, register, and manage API endpoints </p> </div> {/* Search Input */} <div className="relative"> <Textarea ref={textareaRef} placeholder="Explore APIs, register endpoints, or query the registry..." value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} className={`min-h-[100px] text-lg ${ isDark ? "bg-white/10 border-white/20 text-white placeholder:text-white/60" : "bg-white border-gray-300 text-gray-900 placeholder:text-gray-500" } backdrop-blur-md resize-none focus:ring-2 ${ isDark ? "focus:ring-[#FF3621]" : "focus:ring-[#FF3621]" } transition-all shadow-lg`} disabled={loading} /> <Button onClick={sendMessage} disabled={loading || !input.trim()} size="lg" className={`absolute bottom-4 right-4 rounded-full text-white shadow-lg ${ isDark ? "bg-[#2C555C] hover:bg-[#24494F]" : "bg-blue-600 hover:bg-blue-700" }`} > {loading ? ( <Loader2 className="h-5 w-5 animate-spin" /> ) : ( <Send className="h-5 w-5" /> )} </Button> </div> {/* Action Buttons */} <div className="flex items-center gap-3 flex-wrap justify-center"> {suggestedActions.map((action) => ( <Button key={action.label} variant="outline" className={`gap-2 ${ isDark ? "bg-white/10 border-white/20 text-white hover:bg-white/20" : "bg-white border-gray-300 text-gray-900 hover:bg-gray-100" } backdrop-blur-sm shadow-md transition-all`} onClick={() => setInput(action.prompt)} > {action.icon} {action.label} </Button> ))} </div> </div> </div> ) : ( /* Conversation View */ <div className="max-w-7xl mx-auto py-8 px-6 space-y-6"> {messages.map((message, index) => ( <div key={index} className={`flex gap-3 ${message.role === "user" ? "justify-end" : "justify-start"}`} > {/* Icon - shown on left for assistant, right for user */} {message.role === "assistant" && ( <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ isDark ? "bg-white/10 text-white" : "bg-gray-200 text-gray-700" }`}> <Bot className="h-5 w-5" /> </div> )} <div className={`max-w-[80%] rounded-2xl px-6 py-4 shadow-lg relative group ${ message.role === "user" ? isDark ? "bg-[#2C555C] text-white border border-white/10" : "bg-[#E3F2FD] text-gray-900 border border-blue-200" : isDark ? "bg-white/10 backdrop-blur-md text-white border border-white/20" : "bg-white text-gray-900 border border-gray-200" }`} > {/* Action Buttons */} <div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> {message.role === "assistant" && message.content !== "Thinking..." && ( <button onClick={() => handleCopyMessage(message.content, index)} className={`p-1.5 rounded-lg transition-all ${ isDark ? "hover:bg-white/10 text-white/60 hover:text-white" : "hover:bg-gray-100 text-gray-500 hover:text-gray-900" }`} title="Copy message" > {copiedIndex === index ? ( <Check className="h-4 w-4 text-green-500" /> ) : ( <Copy className="h-4 w-4" /> )} </button> )} {message.role === "user" && editingIndex !== index && ( <button onClick={() => handleEditMessage(index, message.content)} className={`p-1.5 rounded-lg transition-all ${ isDark ? "hover:bg-white/10 text-white/60 hover:text-white" : "hover:bg-black/5 text-gray-500 hover:text-gray-900" }`} title="Edit and resend" > <Edit2 className="h-4 w-4" /> </button> )} </div> {/* Message Content */} {editingIndex === index ? ( <div className="space-y-3"> <Textarea value={editingContent} onChange={(e) => setEditingContent(e.target.value)} className={`min-h-[100px] ${ isDark ? "bg-white/10 border-white/20 text-white" : "bg-white border-gray-300 text-gray-900" }`} autoFocus /> <div className="flex gap-2 justify-end"> <Button size="sm" variant="outline" onClick={handleCancelEdit} className={isDark ? "border-white/20 text-white hover:bg-white/10" : ""} > Cancel </Button> <Button size="sm" onClick={() => handleSaveEdit(index)} className={ isDark ? "bg-[#2C555C] hover:bg-[#24494F] text-white" : "bg-blue-600 hover:bg-blue-700 text-white" } > Send </Button> </div> </div> ) : ( <> <div className={`prose ${isDark ? 'prose-invert' : 'prose-gray'} max-w-none break-words ${message.content === "Thinking..." ? "typing-indicator" : ""} ${ isDark ? "[&_a]:text-blue-400 [&_a]:underline [&_a:hover]:text-blue-300" : "[&_a]:text-blue-600 [&_a]:underline [&_a:hover]:text-blue-700 [&_a]:font-medium" }`} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize( marked.parse(message.content, { breaks: true, gfm: true }) as string, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'code', 'pre', 'blockquote', 'span', 'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'del', 'input'], ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'type', 'checked', 'disabled', 'rel'] } ) }} /> {message.tool_calls && message.tool_calls.length > 0 && ( <div className="mt-3 flex flex-wrap gap-2"> {message.tool_calls.map((toolCall, tcIndex) => ( <span key={tcIndex} className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium ${ message.role === "user" ? "bg-white/20" : isDark ? "bg-[#FF3621]/20 text-[#FF8A80]" : "bg-[#FF3621]/10 text-[#FF3621]" }`} > <Sparkles className="h-3 w-3" /> {toolCall.tool} </span> ))} </div> )} {message.trace_id && ( <div className="mt-3"> <button onClick={() => onViewTrace && onViewTrace(message.trace_id!)} className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium transition-all hover:scale-105 ${ isDark ? "bg-green-500/20 text-green-300 hover:bg-green-500/30" : "bg-green-100 text-green-700 hover:bg-green-200" }`} > <Activity className="h-3 w-3" /> View Trace </button> </div> )} </> )} </div> {/* User Icon - shown on right for user messages */} {message.role === "user" && ( <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ isDark ? "bg-[#2C555C] text-white border border-white/10" : "bg-[#E3F2FD] text-gray-700 border border-blue-200" }`}> <User className="h-5 w-5" /> </div> )} </div> ))} {loading && ( <div className="flex justify-start"> <div className={`max-w-[80%] rounded-2xl px-6 py-4 shadow-lg ${ isDark ? "bg-white/10 backdrop-blur-md border border-white/20" : "bg-white border border-gray-200" }`}> <div className={`flex items-center gap-2 ${ isDark ? "text-white/80" : "text-gray-600" }`}> <Loader2 className="h-4 w-4 animate-spin" /> <span className="text-sm font-medium">Agent is thinking and using tools...</span> </div> </div> </div> )} <div ref={messagesEndRef} /> </div> )} </div> {/* System Prompt Trigger Button - Fixed on Right Edge */} <button onClick={handleOpenSystemPrompt} onMouseEnter={() => setShowSystemPrompt(true)} className={`fixed right-0 top-1/2 -translate-y-1/2 z-20 px-2 py-6 rounded-l-lg shadow-lg transition-all duration-300 ${ isDark ? "bg-white/10 border-l border-t border-b border-white/20 text-white hover:bg-white/20" : "bg-white border-l border-t border-b border-gray-200 text-gray-900 hover:bg-gray-50" } backdrop-blur-md flex items-center gap-2 text-sm writing-mode-vertical`} style={{ writingMode: 'vertical-rl' }} > <Plus className="h-4 w-4" /> <span>{systemPrompt ? "Edit System Prompt" : "Add System Prompt"}</span> </button> {/* System Prompt Panel - Slides from Right */} <div className={`fixed right-0 top-0 h-full w-96 z-30 transition-transform duration-300 ${ showSystemPrompt ? "translate-x-0" : "translate-x-full" } ${ isDark ? "bg-black/90" : "bg-white/95" } backdrop-blur-lg border-l ${ isDark ? "border-white/20" : "border-gray-200" } shadow-2xl`} onMouseLeave={() => !tempSystemPrompt && setShowSystemPrompt(false)} > <div className="flex flex-col h-full p-6"> <div className="flex items-center justify-between mb-6"> <h3 className={`text-lg font-semibold ${ isDark ? "text-white" : "text-gray-900" }`}> System Prompt </h3> <button onClick={handleCancelSystemPrompt} className={`p-2 rounded-lg transition-colors ${ isDark ? "hover:bg-white/10 text-white" : "hover:bg-gray-100 text-gray-900" }`} > <span className="text-xl">&times;</span> </button> </div> <div className="flex-1 flex flex-col gap-4"> <label className={`text-sm font-medium ${ isDark ? "text-white/80" : "text-gray-700" }`}> Customize the agent's behavior and role: </label> <Textarea value={tempSystemPrompt} onChange={(e) => setTempSystemPrompt(e.target.value)} placeholder="Optionally override the system prompt. Define the agent's role, capabilities, and behavior here..." className={`flex-1 ${ isDark ? "bg-white/5 border-[#FF3621]/50 text-white placeholder:text-white/40 focus:border-[#FF3621]" : "bg-white border-[#FF3621]/50 text-gray-900 placeholder:text-gray-400 focus:border-[#FF3621]" } resize-none`} /> </div> <div className={`flex items-center justify-end gap-2 mt-6 pt-6 border-t ${ isDark ? "border-white/20" : "border-gray-200" }`}> <Button variant="ghost" size="sm" onClick={handleResetSystemPrompt} className={isDark ? "text-[#FF8A80] hover:text-[#FF3621] hover:bg-white/10" : "text-[#FF3621] hover:text-[#E02E1A]"} > Reset </Button> <Button variant="outline" size="sm" onClick={handleCancelSystemPrompt} className={isDark ? "border-white/20 text-white hover:bg-white/10" : ""} > Cancel </Button> <Button size="sm" onClick={handleSaveSystemPrompt} className={ isDark ? "bg-[#2C555C] hover:bg-[#24494F] text-white" : "bg-blue-600 hover:bg-blue-700 text-white" } > Save </Button> </div> </div> </div> {/* Bottom Input (shown when in conversation) */} {messages.length > 0 && ( <div className={`p-4 ${ isDark ? "bg-black/20" : "bg-white/60" } backdrop-blur-sm border-t ${ isDark ? "border-white/10" : "border-gray-200" }`}> <div className="max-w-7xl mx-auto"> {/* Quick Action Buttons - Horizontal row above input */} <div className="flex items-center gap-2 mb-3 overflow-x-auto pb-2"> {suggestedActions.map((action, index) => ( <button key={action.label} onClick={() => setInput(action.prompt)} className={`group flex items-center gap-2 px-4 py-2 rounded-full transition-all duration-300 hover:scale-105 whitespace-nowrap ${ isDark ? "bg-white/10 border border-white/20 text-white hover:bg-white/20" : "bg-white border border-gray-200 text-gray-900 hover:bg-gray-50" } backdrop-blur-md shadow-md`} style={{ animation: `fadeInRight 0.3s ease-out ${index * 0.1}s both`, }} > {action.icon} <span className="text-sm font-medium">{action.label}</span> </button> ))} </div> <div className="relative"> <Textarea ref={textareaRef} placeholder="Continue the conversation..." value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} className={`min-h-[60px] pr-14 ${ isDark ? "bg-white/10 border-white/20 text-white placeholder:text-white/60" : "bg-white border-gray-300 text-gray-900 placeholder:text-gray-500" } backdrop-blur-md resize-none shadow-lg`} disabled={loading} /> <Button onClick={sendMessage} disabled={loading || !input.trim()} size="icon" className={`absolute bottom-3 right-3 rounded-full text-white shadow-lg ${ isDark ? "bg-[#2C555C] hover:bg-[#24494F]" : "bg-blue-600 hover:bg-blue-700" }`} > {loading ? ( <Loader2 className="h-5 w-5 animate-spin" /> ) : ( <Send className="h-5 w-5" /> )} </Button> </div> </div> </div> )} {/* FAQ/Help Button - Bottom Left */} <Dialog> <DialogTrigger asChild> <button className={`fixed bottom-6 left-6 z-20 flex items-center gap-3 px-4 py-3 rounded-full shadow-lg transition-all duration-300 hover:scale-105 ${ isDark ? "bg-white/10 border border-white/20 text-white hover:bg-white/20" : "bg-white border border-gray-200 text-gray-900 hover:bg-gray-50" } backdrop-blur-md`} title="Help & FAQ" > <HelpCircle className="h-6 w-6" /> <span className="text-sm font-medium">User Guide</span> </button> </DialogTrigger> <DialogContent className={`max-w-2xl max-h-[80vh] overflow-y-auto ${ isDark ? "bg-gray-900 text-white border-white/20" : "bg-white text-gray-900" }`}> <DialogHeader> <DialogTitle className="text-2xl font-bold">How to Use the API Registry Agent</DialogTitle> <DialogDescription className={isDark ? "text-gray-400" : "text-gray-600"}> Your AI-powered assistant for managing and testing APIs </DialogDescription> </DialogHeader> <div className="space-y-6 mt-4"> <section> <h3 className="text-lg font-semibold mb-2">🚀 Getting Started</h3> <p className={isDark ? "text-gray-300" : "text-gray-700"}> The API Registry Agent uses MCP (Model Context Protocol) tools to help you discover, register, query, and test API endpoints. Simply chat with the agent using natural language! </p> </section> <section> <h3 className="text-lg font-semibold mb-2">🎯 Quick Actions</h3> <p className={isDark ? "text-gray-300 mb-2" : "text-gray-700 mb-2"}> Use the quick action buttons for common tasks: </p> <ul className={`list-disc list-inside space-y-1 ${isDark ? "text-gray-300" : "text-gray-700"}`}> <li><strong>Discover:</strong> Find and explore new APIs from the web</li> <li><strong>Register:</strong> Add APIs to your centralized registry</li> <li><strong>Query:</strong> Check what APIs are in your registry</li> <li><strong>Test:</strong> Verify API health and functionality</li> <li><strong>Tools:</strong> See all available MCP tools</li> </ul> </section> <section> <h3 className="text-lg font-semibold mb-2">💬 Example Prompts</h3> <ul className={`list-disc list-inside space-y-1 ${isDark ? "text-gray-300" : "text-gray-700"}`}> <li>"Discover APIs related to weather data"</li> <li>"Register the API at https://api.example.com"</li> <li>"What APIs are in my registry?"</li> <li>"Test if my weather API is healthy"</li> <li>"Execute a SQL query to count all registered APIs"</li> </ul> </section> <section> <h3 className="text-lg font-semibold mb-2">🛠️ Available Tools</h3> <ul className={`list-disc list-inside space-y-1 ${isDark ? "text-gray-300" : "text-gray-700"}`}> <li><strong>create_http_connection:</strong> Create UC HTTP connections (secure credentials)</li> <li><strong>smart_register_with_connection:</strong> One-step API registration (creates connection + registers API)</li> <li><strong>discover_api_endpoint:</strong> Test and validate API endpoints</li> <li><strong>fetch_api_documentation:</strong> Parse API documentation</li> <li><strong>check_api_http_registry:</strong> View registered APIs</li> <li><strong>call_registered_api:</strong> Call registered APIs via UC connections</li> <li><strong>execute_dbsql:</strong> Run SQL queries on Databricks</li> <li><strong>list_warehouses:</strong> List SQL warehouses</li> <li><strong>list_http_connections:</strong> List UC HTTP connections</li> <li><strong>list_dbfs_files:</strong> Browse DBFS files</li> </ul> </section> <section> <h3 className="text-lg font-semibold mb-2">⚙️ Custom System Prompt</h3> <p className={isDark ? "text-gray-300" : "text-gray-700"}> Click the "Add System Prompt" button on the right edge to customize the agent's behavior and role for your specific use case. </p> </section> <section> <h3 className="text-lg font-semibold mb-2">✨ Features</h3> <ul className={`list-disc list-inside space-y-1 ${isDark ? "text-gray-300" : "text-gray-700"}`}> <li>Markdown rendering for formatted responses</li> <li>Real-time tool execution tracking</li> <li>Model selection (Claude, Llama, etc.)</li> <li>Dark/Light theme toggle</li> <li>Conversation history management</li> </ul> </section> </div> </DialogContent> </Dialog> {/* Secure Credential Input Dialog */} <Dialog open={showCredentialDialog} onOpenChange={setShowCredentialDialog}> <DialogContent className={`sm:max-w-4xl max-h-[90vh] overflow-y-auto ${ isDark ? "bg-[#1C3D42] border-white/20 text-white" : "bg-white border-gray-200 text-gray-900" }`}> <DialogHeader> <DialogTitle className={isDark ? "text-white" : "text-gray-900"}> {endpointRegistrationData?.auth_type && endpointRegistrationData.auth_type !== "none" ? "🔐 Endpoint Selection & Credential Input" : "📡 Select Endpoints to Register"} </DialogTitle> <DialogDescription className={isDark ? "text-white/60" : "text-gray-600"}> {pendingApiName && `For ${pendingApiName} API - `} {endpointRegistrationData?.auth_type && endpointRegistrationData.auth_type !== "none" ? `Select endpoints to register and provide your ${credentialType === "api_key" ? "API Key" : "Bearer Token"}.` : "Select which endpoints you want to register."} </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {/* Show endpoint selection if provided */} {pendingEndpoints.length > 0 && ( <div className={`rounded-lg border p-4 space-y-3 ${ isDark ? "border-white/20 bg-black/10" : "border-gray-200 bg-gray-50" }`}> <div className="flex items-center justify-between mb-2"> <div className="flex items-center gap-2"> <span className="text-lg">📡</span> <h4 className={`font-semibold text-sm ${isDark ? "text-white" : "text-gray-900"}`}> Select Endpoints to Register ({selectedEndpoints.size}/{pendingEndpoints.length}) </h4> </div> <div className="flex gap-2"> <button onClick={() => setSelectedEndpoints(new Set(pendingEndpoints.map((_, idx) => idx)))} className={`text-xs px-2 py-1 rounded ${ isDark ? "hover:bg-white/10 text-blue-400" : "hover:bg-gray-200 text-blue-600" }`} > Select All </button> <button onClick={() => setSelectedEndpoints(new Set())} className={`text-xs px-2 py-1 rounded ${ isDark ? "hover:bg-white/10 text-gray-400" : "hover:bg-gray-200 text-gray-600" }`} > Clear </button> </div> </div> <div className="space-y-2 max-h-64 overflow-y-auto"> {pendingEndpoints.map((endpoint, idx) => ( <label key={idx} className={`flex items-start gap-3 p-3 rounded cursor-pointer transition-colors ${ selectedEndpoints.has(idx) ? isDark ? "bg-blue-500/20 border border-blue-500/50" : "bg-blue-50 border border-blue-200" : isDark ? "bg-white/5 hover:bg-white/10" : "bg-white hover:bg-gray-50" }`} > <input type="checkbox" checked={selectedEndpoints.has(idx)} onChange={(e) => { const newSelected = new Set(selectedEndpoints); if (e.target.checked) { newSelected.add(idx); } else { newSelected.delete(idx); } setSelectedEndpoints(newSelected); }} className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2 mb-1 flex-wrap"> <code className={`font-mono font-medium text-xs ${ isDark ? "text-blue-400" : "text-blue-600" }`}> {endpoint.method} </code> <code className={`font-mono text-xs ${ isDark ? "text-green-400" : "text-green-600" }`}> {endpoint.path} </code> </div> <p className={`text-xs ${isDark ? "text-white/60" : "text-gray-600"}`}> {endpoint.description} </p> </div> </label> ))} </div> <p className={`text-xs pt-2 border-t ${ isDark ? "text-white/40 border-white/10" : "text-gray-500 border-gray-200" }`}> 💡 Select the endpoints you want to register. You can always register more later. </p> </div> )} {/* Show credential input ONLY if authentication is required */} {endpointRegistrationData?.auth_type && endpointRegistrationData.auth_type !== "none" && ( <div className="space-y-2"> <label className={`text-sm font-medium ${isDark ? "text-white" : "text-gray-900"}`}> {credentialType === "api_key" ? "API Key" : "Bearer Token"} <span className="text-red-500">*</span> </label> <Input type="password" placeholder={credentialType === "api_key" ? "Enter your API key..." : "Enter your bearer token..."} value={credentialValue} onChange={(e) => setCredentialValue(e.target.value)} className={`font-mono ${ isDark ? "bg-black/20 border-white/20 text-white placeholder:text-white/40" : "bg-gray-50 border-gray-300 text-gray-900 placeholder:text-gray-400" }`} autoFocus /> <p className={`text-xs ${isDark ? "text-white/40" : "text-gray-500"}`}> • Your credential is masked for security <br /> • Stored encrypted in Databricks secret scopes <br /> • Never logged or displayed in plain text </p> </div> )} </div> <div className="flex justify-end gap-3"> <Button variant="outline" onClick={() => { setShowCredentialDialog(false); setCredentialValue(""); }} className={isDark ? "border-white/20 text-white hover:bg-white/10" : ""} > Cancel </Button> <Button onClick={async () => { // Require credential only if auth is needed const requiresAuth = endpointRegistrationData?.auth_type && endpointRegistrationData.auth_type !== "none"; if (requiresAuth && !credentialValue.trim()) return; // DEBUG: Log credential before sending console.log(`🔐 [Frontend] Credential type: ${credentialType}`); console.log(`🔐 [Frontend] Credential value length: ${credentialValue.length} chars`); console.log(`🔐 [Frontend] Credential preview: ${credentialValue.substring(0, 10)}...`); // SECURE: Store credential in session state, NOT in message content! // Build updated credentials object const updatedCredentials = requiresAuth ? { ...storedCredentials, [credentialType]: credentialValue, } : storedCredentials; console.log(`🔐 [Frontend] Updated credentials:`, { ...updatedCredentials, [credentialType]: updatedCredentials[credentialType]?.substring(0, 10) + '...' }); // Only update state if auth is required if (requiresAuth) { setStoredCredentials(updatedCredentials); } setShowCredentialDialog(false); setCredentialValue(""); // Build message with selected endpoints info const selectedEndpointsList = Array.from(selectedEndpoints) .map(idx => pendingEndpoints[idx]) .map(ep => ep.path) .join(", "); // Build appropriate message based on auth requirement (requiresAuth already declared above) const safeMessage = selectedEndpoints.size > 0 ? requiresAuth ? `I've securely provided my ${credentialType === "api_key" ? "API key" : "bearer token"}${pendingApiName ? ` for ${pendingApiName}` : ""}. Please register these ${selectedEndpoints.size} endpoint(s): ${selectedEndpointsList}` : `Please register these ${selectedEndpoints.size} endpoint(s) for ${pendingApiName || "the API"}: ${selectedEndpointsList}` : requiresAuth ? `I've securely provided my ${credentialType === "api_key" ? "API key" : "bearer token"}${pendingApiName ? ` for ${pendingApiName}` : ""}.` : `Ready to register ${pendingApiName || "the API"}.`; const userMessage: Message = { role: "user", content: safeMessage, }; setMessages((prev) => [...prev, userMessage, { role: "assistant", content: "Thinking...", }]); setLoading(true); try { const response = await fetch("/api/agent/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messages: [...messages.map(m => ({ role: m.role, content: m.content })), userMessage], model: selectedModel, system_prompt: systemPrompt || undefined, warehouse_id: selectedWarehouse || undefined, catalog_schema: selectedCatalogSchema || undefined, // Pass credentials as metadata, NOT in message content! credentials: updatedCredentials, }), }); if (!response.ok) { // Handle HTTP errors const errorData = await response.json().catch(() => ({ detail: "Unknown error" })); console.error("❌ [ERROR] API request failed:", response.status, errorData); setMessages((prev) => prev.slice(0, -1)); setMessages((prev) => [ ...prev, { role: "assistant", content: `Error: ${errorData.detail || "Request failed"}`, }, ]); return; } const data = await response.json(); setMessages((prev) => prev.slice(0, -1)); if (data.detail) { console.error("❌ [ERROR] Error in response data:", data.detail); setMessages((prev) => [ ...prev, { role: "assistant", content: `Error: ${data.detail}`, }, ]); return; } // Strip any credential markers from response const cleanedResponse = data.response .replace(/\[CREDENTIAL_REQUEST:API_KEY\]/g, "") .replace(/\[CREDENTIAL_REQUEST:BEARER_TOKEN\]/g, "") .trim(); setMessages((prev) => [ ...prev, { role: "assistant", content: cleanedResponse, tool_calls: data.tool_calls, trace_id: data.trace_id, }, ]); } catch (error) { console.error("Failed to send message:", error); setMessages((prev) => prev.slice(0, -1)); setMessages((prev) => [ ...prev, { role: "assistant", content: "Sorry, I encountered an error processing your request.", }, ]); } finally { setLoading(false); setInput(""); } }} disabled={ (endpointRegistrationData?.auth_type && endpointRegistrationData.auth_type !== "none" && !credentialValue.trim()) || (pendingEndpoints.length > 0 && selectedEndpoints.size === 0) } className={`${ isDark ? "bg-[#2C555C] hover:bg-[#24494F] text-white" : "bg-blue-600 hover:bg-blue-700 text-white" }`} > Submit Securely </Button> </div> </DialogContent> </Dialog> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lucamilletti99/dataverse_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server