Skip to main content
Glama
accounts.html20.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Google Calendar MCP - Account Management</title> <link rel="stylesheet" href="styles.css"> <style> /* Page-specific styles for account management */ .container { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem; } header { text-align: center; margin-bottom: 2.5rem; } h1 { margin-bottom: 0.5rem; } .subtitle { color: var(--text-muted); font-size: 0.95rem; font-weight: 400; } /* Card overrides for this page */ .card { padding: 1.75rem; margin-bottom: 1.25rem; box-shadow: var(--shadow-md); text-align: left; min-width: auto; max-width: none; } .card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1.25rem; color: var(--text-primary); letter-spacing: -0.01em; } /* Account list */ .account-list { display: flex; flex-direction: column; gap: 0.75rem; } .account-item { background: var(--bg-warm); border-radius: var(--radius-md); border: 1px solid var(--border-warm); overflow: hidden; transition: box-shadow 0.2s ease, border-color 0.2s ease; } .account-item:hover { box-shadow: var(--shadow-sm); } .account-header { display: flex; align-items: center; padding: 0.75rem 1rem; cursor: pointer; transition: background 0.15s ease; gap: 0.75rem; } .account-header:hover { background: var(--bg-warm-dark); } .account-header.expanded { border-bottom: 1px solid var(--border-warm); background: var(--bg-warm-dark); } .account-info { display: flex; align-items: center; gap: 0.75rem; } .account-expand { color: var(--text-muted); transition: transform 0.2s ease; font-size: 0.7rem; opacity: 0.7; } .account-expand.expanded { transform: rotate(90deg); } .account-details { flex: 1; } .account-title { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .account-email { font-weight: 500; color: var(--text-primary); font-size: 0.95rem; } .account-separator { color: var(--text-muted); font-size: 0.9rem; } .account-id { color: var(--text-secondary); font-size: 0.9rem; font-weight: 400; } .account-meta { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.25rem; flex-wrap: wrap; } .account-status { display: inline-flex; align-items: center; padding: 0.15rem 0.5rem; border-radius: 20px; font-size: 0.7rem; font-weight: 500; text-transform: lowercase; } .status-active { background: var(--accent-success-light); color: var(--accent-success); } .status-expired { background: var(--accent-warning-light); color: var(--accent-warning); } .calendar-count { color: var(--text-muted); font-size: 0.8rem; } .account-actions { display: flex; gap: 0.5rem; margin-left: auto; } /* Calendar list */ .calendar-list { display: none; padding: 0.5rem 1rem 0.75rem; background: var(--card-bg); max-height: 280px; overflow-y: auto; } .calendar-list.expanded { display: block; } .calendar-list::-webkit-scrollbar { width: 5px; } .calendar-list::-webkit-scrollbar-track { background: transparent; } .calendar-list::-webkit-scrollbar-thumb { background: var(--border-warm); border-radius: 3px; } .calendar-item { display: flex; align-items: center; padding: 0.35rem 0.5rem; border-radius: 4px; transition: background 0.12s ease; } .calendar-item:hover { background: var(--bg-warm); } .calendar-color { width: 8px; height: 8px; border-radius: 2px; margin-right: 0.6rem; flex-shrink: 0; } .calendar-name { font-size: 0.8rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 0.4rem; } .calendar-name.primary { color: var(--text-primary); font-weight: 500; } .calendar-badges { display: flex; gap: 0.25rem; flex-shrink: 0; } .access-badge { padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.6rem; font-weight: 500; text-transform: lowercase; letter-spacing: 0.01em; opacity: 0.7; } .access-owner { background: transparent; color: var(--text-muted); } .access-writer { background: transparent; color: var(--accent-success); } .access-reader { background: transparent; color: var(--text-muted); } .access-freeBusyReader { background: transparent; color: var(--text-muted); font-size: 0.55rem; } .primary-badge { background: var(--accent-primary); color: white; opacity: 1; } .calendar-loading { text-align: center; padding: 1rem; color: var(--text-muted); font-size: 0.8rem; } .calendar-error { text-align: center; padding: 1rem; color: var(--accent-danger); font-size: 0.8rem; } /* Button size overrides for inline buttons */ .account-actions button { padding: 0.6rem 1.1rem; font-size: 0.85rem; } /* Add account form */ .add-account-form { display: flex; gap: 1rem; align-items: flex-end; } @media (max-width: 600px) { .container { padding: 2rem 1rem; } .add-account-form { flex-direction: column; align-items: stretch; } .account-header { flex-direction: column; align-items: flex-start; gap: 0.75rem; } .account-actions { width: 100%; justify-content: flex-end; } } </style> </head> <body> <div class="container"> <header> <h1>Google Calendar MCP</h1> <p class="subtitle">Manage your authenticated Google accounts</p> </header> <div id="message"></div> <div class="card"> <h2>Add Account</h2> <form class="add-account-form" id="addAccountForm"> <div class="form-group"> <label for="accountId">Account Nickname</label> <input type="text" id="accountId" name="accountId" placeholder="e.g., work, personal, home" pattern="[a-z0-9_-]{1,64}" title="Choose a friendly name: 1-64 characters (lowercase letters, numbers, dashes, underscores)" required > </div> <button type="submit" class="btn-primary">Add Account</button> </form> </div> <div class="card"> <h2>Authenticated Accounts</h2> <div id="accountList" class="loading">Loading accounts...</div> </div> </div> <script> const API_BASE = '/api/accounts'; // Cache for calendar data const calendarCache = new Map(); function showMessage(message, type = 'success') { const messageDiv = document.getElementById('message'); messageDiv.className = 'message-' + type; messageDiv.textContent = message; setTimeout(() => { messageDiv.className = ''; messageDiv.textContent = ''; }, 5000); } async function loadAccounts() { try { // Clear calendar cache when reloading accounts calendarCache.clear(); const response = await fetch(API_BASE); if (!response.ok) { throw new Error(`Failed to load accounts: ${response.statusText}`); } const data = await response.json(); renderAccounts(data.accounts); } catch (error) { document.getElementById('accountList').innerHTML = `<div class="message-error">Error loading accounts: ${error.message}</div>`; } } function renderAccounts(accounts) { const accountList = document.getElementById('accountList'); if (!accounts || accounts.length === 0) { accountList.innerHTML = '<div class="empty-state">No accounts authenticated yet</div>'; return; } accountList.innerHTML = accounts.map(account => { const calendars = account.calendars || []; const owned = calendars.filter(c => c.accessRole === 'owner').length; const shared = calendars.length - owned; const calendarSummary = calendars.length > 0 ? `${calendars.length} calendars (${owned} owned, ${shared} shared)` : ''; return ` <div class="account-item" data-account-id="${escapeHtml(account.id)}"> <div class="account-header" onclick="toggleAccount('${escapeHtml(account.id)}')"> <div class="account-info"> <span class="account-expand">▶</span> <div class="account-details"> <div class="account-title"> <span class="account-email">${escapeHtml(account.email)}</span> <span class="account-separator">–</span> <span class="account-id">${escapeHtml(account.id)}</span> <span class="account-status status-${account.status}">${account.status}</span> </div> <div class="account-meta"> <span class="calendar-count">${calendarSummary}</span> </div> </div> </div> <div class="account-actions" onclick="event.stopPropagation()"> ${account.status === 'expired' ? `<button class="btn-warning" onclick="reauthAccount('${escapeHtml(account.id)}')">Re-auth</button>` : ''} <button class="btn-danger" onclick="removeAccount('${escapeHtml(account.id)}')">Remove</button> </div> </div> <div class="calendar-list" id="calendars-${escapeHtml(account.id)}"> ${renderCalendarsHTML(calendars)} </div> </div> `}).join(''); } function toggleAccount(accountId) { const item = document.querySelector(`[data-account-id="${accountId}"]`); const header = item.querySelector('.account-header'); const expand = item.querySelector('.account-expand'); const calendarList = document.getElementById(`calendars-${accountId}`); const isExpanded = calendarList.classList.contains('expanded'); if (isExpanded) { header.classList.remove('expanded'); expand.classList.remove('expanded'); calendarList.classList.remove('expanded'); } else { header.classList.add('expanded'); expand.classList.add('expanded'); calendarList.classList.add('expanded'); } } function renderCalendarsHTML(calendars) { if (!calendars || calendars.length === 0) { return '<div class="calendar-loading">No calendars found</div>'; } return calendars.map(cal => ` <div class="calendar-item"> <div class="calendar-color" style="background-color: ${cal.backgroundColor || '#4285f4'}"></div> <div class="calendar-name ${cal.primary ? 'primary' : ''}"> ${escapeHtml(cal.summaryOverride || cal.summary || 'Unnamed Calendar')} </div> <div class="calendar-badges"> ${cal.primary ? '<span class="access-badge primary-badge">Primary</span>' : ''} <span class="access-badge access-${cal.accessRole}">${formatAccessRole(cal.accessRole)}</span> </div> </div> `).join(''); } function formatAccessRole(role) { const labels = { 'owner': 'Owner', 'writer': 'Editor', 'reader': 'Viewer', 'freeBusyReader': 'Free/Busy' }; return labels[role] || role; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function addAccount(accountId) { try { // Get OAuth URL from server const response = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accountId }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to initiate OAuth flow'); } // Open OAuth URL in popup const popup = window.open( data.authUrl, 'oauth', 'width=600,height=700,left=100,top=100' ); if (!popup) { showMessage('Popup blocked. Please allow popups for this site.', 'error'); return; } // Listen for auth success message const handleMessage = (event) => { // Validate origin to prevent XSS/clickjacking attacks if (event.origin !== window.location.origin) { return; } if (event.data.type === 'auth-success') { window.removeEventListener('message', handleMessage); showMessage(`Account "${accountId}" authenticated successfully!`, 'success'); loadAccounts(); } }; window.addEventListener('message', handleMessage); // Fallback: check if popup is closed without success const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', handleMessage); // Wait a bit then reload accounts in case auth completed setTimeout(() => loadAccounts(), 500); } }, 500); } catch (error) { showMessage(error.message, 'error'); } } async function removeAccount(accountId) { if (!confirm(`Are you sure you want to remove account "${accountId}"?`)) { return; } try { const response = await fetch(`${API_BASE}/${accountId}`, { method: 'DELETE' }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to remove account'); } showMessage(`Account "${accountId}" removed successfully!`, 'success'); loadAccounts(); } catch (error) { showMessage(error.message, 'error'); } } async function reauthAccount(accountId) { try { // Get OAuth URL from server const response = await fetch(`${API_BASE}/${accountId}/reauth`, { method: 'POST' }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to initiate re-authentication'); } // Open OAuth URL in popup const popup = window.open( data.authUrl, 'oauth', 'width=600,height=700,left=100,top=100' ); if (!popup) { showMessage('Popup blocked. Please allow popups for this site.', 'error'); return; } // Listen for auth success message const handleMessage = (event) => { // Validate origin to prevent XSS/clickjacking attacks if (event.origin !== window.location.origin) { return; } if (event.data.type === 'auth-success') { window.removeEventListener('message', handleMessage); showMessage(`Account "${accountId}" re-authenticated successfully!`, 'success'); loadAccounts(); } }; window.addEventListener('message', handleMessage); // Fallback: check if popup is closed without success const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', handleMessage); // Wait a bit then reload accounts in case auth completed setTimeout(() => loadAccounts(), 500); } }, 500); } catch (error) { showMessage(error.message, 'error'); } } document.getElementById('addAccountForm').addEventListener('submit', (e) => { e.preventDefault(); const accountId = document.getElementById('accountId').value.trim(); if (accountId) { addAccount(accountId); document.getElementById('accountId').value = ''; } }); // Load accounts on page load loadAccounts(); </script> </body> </html>

Latest Blog Posts

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/nspady/google-calendar-mcp'

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