Skip to main content
Glama

API Registry MCP Server

RegistryPage.tsx•24.2 kB
/** * RegistryPage - View and manage all registered APIs */ import { useState, useEffect } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { RefreshCw, Edit2, Trash2, Save, X, ExternalLink, Check, AlertCircle } from 'lucide-react'; import { useTheme } from '@/components/theme-provider'; interface RegisteredAPI { api_id: string; api_name: string; description: string; connection_name: string; // UC HTTP Connection name host: string; // API host (e.g., "api.github.com") base_path?: string; // Base path for API (e.g., "/v1", "/api") auth_type: string; // "none", "api_key", or "bearer_token" secret_scope?: string; // "mcp_api_keys" or "mcp_bearer_tokens" documentation_url?: string; available_endpoints?: string; // JSON array of available endpoints example_calls?: string; // JSON array of example calls status: string; validation_message?: string; user_who_requested?: string; created_at?: string; modified_date?: string; } interface RegistryPageProps { selectedWarehouse: string; selectedCatalogSchema: string; } // Helper function to redact sensitive query parameters for display const redactSensitiveParams = (path: string): string => { if (!path) return ''; const sensitiveParams = ['api_key', 'apikey', 'token', 'access_token', 'secret', 'password', 'key']; let redactedPath = path; sensitiveParams.forEach(param => { // Match parameter in query string with any value const patterns = [ new RegExp(`([?&])${param}=([^&]+)`, 'gi'), new RegExp(`([?&])${param}%3D([^&]+)`, 'gi'), // URL encoded = ]; patterns.forEach(pattern => { redactedPath = redactedPath.replace(pattern, `$1${param}=[REDACTED]`); }); }); return redactedPath; }; export function RegistryPage({ selectedWarehouse, selectedCatalogSchema }: RegistryPageProps) { const [apis, setApis] = useState<RegisteredAPI[]>([]); const [loading, setLoading] = useState(false); const [editingId, setEditingId] = useState<string | null>(null); const [editForm, setEditForm] = useState<Partial<RegisteredAPI>>({}); const [testingId, setTestingId] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); const { theme } = useTheme(); const isDark = theme === 'dark'; useEffect(() => { if (selectedWarehouse && selectedCatalogSchema) { loadApis(); } else { // Clear error if warehouse/catalog not selected setError(null); setApis([]); } }, [selectedWarehouse, selectedCatalogSchema]); const loadApis = async () => { if (!selectedWarehouse || !selectedCatalogSchema) { setError('Please select a warehouse and catalog.schema'); setLoading(false); return; } try { setLoading(true); setError(null); // Parse catalog and schema from full_name const [catalog, schema] = selectedCatalogSchema.split('.'); const params = new URLSearchParams({ catalog, schema, warehouse_id: selectedWarehouse, }); const response = await fetch(`/api/registry/list?${params.toString()}`); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'Failed to load APIs' })); const errorMessage = errorData.detail || 'Failed to load APIs'; throw new Error(errorMessage); } const data = await response.json(); setApis(data.apis || []); } catch (error) { console.error('Failed to load APIs:', error); const errorMessage = error instanceof Error ? error.message : 'Failed to load APIs'; setError(errorMessage); setApis([]); } finally { setLoading(false); } }; const handleEdit = (api: RegisteredAPI) => { setEditingId(api.api_id); setEditForm(api); }; const handleCancelEdit = () => { setEditingId(null); setEditForm({}); }; const handleSaveEdit = async () => { if (!selectedWarehouse || !selectedCatalogSchema) { alert('Please select a warehouse and catalog.schema'); return; } try { // Parse catalog and schema from full_name const [catalog, schema] = selectedCatalogSchema.split('.'); const params = new URLSearchParams({ catalog, schema, warehouse_id: selectedWarehouse, api_name: editForm.api_name || '', description: editForm.description || '', }); // Add documentation_url if provided if (editForm.documentation_url) { params.append('documentation_url', editForm.documentation_url); } const response = await fetch(`/api/registry/update/${editingId}?${params.toString()}`, { method: 'POST', }); if (!response.ok) { throw new Error('Failed to update API'); } // Reload APIs to get fresh data await loadApis(); setEditingId(null); setEditForm({}); } catch (error) { console.error('Failed to save API:', error); alert('Failed to update API. Please try again.'); } }; const handleDelete = async (apiId: string) => { if (!confirm('Are you sure you want to delete this API?')) return; if (!selectedWarehouse || !selectedCatalogSchema) { alert('Please select a warehouse and catalog.schema'); return; } try { // Parse catalog and schema from full_name const [catalog, schema] = selectedCatalogSchema.split('.'); const params = new URLSearchParams({ catalog, schema, warehouse_id: selectedWarehouse, }); const response = await fetch(`/api/registry/delete/${apiId}?${params.toString()}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete API'); } // Reload APIs to get fresh data await loadApis(); } catch (error) { console.error('Failed to delete API:', error); alert('Failed to delete API. Please try again.'); } }; const handleTestHealth = async (api: RegisteredAPI) => { try { setTestingId(api.api_id); // Call the API using the new execute_api_call tool const response = await fetch('/api/agent/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: `Test the API "${api.api_name}" by calling it` }], model: 'databricks-claude-sonnet-4', }), }); await response.json(); // Update status setApis(apis.map(a => a.api_id === api.api_id ? { ...a, status: 'valid', modified_date: new Date().toISOString() } : a )); } catch (error) { console.error('Failed to test API health:', error); setApis(apis.map(a => a.api_id === api.api_id ? { ...a, status: 'error' } : a )); } finally { setTestingId(null); } }; const getStatusBadge = (status: string) => { switch (status) { case 'valid': return ( <Badge className="bg-green-500/20 text-green-300 border-green-500/30"> <Check className="h-3 w-3 mr-1" /> Healthy </Badge> ); case 'error': return ( <Badge className="bg-[#FF3621]/20 text-[#FF8A80] border-[#FF3621]/30"> <AlertCircle className="h-3 w-3 mr-1" /> Error </Badge> ); case 'pending': return ( <Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30"> Pending </Badge> ); default: return ( <Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30"> Unknown </Badge> ); } }; return ( <div className={`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' }`}> {/* Header */} <div className={`p-6 border-b ${isDark ? 'border-white/10' : 'border-gray-200'}`}> <div className="flex items-center justify-between"> <div> <h1 className={`text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}> API Registry </h1> <p className={`text-sm mt-1 ${isDark ? 'text-white/60' : 'text-gray-600'}`}> Manage your registered API endpoints </p> </div> <Button onClick={loadApis} disabled={loading} 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' }`} variant="outline" > <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} /> Refresh </Button> </div> </div> {/* API Cards */} <div className="p-6 overflow-y-auto" style={{ height: 'calc(100% - 100px)' }}> {!selectedWarehouse || !selectedCatalogSchema ? ( <div className="flex flex-col items-center justify-center h-64"> <AlertCircle className={`h-12 w-12 mb-4 ${isDark ? 'text-white/40' : 'text-gray-400'}`} /> <p className={`text-lg ${isDark ? 'text-white/60' : 'text-gray-600'}`}> Please select a warehouse and catalog.schema </p> <p className={`text-sm mt-2 ${isDark ? 'text-white/40' : 'text-gray-400'}`}> Go to Chat Playground to configure your database settings </p> </div> ) : error ? ( <div className="flex flex-col items-center justify-center h-64"> <AlertCircle className={`h-12 w-12 mb-4 ${isDark ? 'text-[#FF8A80]' : 'text-[#FF3621]'}`} /> <p className={`text-lg font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}> {error} </p> <div className={`text-sm mt-2 text-center max-w-md ${isDark ? 'text-white/60' : 'text-gray-600'}`}> {error.toLowerCase().includes('table') || error.toLowerCase().includes('api_http_registry') ? ( <> <p>Switch to a catalog.schema with the api_http_registry table,</p> <p className="mt-1">or run setup_api_http_registry_table.sql to create it in <span className="font-mono">{selectedCatalogSchema}</span></p> </> ) : ( <p>Please check your warehouse and catalog.schema selection</p> )} </div> </div> ) : loading ? ( <div className="flex items-center justify-center h-64"> <RefreshCw className={`h-8 w-8 animate-spin ${isDark ? 'text-white/60' : 'text-gray-400'}`} /> </div> ) : apis.length === 0 ? ( <div className="flex flex-col items-center justify-center h-64"> <p className={`text-lg ${isDark ? 'text-white/60' : 'text-gray-600'}`}> No APIs registered yet </p> <p className={`text-sm mt-2 ${isDark ? 'text-white/40' : 'text-gray-400'}`}> Use the Chat Playground to register your first API </p> </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {apis.map((api) => ( <Card key={api.api_id} className={`${ isDark ? 'bg-white/10 border-white/20 backdrop-blur-md' : 'bg-white border-gray-200' } transition-all hover:shadow-lg flex flex-col`} > <CardContent className="p-6 flex-1 flex flex-col"> {editingId === api.api_id ? ( // Edit Mode <div className="flex-1 flex flex-col"> <div className="space-y-4 flex-1"> <div> <label className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}> Name </label> <Input value={editForm.api_name || ''} onChange={(e) => setEditForm({ ...editForm, api_name: e.target.value })} className={isDark ? 'bg-white/5 border-white/20 text-white' : ''} /> </div> <div> <label className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}> Description </label> <Textarea value={editForm.description || ''} onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} className={isDark ? 'bg-white/5 border-white/20 text-white' : ''} rows={3} /> </div> <div> <label className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}> Documentation URL </label> <Input value={editForm.documentation_url || ''} onChange={(e) => setEditForm({ ...editForm, documentation_url: e.target.value })} className={isDark ? 'bg-white/5 border-white/20 text-white' : ''} placeholder="https://api.example.com/docs" /> </div> <div className={`text-xs ${isDark ? 'text-white/60' : 'text-gray-500'}`}> Note: Connection details (host, base_path, auth) are set during registration and cannot be edited. </div> </div> <div className="flex gap-2 mt-4"> <Button size="sm" onClick={handleSaveEdit} className="flex-1 bg-[#FF3621] hover:bg-[#E02E1A] text-white" > <Save className="h-4 w-4 mr-1" /> Save </Button> <Button size="sm" variant="outline" onClick={handleCancelEdit} className={isDark ? 'border-white/20 text-white hover:bg-white/10' : ''} > <X className="h-4 w-4 mr-1" /> Cancel </Button> </div> </div> ) : ( // View Mode <div className="flex-1 flex flex-col"> <div className="flex-1"> <div className="flex items-start justify-between mb-4"> <div className="flex-1"> <h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}> {api.api_name} </h3> <p className={`text-sm mt-1 ${isDark ? 'text-white/60' : 'text-gray-600'}`}> {api.description} </p> </div> {getStatusBadge(api.status)} </div> <div className={`text-xs space-y-2 ${isDark ? 'text-white/80' : 'text-gray-700'}`}> <div> <span className="font-medium">Connection:</span> <div className="mt-1 font-mono text-xs bg-black/20 px-2 py-1 rounded"> {api.connection_name} </div> </div> <div> <span className="font-medium">Host:</span> <div className="mt-1 font-mono text-xs bg-black/20 px-2 py-1 rounded break-all"> {api.host} </div> </div> {api.base_path && ( <div> <span className="font-medium">Base Path:</span> <div className="mt-1 font-mono text-xs bg-black/20 px-2 py-1 rounded break-all"> {api.base_path} </div> </div> )} <div> <span className="font-medium">Auth Type:</span> {api.auth_type} </div> {api.secret_scope && ( <div> <span className="font-medium">Secret Scope:</span> {api.secret_scope} </div> )} {api.documentation_url && ( <div> <span className="font-medium">Documentation:</span> <a href={api.documentation_url} target="_blank" rel="noopener noreferrer" className="flex items-start gap-1 hover:underline mt-1 group" > <span className="break-all">{api.documentation_url}</span> <ExternalLink className="h-3 w-3 flex-shrink-0 mt-0.5 opacity-60 group-hover:opacity-100" /> </a> </div> )} {api.available_endpoints && (() => { try { const endpoints = JSON.parse(api.available_endpoints); if (endpoints && endpoints.length > 0) { return ( <div> <span className="font-medium">Available Endpoints:</span> <div className="mt-1 space-y-1"> {endpoints.map((endpoint: any, idx: number) => ( <div key={idx} className={`text-xs ${isDark ? 'text-white/70' : 'text-gray-600'}`}> <span className="font-mono">{endpoint.method || 'GET'}</span> <span className="ml-1 font-mono">{endpoint.path}</span> {endpoint.description && <span className="ml-2">- {endpoint.description}</span>} </div> ))} </div> </div> ); } } catch (e) { return null; } return null; })()} {api.example_calls && (() => { try { const examples = JSON.parse(api.example_calls); if (examples && examples.length > 0) { return ( <div> <span className="font-medium">Example Calls:</span> <div className="mt-1 space-y-1"> {examples.map((example: any, idx: number) => ( <div key={idx} className={`text-xs ${isDark ? 'text-white/70' : 'text-gray-600'}`}> <div className="font-mono">{example.path}</div> {example.description && <div className="ml-2 text-xs">{example.description}</div>} </div> ))} </div> </div> ); } } catch (e) { return null; } return null; })()} {api.user_who_requested && ( <div className={isDark ? 'text-white/60' : 'text-gray-500'}> <span className="font-medium">Requested by:</span> {api.user_who_requested} </div> )} {api.created_at && ( <div className={isDark ? 'text-white/40' : 'text-gray-400'}> Created: {new Date(api.created_at).toLocaleDateString()} </div> )} {api.modified_date && ( <div className={isDark ? 'text-white/40' : 'text-gray-400'}> Last modified: {new Date(api.modified_date).toLocaleDateString()} </div> )} </div> </div> <div className="flex gap-2 pt-4 mt-auto"> <Button size="sm" onClick={() => handleTestHealth(api)} disabled={testingId === api.api_id} className={`flex-1 ${ isDark ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-900' }`} variant="outline" > <RefreshCw className={`h-4 w-4 mr-1 ${testingId === api.api_id ? 'animate-spin' : ''}`} /> Test Health </Button> <Button size="sm" onClick={() => handleEdit(api)} className={`${ isDark ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-900' }`} variant="outline" > <Edit2 className="h-4 w-4" /> </Button> <Button size="sm" onClick={() => handleDelete(api.api_id)} className="bg-[#FF3621]/20 hover:bg-[#FF3621]/30 text-[#FF8A80] border-[#FF3621]/30" variant="outline" > <Trash2 className="h-4 w-4" /> </Button> </div> </div> )} </CardContent> </Card> ))} </div> )} </div> </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