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
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>