UNPKG

mcp-quiz-server

Version:

๐Ÿง  AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

475 lines (426 loc) โ€ข 18.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SSE MCP Auth Test Client</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 20px rgba(0,0,0,0.1); overflow: hidden; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; } .content { padding: 30px; } .auth-section { background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px; border-left: 4px solid #007bff; } .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; } .status-card { background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; border: 2px solid transparent; } .status-card.connected { border-color: #28a745; background: #d4edda; } .status-card.authenticated { border-color: #007bff; background: #d1ecf1; } .status-card.error { border-color: #dc3545; background: #f8d7da; } .btn-group { display: flex; gap: 10px; flex-wrap: wrap; margin: 15px 0; } button { padding: 12px 20px; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.2s; flex: 1; min-width: 120px; } button:disabled { opacity: 0.6; cursor: not-allowed; } .btn-primary { background: #007bff; color: white; } .btn-primary:hover { background: #0056b3; } .btn-success { background: #28a745; color: white; } .btn-success:hover { background: #1e7e34; } .btn-danger { background: #dc3545; color: white; } .btn-danger:hover { background: #c82333; } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover { background: #545b62; } .log-container { background: #2d3748; color: #e2e8f0; border-radius: 8px; height: 300px; overflow-y: auto; padding: 15px; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.4; } .log-entry { margin-bottom: 5px; word-wrap: break-word; } .log-timestamp { color: #a0aec0; margin-right: 8px; } .log-type-info { color: #63b3ed; } .log-type-success { color: #68d391; } .log-type-error { color: #fc8181; } .log-type-warning { color: #faf089; } .input-group { display: flex; gap: 10px; margin: 15px 0; align-items: center; } .input-group label { min-width: 100px; font-weight: 500; } .input-group input, .input-group select { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; } .token-display { background: #f1f3f4; border: 1px solid #ddd; border-radius: 4px; padding: 10px; font-family: monospace; font-size: 12px; word-break: break-all; max-height: 100px; overflow-y: auto; } </style> </head> <body> <div class="container"> <div class="header"> <h1>๐Ÿ” SSE MCP Authentication Test Client</h1> <p>Complete SSE transport testing with JWT authentication</p> </div> <div class="content"> <!-- Authentication Section --> <div class="auth-section"> <h3>๐ŸŽซ Authentication Management</h3> <div class="input-group"> <label>Username:</label> <input type="text" id="username" value="test-user" placeholder="Enter username"> </div> <div class="input-group"> <label>Permissions:</label> <select id="permissions" multiple size="3"> <option value="*" selected>All permissions (*)</option> <option value="quiz:read">Quiz Read</option> <option value="quiz:write">Quiz Write</option> <option value="quiz:delete">Quiz Delete</option> <option value="analytics:read">Analytics Read</option> </select> </div> <div class="btn-group"> <button class="btn-primary" onclick="generateToken()">๐ŸŽซ Generate Test Token</button> <button class="btn-secondary" onclick="clearToken()">๐Ÿ—‘๏ธ Clear Token</button> </div> <div id="tokenDisplay" class="token-display" style="display: none;"></div> </div> <!-- Connection Status --> <div class="status-grid"> <div class="status-card" id="connectionStatus"> <div id="connectionState">โŒ Disconnected</div> <div>Connection Status</div> </div> <div class="status-card" id="authStatus"> <div id="authState">โŒ Not Authenticated</div> <div>Auth Status</div> </div> <div class="status-card" id="messageCount"> <div id="messageCountValue">0</div> <div>Messages Received</div> </div> <div class="status-card" id="connectionTime"> <div id="connectionTimeValue">00:00:00</div> <div>Connection Time</div> </div> </div> <!-- Controls --> <div class="btn-group"> <button class="btn-success" onclick="connectSSE()" id="connectBtn">๐Ÿ”Œ Connect SSE</button> <button class="btn-danger" onclick="disconnectSSE()" id="disconnectBtn" disabled>๐Ÿ”Œ Disconnect</button> <button class="btn-primary" onclick="testMCPCall()" id="mcpTestBtn" disabled>๐Ÿงช Test MCP Call</button> <button class="btn-secondary" onclick="clearLog()">๐Ÿงน Clear Log</button> </div> <!-- Event Log --> <h3>๐Ÿ“‹ Event Log</h3> <div class="log-container" id="eventLog"></div> </div> </div> <script> let eventSource = null; let connectionStartTime = null; let messageCount = 0; let currentToken = null; let connectionTimer = null; // Initialize document.addEventListener('DOMContentLoaded', function() { log('๐ŸŒ SSE MCP Auth Test Client loaded', 'info'); log('๐Ÿ“‹ Generate a test token and connect to test SSE authentication', 'info'); }); async function generateToken() { try { log('๐ŸŽซ Generating test token...', 'info'); const username = document.getElementById('username').value || 'test-user'; const permissionSelect = document.getElementById('permissions'); const permissions = Array.from(permissionSelect.selectedOptions).map(opt => opt.value); const response = await fetch('/auth/test-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, permissions: permissions.length > 0 ? permissions : ['*'] }) }); const result = await response.json(); if (result.success) { currentToken = result.token; // Display token info const tokenDisplay = document.getElementById('tokenDisplay'); tokenDisplay.innerHTML = ` <strong>๐ŸŽซ Test Token Generated:</strong><br> <strong>User:</strong> ${result.user.username}<br> <strong>Permissions:</strong> ${result.user.permissions.join(', ')}<br> <strong>Expires:</strong> ${result.expiresIn}<br> <strong>Token:</strong> ${result.token.substring(0, 50)}...<br> <strong>SSE URL:</strong> ${result.usage.sseEndpoint} `; tokenDisplay.style.display = 'block'; log(`โœ… Token generated for user: ${result.user.username}`, 'success'); log(`๐Ÿ”‘ Permissions: ${result.user.permissions.join(', ')}`, 'info'); // Enable connect button document.getElementById('connectBtn').disabled = false; } else { log(`โŒ Token generation failed: ${result.error}`, 'error'); } } catch (error) { log(`โŒ Token generation error: ${error.message}`, 'error'); } } function clearToken() { currentToken = null; document.getElementById('tokenDisplay').style.display = 'none'; document.getElementById('connectBtn').disabled = true; if (eventSource) { disconnectSSE(); } log('๐Ÿ—‘๏ธ Token cleared', 'info'); } function connectSSE() { if (!currentToken) { log('โŒ No token available. Generate a token first.', 'error'); return; } if (eventSource) { log('โš ๏ธ Already connected. Disconnect first.', 'warning'); return; } try { log('๐Ÿ”Œ Connecting to SSE server with authentication...', 'info'); // Connect with token in URL parameter const sseUrl = `http://localhost:3001/sse?token=${currentToken}`; eventSource = new EventSource(sseUrl); connectionStartTime = Date.now(); startConnectionTimer(); eventSource.onopen = function(event) { log('๐ŸŽ‰ SSE connection opened', 'success'); updateConnectionStatus(true, false); document.getElementById('connectBtn').disabled = true; document.getElementById('disconnectBtn').disabled = false; }; eventSource.onmessage = function(event) { messageCount++; document.getElementById('messageCountValue').textContent = messageCount; try { const data = JSON.parse(event.data); if (data.type === 'connected') { updateConnectionStatus(true, data.data.authenticated); log(`๐Ÿ” Authenticated as: ${data.data.user}`, 'success'); log(`๐Ÿ›ก๏ธ Permissions: ${data.data.permissions.join(', ')}`, 'info'); document.getElementById('mcpTestBtn').disabled = false; } else if (data.type === 'heartbeat') { log(`๐Ÿ’“ Heartbeat: ${data.data.timestamp}`, 'info'); } else { log(`๐Ÿ“จ ${data.type}: ${JSON.stringify(data.data)}`, 'info'); } } catch (e) { log(`๐Ÿ“จ Raw message: ${event.data}`, 'info'); } }; eventSource.onerror = function(error) { log('โŒ SSE connection error', 'error'); console.error('SSE Error:', error); updateConnectionStatus(false, false); eventSource = null; stopConnectionTimer(); resetButtons(); }; } catch (error) { log(`โŒ Connection failed: ${error.message}`, 'error'); } } function disconnectSSE() { if (eventSource) { eventSource.close(); eventSource = null; log('๐Ÿ”Œ Disconnected from SSE server', 'info'); updateConnectionStatus(false, false); stopConnectionTimer(); resetButtons(); } } async function testMCPCall() { if (!currentToken) { log('โŒ No authentication token available', 'error'); return; } try { log('๐Ÿงช Testing MCP call...', 'info'); const mcpRequest = { jsonrpc: '2.0', id: Date.now(), method: 'tools/list', params: {} }; const response = await fetch('http://localhost:3001/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${currentToken}` }, body: JSON.stringify(mcpRequest) }); const result = await response.json(); if (result.result) { log(`โœ… MCP call successful: Found ${result.result.tools?.length || 0} tools`, 'success'); if (result.result.tools?.length > 0) { log(`๐Ÿ”ง Available tools: ${result.result.tools.map(t => t.name).join(', ')}`, 'info'); } } else if (result.error) { log(`โŒ MCP call failed: ${result.error.message}`, 'error'); } else { log(`โŒ Unexpected MCP response: ${JSON.stringify(result)}`, 'error'); } } catch (error) { log(`โŒ MCP call error: ${error.message}`, 'error'); } } function updateConnectionStatus(connected, authenticated) { const connectionStatus = document.getElementById('connectionStatus'); const authStatus = document.getElementById('authStatus'); const connectionState = document.getElementById('connectionState'); const authState = document.getElementById('authState'); if (connected) { connectionStatus.className = 'status-card connected'; connectionState.textContent = 'โœ… Connected'; } else { connectionStatus.className = 'status-card error'; connectionState.textContent = 'โŒ Disconnected'; } if (authenticated) { authStatus.className = 'status-card authenticated'; authState.textContent = '๐Ÿ” Authenticated'; } else { authStatus.className = 'status-card error'; authState.textContent = 'โŒ Not Authenticated'; } } function resetButtons() { document.getElementById('connectBtn').disabled = !currentToken; document.getElementById('disconnectBtn').disabled = true; document.getElementById('mcpTestBtn').disabled = true; } function startConnectionTimer() { connectionTimer = setInterval(() => { if (connectionStartTime) { const elapsed = Date.now() - connectionStartTime; const seconds = Math.floor(elapsed / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const timeStr = [ hours.toString().padStart(2, '0'), (minutes % 60).toString().padStart(2, '0'), (seconds % 60).toString().padStart(2, '0') ].join(':'); document.getElementById('connectionTimeValue').textContent = timeStr; } }, 1000); } function stopConnectionTimer() { if (connectionTimer) { clearInterval(connectionTimer); connectionTimer = null; } document.getElementById('connectionTimeValue').textContent = '00:00:00'; connectionStartTime = null; } function log(message, type = 'info') { const logContainer = document.getElementById('eventLog'); const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; const typeClass = `log-type-${type}`; logEntry.innerHTML = ` <span class="log-timestamp">[${timestamp}]</span> <span class="${typeClass}">${message}</span> `; logContainer.appendChild(logEntry); logContainer.scrollTop = logContainer.scrollHeight; } function clearLog() { document.getElementById('eventLog').innerHTML = ''; messageCount = 0; document.getElementById('messageCountValue').textContent = '0'; log('๐Ÿงน Log cleared', 'info'); } </script> </body> </html>