Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
admin.html38.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SAP MCP Server - Admin Dashboard</title> <!-- SAP UI5 Theme and Fonts --> <link rel="stylesheet" href="https://ui5.sap.com/resources/sap/ui/core/themes/sap_horizon/library.css"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=72:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: '72', -apple-system, BlinkMacSystemFont, sans-serif; background: #f5f6fa; min-height: 100vh; color: #32363a; } /* Header */ .header { background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between; border-bottom: 3px solid #0070f2; } .header h1 { color: #0070f2; font-weight: 600; font-size: 1.5rem; display: flex; align-items: center; gap: 0.5rem; } .sap-logo { width: 32px; height: 32px; background: #0070f2; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 14px; } .user-info { display: flex; align-items: center; gap: 1rem; font-size: 0.9rem; color: #6a6d70; } .status-badge { background: #0070f2; color: white; padding: 0.25rem 0.75rem; border-radius: 16px; font-size: 0.8rem; font-weight: 500; } /* Main Container */ .container { max-width: 1400px; margin: 0 auto; padding: 2rem; } /* Statistics Cards */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } .stat-card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid #e4e7ea; transition: all 0.2s ease; } .stat-card:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); border-color: #0070f2; } .stat-value { font-size: 2rem; font-weight: 700; color: #0070f2; margin-bottom: 0.5rem; } .stat-label { color: #6a6d70; font-size: 0.9rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } /* Users Table */ .table-container { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid #e4e7ea; overflow: hidden; } .table-header { padding: 1.5rem; background: #f8f9fb; border-bottom: 1px solid #e4e7ea; display: flex; align-items: center; justify-content: space-between; } .table-title { font-size: 1.25rem; font-weight: 600; color: #32363a; } .refresh-btn { background: #0070f2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.9rem; cursor: pointer; transition: background 0.2s ease; display: flex; align-items: center; gap: 0.5rem; } .refresh-btn:hover { background: #005bb5; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { padding: 1rem 1.5rem; text-align: left; border-bottom: 1px solid #e4e7ea; } .table th { background: #f8f9fb; font-weight: 600; color: #32363a; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px; } .table td { font-size: 0.9rem; } .table tbody tr:hover { background: #f8f9fb; } /* Role Badges */ .role-badge { padding: 0.25rem 0.75rem; border-radius: 16px; font-size: 0.8rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .role-admin { background: #ffeaa7; color: #d63031; } .role-editor { background: #a7f0ba; color: #00b894; } .role-viewer { background: #74b9ff; color: white; } /* Status Indicators */ .status-indicator { display: inline-flex; align-items: center; gap: 0.5rem; } .status-dot { width: 8px; height: 8px; border-radius: 50%; } .status-active .status-dot { background: #00b894; } .status-expired .status-dot { background: #d63031; } /* Action Buttons */ .action-btn { padding: 0.25rem 0.75rem; border: 1px solid #d1d8e0; background: white; border-radius: 4px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s ease; margin-right: 0.5rem; } .action-btn:hover { border-color: #0070f2; color: #0070f2; } /* Loading State */ .loading { display: flex; align-items: center; justify-content: center; padding: 3rem; color: #6a6d70; } .spinner { width: 32px; height: 32px; border: 3px solid #e4e7ea; border-top: 3px solid #0070f2; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Responsive */ @media (max-width: 768px) { .header { padding: 1rem; flex-direction: column; gap: 1rem; } .container { padding: 1rem; } .stats-grid { grid-template-columns: 1fr; } .table-container { overflow-x: auto; } .table { min-width: 800px; } } /* Error State */ .error { background: #fff5f5; border: 1px solid #fed7d7; color: #c53030; padding: 1rem; border-radius: 8px; margin: 1rem 0; } </style> </head> <body> <div class="header"> <div> <h1> <div class="sap-logo">S</div> MCP Server Admin Dashboard </h1> </div> <div class="user-info"> <span id="currentUser">Loading...</span> <div class="status-badge" id="authStatus">Admin</div> </div> </div> <div class="container"> <!-- Statistics Cards --> <div class="stats-grid" id="statsGrid"> <div class="stat-card"> <div class="stat-value" id="totalUsers">-</div> <div class="stat-label">Total Users</div> </div> <div class="stat-card"> <div class="stat-value" id="activeUsers">-</div> <div class="stat-label">Active Sessions</div> </div> <div class="stat-card"> <div class="stat-value" id="adminUsers">-</div> <div class="stat-label">Admin Users</div> </div> </div> <!-- OData Configuration Management --> <div class="table-container" style="margin-bottom: 2rem;"> <div class="table-header"> <div class="table-title">🔧 OData Service Configuration</div> <button class="refresh-btn" onclick="reloadODataConfig()" id="reloadBtn"> <span>🔄</span> Reload Services </button> </div> <div style="padding: 1.5rem; background: white; border-radius: 0 0 8px 8px; border: 1px solid #e3e6ea; border-top: none;"> <!-- Current Configuration Status --> <div id="configStatus" style="margin-bottom: 1.5rem;"> <div class="loading"> <div class="spinner"></div> Loading configuration status... </div> </div> </div> </div> <!-- Destination Status --> <div class="table-container" style="margin-bottom: 2rem;"> <div class="table-header"> <div class="table-title">🔗 Destination Status</div> <button class="refresh-btn" onclick="loadDestinationStatus()" id="destStatusBtn"> <span>🔄</span> Check Status </button> </div> <div id="destinationStatusContent"> <div id="destinationLoadingState" style="display: flex; align-items: center; justify-content: center; padding: 2rem; color: #666;"> <div>⏳ Loading destination status...</div> </div> <div id="destinationStatusDisplay" style="display: none;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-bottom: 1rem;"> <!-- Design-Time Destination --> <div class="stat-card"> <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;"> <div id="designTimeStatusIcon">🔄</div> <strong>Design-Time Destination</strong> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Name:</strong> <span id="designTimeName">-</span> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Status:</strong> <span id="designTimeStatus">-</span> </div> <div id="designTimeError" style="font-size: 0.8rem; color: #c53030; margin-top: 0.5rem; display: none;"></div> <div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;"> Used for: Service discovery, metadata </div> </div> <!-- Runtime Destination --> <div class="stat-card"> <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;"> <div id="runtimeStatusIcon">🔄</div> <strong>Runtime Destination</strong> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Name:</strong> <span id="runtimeName">-</span> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Status:</strong> <span id="runtimeStatus">-</span> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Auth:</strong> <span id="runtimeAuthType">-</span> </div> <div id="runtimeHybrid" style="font-size: 0.8rem; color: #0070f2; margin-bottom: 0.25rem; display: none;"> 🔒 Hybrid: Principal Propagation + BasicAuth fallback </div> <div id="runtimeError" style="font-size: 0.8rem; color: #c53030; margin-top: 0.5rem; display: none;"></div> <div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;"> Used for: CRUD operations, data access </div> </div> <!-- Configuration Info --> <div class="stat-card"> <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;"> <div>⚙️</div> <strong>Configuration</strong> </div> <div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;"> <strong>Mode:</strong> <span id="configMode">-</span> </div> <div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;"> <div id="configModeDescription">-</div> </div> </div> </div> </div> </div> </div> <!-- Users Table --> <div class="table-container"> <div class="table-header"> <div class="table-title">User Sessions & Authorizations</div> <button class="refresh-btn" onclick="loadUsers()"> <span>🔄</span> Refresh </button> </div> <div id="loadingState" class="loading"> <div class="spinner"></div> Loading user data... </div> <div id="errorState" class="error" style="display: none;"> <strong>Error:</strong> <span id="errorMessage"></span> </div> <table class="table" id="usersTable" style="display: none;"> <thead> <tr> <th>User</th> <th>Role</th> <th>Scopes</th> <th>Status</th> <th>Last Activity</th> <th>Session Type</th> <th>Actions</th> </tr> </thead> <tbody id="usersTableBody"> </tbody> </table> </div> </div> <script> let usersData = []; // Format date for display function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleString(); } // Format relative time function formatRelativeTime(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffMinutes = Math.floor(diffMs / (1000 * 60)); if (diffHours > 24) { return `${Math.floor(diffHours / 24)}d ago`; } else if (diffHours > 0) { return `${diffHours}h ago`; } else if (diffMinutes > 0) { return `${diffMinutes}m ago`; } else { return 'Just now'; } } // Get session ID from URL parameters or localStorage function getSessionId() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('session') || localStorage.getItem('mcp_session_id') || 'global_user_auth'; } // Load users data async function loadUsers() { try { document.getElementById('loadingState').style.display = 'flex'; document.getElementById('errorState').style.display = 'none'; document.getElementById('usersTable').style.display = 'none'; const sessionId = getSessionId(); const response = await fetch(`/auth/admin/users?session=${encodeURIComponent(sessionId)}`); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to load users'); } usersData = data.users; updateStatistics(data.summary); updateUsersTable(data.users); // Display user info with Session ID const currentSessionId = getSessionId(); document.getElementById('currentUser').innerHTML = `${data.requestedBy}<br><small style="color: #666;">Session ID: <code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 0.8em;">${currentSessionId}</code></small>`; document.getElementById('loadingState').style.display = 'none'; document.getElementById('usersTable').style.display = 'table'; } catch (error) { console.error('Error loading users:', error); document.getElementById('loadingState').style.display = 'none'; document.getElementById('errorState').style.display = 'block'; document.getElementById('errorMessage').textContent = error.message; } } // Update statistics cards function updateStatistics(summary) { document.getElementById('totalUsers').textContent = summary.totalUsers; document.getElementById('activeUsers').textContent = summary.activeUsers; document.getElementById('adminUsers').textContent = summary.adminUsers; } // Update users table function updateUsersTable(users) { const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = ''; users.forEach(user => { const row = document.createElement('tr'); const statusClass = user.isActive ? 'status-active' : 'status-expired'; const statusText = user.isActive ? 'Active' : 'Expired'; row.innerHTML = ` <td> <strong>${user.user}</strong> <br> <small style="color: #6a6d70;">${user.clientInfo.ipAddress}</small> </td> <td> <span class="role-badge role-${user.role}">${user.role}</span> </td> <td> <span style="font-size: 0.8rem; color: #6a6d70;"> ${user.scopes.join(', ') || 'None'} </span> </td> <td> <div class="status-indicator ${statusClass}"> <div class="status-dot"></div> ${statusText} </div> </td> <td> <span title="${formatDate(user.lastActivity)}"> ${formatRelativeTime(user.lastActivity)} </span> </td> <td> <span style="font-size: 0.8rem; color: #6a6d70;"> ${user.sessionType} </span> </td> <td> <button class="action-btn" onclick="viewSession('${user.sessionId}')">View</button> <button class="action-btn" onclick="deleteSession('${user.sessionId}')">Delete</button> </td> `; tbody.appendChild(row); }); } // View session details function viewSession(sessionId) { const user = usersData.find(u => u.sessionId === sessionId); if (!user) return; const details = ` Session Details for ${user.user}: Session ID: ${user.sessionId} Role: ${user.role} Scopes: ${user.scopes.join(', ')} Status: ${user.isActive ? 'Active' : 'Expired'} Authenticated: ${formatDate(user.authenticatedAt)} Last Activity: ${formatDate(user.lastActivity)} Expires: ${formatDate(user.expiresAt)} Client: ${user.clientInfo.userAgent} IP: ${user.clientInfo.ipAddress} Type: ${user.sessionType} `; alert(details); } // Delete session async function deleteSession(sessionId) { const user = usersData.find(u => u.sessionId === sessionId); if (!user) return; if (confirm(`Are you sure you want to delete the session for ${user.user}?`)) { try { const authSessionId = getSessionId(); const response = await fetch(`/auth/admin/users/delete?session=${encodeURIComponent(authSessionId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId }) }); const data = await response.json(); if (response.ok) { alert(`Session for ${user.user} deleted successfully`); loadUsers(); // Refresh the table } else { alert(`Error: ${data.message}`); } } catch (error) { alert(`Error deleting session: ${error.message}`); } } } // Reload OData configuration and trigger service rediscovery async function reloadODataConfig() { const reloadBtn = document.getElementById('reloadBtn'); const originalText = reloadBtn.innerHTML; try { // Update button to show loading state reloadBtn.innerHTML = '<span>⏳</span> Reloading...'; reloadBtn.disabled = true; const authSessionId = getSessionId(); const response = await fetch(`/auth/admin/odata/reload?session=${encodeURIComponent(authSessionId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const result = await response.json(); if (response.ok && result.success) { alert(`✅ OData Configuration Reload Successful!\n\n${result.message}\n\nServices Count: ${result.servicesCount || 'Unknown'}\n\nTriggered by: ${result.triggeredBy}\nTime: ${new Date(result.triggeredAt).toLocaleString()}`); } else { alert(`❌ OData Reload Failed!\n\n${result.message || result.error}`); } } catch (error) { alert(`❌ Error reloading OData configuration: ${error.message}`); } finally { // Restore button reloadBtn.innerHTML = originalText; reloadBtn.disabled = false; } } // Load OData configuration status async function loadConfigStatus() { const statusDiv = document.getElementById('configStatus'); try { const authSessionId = getSessionId(); const response = await fetch(`/auth/admin/odata/status?session=${encodeURIComponent(authSessionId)}`); const result = await response.json(); if (response.ok && result.success) { const config = result.configuration.config; const servicesCount = result.configuration.servicesCount; const discoveredServices = result.configuration.discoveredServices; statusDiv.innerHTML = ` <div class="info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1rem;"> <div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff;"> <div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">📍 Configuration Source</div> <div style="color: #6c757d; font-size: 0.9rem;">${config.configurationSource || 'Unknown'}</div> </div> <div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745;"> <div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">📊 Services Discovered</div> <div style="color: #6c757d; font-size: 0.9rem; font-weight: 600;">${servicesCount} services</div> </div> <div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107;"> <div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">🔄 Max Services</div> <div style="color: #6c757d; font-size: 0.9rem;">${config.maxServices || 'Not set'}</div> </div> </div> <div style="background: #e7f3ff; border: 1px solid #b8daff; border-radius: 6px; padding: 1rem; margin-bottom: 1rem;"> <div style="font-weight: 600; color: #004085; margin-bottom: 0.5rem;">🔧 Current Configuration</div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; font-size: 0.9rem;"> <div> <strong>Service Patterns:</strong><br> <code style="background: white; padding: 0.2rem 0.4rem; border-radius: 3px; display: inline-block; margin-top: 0.25rem;"> ${Array.isArray(config.servicePatterns) ? config.servicePatterns.join(', ') : (config.servicePatterns || 'Not set')} </code> </div> <div> <strong>Exclusion Patterns:</strong><br> <code style="background: white; padding: 0.2rem 0.4rem; border-radius: 3px; display: inline-block; margin-top: 0.25rem;"> ${Array.isArray(config.exclusionPatterns) ? config.exclusionPatterns.join(', ') : (config.exclusionPatterns || 'None')} </code> </div> </div> <div style="margin-top: 0.75rem; font-size: 0.9rem;"> <strong>Allow All Services:</strong> <span style="color: ${config.allowAllServices ? '#28a745' : '#dc3545'}; font-weight: 600;"> ${config.allowAllServices ? 'Yes' : 'No'} </span> </div> </div> ${discoveredServices.length > 0 ? ` <details style="border: 1px solid #dee2e6; border-radius: 6px; margin-bottom: 1rem;"> <summary style="padding: 0.75rem; background: #f8f9fa; cursor: pointer; font-weight: 600;"> 📋 Discovered Services (${discoveredServices.length}) </summary> <div style="padding: 1rem; max-height: 200px; overflow-y: auto;"> ${discoveredServices.map(service => ` <div style="padding: 0.5rem; border-bottom: 1px solid #e9ecef; font-size: 0.9rem;"> <div style="font-weight: 600;">${service.name}</div> <div style="color: #6c757d;">ID: ${service.id}</div> </div> `).join('')} </div> </details> ` : ''} `; } else { statusDiv.innerHTML = ` <div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 6px; padding: 1rem; color: #721c24;"> ❌ Failed to load configuration status: ${result.message || 'Unknown error'} </div> `; } } catch (error) { statusDiv.innerHTML = ` <div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 6px; padding: 1rem; color: #721c24;"> ❌ Error loading configuration: ${error.message} </div> `; } } // Load destination status async function loadDestinationStatus() { const statusBtn = document.getElementById('destStatusBtn'); const loadingState = document.getElementById('destinationLoadingState'); const statusDisplay = document.getElementById('destinationStatusDisplay'); try { statusBtn.disabled = true; loadingState.style.display = 'flex'; statusDisplay.style.display = 'none'; const authSessionId = getSessionId(); const response = await fetch(`/auth/admin/destinations/status?session=${encodeURIComponent(authSessionId)}`); const result = await response.json(); if (response.ok && result.success) { updateDestinationStatusDisplay(result.destinations); loadingState.style.display = 'none'; statusDisplay.style.display = 'block'; } else { throw new Error(result.message || 'Failed to load destination status'); } } catch (error) { console.error('Failed to load destination status:', error); loadingState.innerHTML = `<div style="color: #c53030;">❌ Failed to load destination status: ${error.message}</div>`; } finally { statusBtn.disabled = false; } } // Update destination status display function updateDestinationStatusDisplay(destinations) { // Design-time destination const designTimeIcon = document.getElementById('designTimeStatusIcon'); const designTimeName = document.getElementById('designTimeName'); const designTimeStatus = document.getElementById('designTimeStatus'); const designTimeError = document.getElementById('designTimeError'); designTimeName.textContent = destinations.designTime.name; if (destinations.designTime.available) { designTimeIcon.textContent = '✅'; designTimeStatus.textContent = 'Available'; designTimeStatus.style.color = '#38a169'; designTimeError.style.display = 'none'; } else { designTimeIcon.textContent = '❌'; designTimeStatus.textContent = 'Not Available'; designTimeStatus.style.color = '#c53030'; if (destinations.designTime.error) { designTimeError.textContent = destinations.designTime.error; designTimeError.style.display = 'block'; } else { designTimeError.style.display = 'none'; } } // Runtime destination const runtimeIcon = document.getElementById('runtimeStatusIcon'); const runtimeName = document.getElementById('runtimeName'); const runtimeStatus = document.getElementById('runtimeStatus'); const runtimeAuthType = document.getElementById('runtimeAuthType'); const runtimeHybrid = document.getElementById('runtimeHybrid'); const runtimeError = document.getElementById('runtimeError'); runtimeName.textContent = destinations.runtime.name; // Display authentication type if (destinations.runtime.authType) { runtimeAuthType.textContent = destinations.runtime.authType; if (destinations.runtime.authType === 'PrincipalPropagation') { runtimeAuthType.style.color = '#0070f2'; } else { runtimeAuthType.style.color = '#666'; } } // Show hybrid authentication indicator if (destinations.runtime.hybrid) { runtimeHybrid.style.display = 'block'; } else { runtimeHybrid.style.display = 'none'; } if (destinations.runtime.available) { runtimeIcon.textContent = '✅'; runtimeStatus.textContent = 'Available'; runtimeStatus.style.color = '#38a169'; runtimeError.style.display = 'none'; } else { // Check if it's a Principal Propagation JWT requirement const isPrincipalPropError = destinations.runtime.error && (destinations.runtime.error.includes('user token') || destinations.runtime.error.includes('PrincipalPropagation')); if (isPrincipalPropError) { runtimeIcon.textContent = '🔐'; runtimeStatus.textContent = 'Requires User JWT'; runtimeStatus.style.color = '#0070f2'; runtimeError.innerHTML = '💡 Principal Propagation requires authenticated user context. Status check from admin dashboard is expected to fail.'; runtimeError.style.color = '#0070f2'; runtimeError.style.display = 'block'; } else { runtimeIcon.textContent = '❌'; runtimeStatus.textContent = 'Not Available'; runtimeStatus.style.color = '#c53030'; if (destinations.runtime.error) { runtimeError.textContent = destinations.runtime.error; runtimeError.style.display = 'block'; } else { runtimeError.style.display = 'none'; } } } // Configuration const configMode = document.getElementById('configMode'); const configModeDescription = document.getElementById('configModeDescription'); if (destinations.config.useSingleDestination) { configMode.textContent = 'Single Destination'; configModeDescription.textContent = 'Both operations use the same destination. Simpler setup, but may have different authentication requirements.'; } else { configMode.textContent = 'Dual Destination'; configModeDescription.textContent = 'Separate destinations for discovery/metadata and runtime operations. Recommended for production environments.'; } } // Enhanced reload function to also refresh status const originalReloadODataConfig = reloadODataConfig; reloadODataConfig = async function() { await originalReloadODataConfig(); // Refresh status after reload setTimeout(loadConfigStatus, 1000); setTimeout(loadDestinationStatus, 1500); }; // Auto-refresh every 30 seconds setInterval(loadUsers, 30000); setInterval(loadConfigStatus, 60000); // Refresh config status every minute setInterval(loadDestinationStatus, 120000); // Refresh destination status every 2 minutes // Load initial data document.addEventListener('DOMContentLoaded', function() { loadUsers(); loadConfigStatus(); loadDestinationStatus(); }); </script> </body> </html>

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/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

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