|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Modern Web Communication Patterns - Interactive Guide</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
min-height: 100vh; |
|
overflow-x: hidden; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 20px; |
|
padding: 40px; |
|
backdrop-filter: blur(10px); |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.header h1 { |
|
font-size: 3em; |
|
margin-bottom: 15px; |
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
|
background: linear-gradient(45deg, #4ecdc4, #ffd93d); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
background-clip: text; |
|
} |
|
|
|
.header p { |
|
font-size: 1.2em; |
|
color: rgba(255, 255, 255, 0.9); |
|
max-width: 800px; |
|
margin: 0 auto; |
|
line-height: 1.6; |
|
} |
|
|
|
.navigation { |
|
display: flex; |
|
justify-content: center; |
|
gap: 15px; |
|
margin-bottom: 30px; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.nav-button { |
|
padding: 12px 24px; |
|
background: rgba(255, 255, 255, 0.1); |
|
border: 2px solid rgba(255, 255, 255, 0.3); |
|
border-radius: 25px; |
|
color: white; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
text-decoration: none; |
|
display: inline-block; |
|
} |
|
|
|
.nav-button:hover, .nav-button.active { |
|
background: rgba(78, 205, 196, 0.3); |
|
border-color: #4ecdc4; |
|
transform: translateY(-2px); |
|
box-shadow: 0 4px 15px rgba(78, 205, 196, 0.3); |
|
} |
|
|
|
.section { |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 20px; |
|
padding: 40px; |
|
margin-bottom: 30px; |
|
backdrop-filter: blur(10px); |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
display: none; |
|
} |
|
|
|
.section.active { |
|
display: block; |
|
} |
|
|
|
.section h2 { |
|
color: #4ecdc4; |
|
font-size: 2.2em; |
|
margin-bottom: 25px; |
|
text-align: center; |
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.section h3 { |
|
color: #ffd93d; |
|
font-size: 1.5em; |
|
margin: 25px 0 15px 0; |
|
border-bottom: 2px solid rgba(255, 217, 61, 0.3); |
|
padding-bottom: 10px; |
|
} |
|
|
|
.demo-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
|
gap: 25px; |
|
margin: 25px 0; |
|
} |
|
|
|
.demo-card { |
|
background: rgba(255, 255, 255, 0.05); |
|
border-radius: 15px; |
|
padding: 25px; |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.demo-card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
.demo-card h4 { |
|
color: #4ecdc4; |
|
margin-bottom: 15px; |
|
font-size: 1.3em; |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
gap: 10px; |
|
margin: 15px 0; |
|
flex-wrap: wrap; |
|
} |
|
|
|
button { |
|
padding: 10px 20px; |
|
border: none; |
|
border-radius: 25px; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
font-size: 14px; |
|
} |
|
|
|
.primary-btn { |
|
background: linear-gradient(45deg, #4ecdc4, #44a08d); |
|
color: white; |
|
} |
|
|
|
.secondary-btn { |
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52); |
|
color: white; |
|
} |
|
|
|
.accent-btn { |
|
background: linear-gradient(45deg, #ffd93d, #ffb74d); |
|
color: #333; |
|
} |
|
|
|
button:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
button:disabled { |
|
background: #666; |
|
cursor: not-allowed; |
|
transform: none; |
|
} |
|
|
|
.status { |
|
padding: 10px 15px; |
|
border-radius: 8px; |
|
margin: 10px 0; |
|
font-weight: 600; |
|
text-align: center; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.status.connected { |
|
background: linear-gradient(45deg, #4ecdc4, #44a08d); |
|
box-shadow: 0 4px 15px rgba(78, 205, 196, 0.3); |
|
} |
|
|
|
.status.disconnected { |
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52); |
|
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); |
|
} |
|
|
|
.status.active { |
|
background: rgba(78, 205, 196, 0.3); |
|
border: 1px solid #4ecdc4; |
|
} |
|
|
|
.status.inactive { |
|
background: rgba(255, 255, 255, 0.1); |
|
border: 1px solid rgba(255, 255, 255, 0.3); |
|
} |
|
|
|
.log { |
|
background: rgba(0, 0, 0, 0.4); |
|
padding: 15px; |
|
border-radius: 10px; |
|
max-height: 250px; |
|
overflow-y: auto; |
|
font-family: 'Courier New', monospace; |
|
font-size: 12px; |
|
line-height: 1.4; |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
margin: 15px 0; |
|
} |
|
|
|
.log-entry { |
|
margin-bottom: 8px; |
|
padding: 6px; |
|
border-radius: 4px; |
|
animation: slideIn 0.3s ease; |
|
} |
|
|
|
.log-entry.request { |
|
background: rgba(255, 193, 7, 0.2); |
|
border-left: 3px solid #ffc107; |
|
} |
|
|
|
.log-entry.response { |
|
background: rgba(40, 167, 69, 0.2); |
|
border-left: 3px solid #28a745; |
|
} |
|
|
|
.log-entry.notification { |
|
background: rgba(78, 205, 196, 0.2); |
|
border-left: 3px solid #4ecdc4; |
|
} |
|
|
|
.log-entry.system { |
|
background: rgba(255, 255, 255, 0.1); |
|
border-left: 3px solid #6c757d; |
|
} |
|
|
|
.log-entry.outgoing { |
|
background: rgba(255, 107, 107, 0.2); |
|
border-left: 4px solid #ff6b6b; |
|
} |
|
|
|
.log-entry.incoming { |
|
background: rgba(78, 205, 196, 0.2); |
|
border-left: 4px solid #4ecdc4; |
|
} |
|
|
|
.log-entry.data { |
|
background: rgba(78, 205, 196, 0.2); |
|
border-left: 3px solid #4ecdc4; |
|
} |
|
|
|
.comparison-table { |
|
width: 100%; |
|
border-collapse: collapse; |
|
margin: 20px 0; |
|
background: rgba(255, 255, 255, 0.05); |
|
border-radius: 10px; |
|
overflow: hidden; |
|
} |
|
|
|
.comparison-table th, |
|
.comparison-table td { |
|
padding: 12px; |
|
text-align: left; |
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|
} |
|
|
|
.comparison-table th { |
|
background: rgba(255, 255, 255, 0.1); |
|
font-weight: 600; |
|
color: #4ecdc4; |
|
} |
|
|
|
.comparison-table tr:hover { |
|
background: rgba(255, 255, 255, 0.05); |
|
} |
|
|
|
.code-block { |
|
background: rgba(0, 0, 0, 0.4); |
|
padding: 15px; |
|
border-radius: 8px; |
|
font-family: 'Courier New', monospace; |
|
font-size: 13px; |
|
margin: 15px 0; |
|
overflow-x: auto; |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
} |
|
|
|
.input-group { |
|
display: flex; |
|
gap: 10px; |
|
margin: 15px 0; |
|
} |
|
|
|
input[type="text"] { |
|
flex: 1; |
|
padding: 12px; |
|
border: 2px solid rgba(255, 255, 255, 0.3); |
|
border-radius: 25px; |
|
background: rgba(255, 255, 255, 0.1); |
|
color: white; |
|
font-size: 14px; |
|
} |
|
|
|
input[type="text"]:focus { |
|
outline: none; |
|
border-color: #4ecdc4; |
|
box-shadow: 0 0 15px rgba(78, 205, 196, 0.3); |
|
} |
|
|
|
input[type="text"]::placeholder { |
|
color: rgba(255, 255, 255, 0.6); |
|
} |
|
|
|
.stats { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); |
|
gap: 10px; |
|
margin: 15px 0; |
|
} |
|
|
|
.stat { |
|
text-align: center; |
|
background: rgba(255, 255, 255, 0.05); |
|
padding: 10px; |
|
border-radius: 8px; |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
} |
|
|
|
.stat-number { |
|
font-size: 1.3em; |
|
font-weight: bold; |
|
color: #4ecdc4; |
|
} |
|
|
|
.stat-label { |
|
font-size: 0.8em; |
|
color: #ccc; |
|
} |
|
|
|
.timestamp { |
|
color: #888; |
|
font-size: 11px; |
|
margin-right: 8px; |
|
} |
|
|
|
.explanation { |
|
background: rgba(255, 255, 255, 0.05); |
|
padding: 20px; |
|
border-radius: 10px; |
|
margin: 20px 0; |
|
border-left: 4px solid #ffd93d; |
|
} |
|
|
|
.flow-diagram { |
|
background: rgba(255, 255, 255, 0.05); |
|
padding: 20px; |
|
border-radius: 10px; |
|
margin: 20px 0; |
|
font-family: 'Courier New', monospace; |
|
font-size: 13px; |
|
line-height: 1.6; |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
} |
|
|
|
.progress-bar { |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 10px; |
|
height: 20px; |
|
margin: 10px 0; |
|
overflow: hidden; |
|
} |
|
|
|
.progress-fill { |
|
height: 100%; |
|
background: linear-gradient(45deg, #4ecdc4, #44a08d); |
|
border-radius: 10px; |
|
transition: width 0.3s ease; |
|
width: 0%; |
|
} |
|
|
|
@keyframes slideIn { |
|
from { |
|
opacity: 0; |
|
transform: translateX(-10px); |
|
} |
|
to { |
|
opacity: 1; |
|
transform: translateX(0); |
|
} |
|
} |
|
|
|
@keyframes pulse { |
|
0% { |
|
box-shadow: 0 0 0 0 rgba(78, 205, 196, 0.4); |
|
} |
|
70% { |
|
box-shadow: 0 0 0 10px rgba(78, 205, 196, 0); |
|
} |
|
100% { |
|
box-shadow: 0 0 0 0 rgba(78, 205, 196, 0); |
|
} |
|
} |
|
|
|
.pulse { |
|
animation: pulse 2s infinite; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.demo-grid { |
|
grid-template-columns: 1fr; |
|
} |
|
|
|
.navigation { |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2em; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>Modern Web Communication Patterns</h1> |
|
<p>Master the evolution from simple HTTP to real-time communication. Explore HTTP polling, Server-Sent Events, JSON-RPC, and how they power modern applications like MCP through interactive demonstrations.</p> |
|
</div> |
|
|
|
<div class="navigation"> |
|
<button class="nav-button active" onclick="showSection('overview')">Overview</button> |
|
<button class="nav-button" onclick="showSection('polling')">HTTP Polling</button> |
|
<button class="nav-button" onclick="showSection('sse')">Server-Sent Events</button> |
|
<button class="nav-button" onclick="showSection('comparison')">REST vs SSE</button> |
|
<button class="nav-button" onclick="showSection('jsonrpc')">JSON-RPC & Notifications</button> |
|
<button class="nav-button" onclick="showSection('mcp')">MCP in Action</button> |
|
</div> |
|
|
|
<!-- Overview Section --> |
|
<div id="overview" class="section active"> |
|
<h2>🌐 The Evolution of Web Communication</h2> |
|
|
|
<div class="explanation"> |
|
<h3>Understanding the Journey</h3> |
|
<p>Web communication has evolved from simple request-response patterns to sophisticated real-time systems. This interactive guide takes you through each major advancement, showing you not just how they work, but why they were created and when to use them.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>📡 HTTP Polling Era</h4> |
|
<p>The early solution for real-time updates. The client repeatedly asks the server "got any new data?" at regular intervals. Simple but inefficient.</p> |
|
<div class="code-block">setInterval(() => { |
|
fetch('/api/data') |
|
.then(response => response.json()) |
|
.then(data => updateUI(data)); |
|
}, 5000); // Check every 5 seconds</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>⚡ Server-Sent Events</h4> |
|
<p>The server can now push data to the client in real-time. The client opens a connection and the server sends updates whenever they occur.</p> |
|
<div class="code-block">const eventSource = new EventSource('/events'); |
|
eventSource.onmessage = (event) => { |
|
const data = JSON.parse(event.data); |
|
updateUI(data); // Real-time updates! |
|
};</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>🔄 JSON-RPC Protocol</h4> |
|
<p>A standardized way to call remote procedures using JSON. It brings structure to client-server communication with requests, responses, and notifications.</p> |
|
<div class="code-block">{ |
|
"jsonrpc": "2.0", |
|
"method": "search_files", |
|
"params": {"query": "README"}, |
|
"id": 123 |
|
}</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>🏗️ Model Context Protocol (MCP)</h4> |
|
<p>The modern approach combining HTTP requests with SSE streaming. It's how AI applications like Claude connect to external tools and data sources.</p> |
|
<div class="code-block">// HTTP request for actions |
|
POST /mcp/tools/call |
|
|
|
// SSE stream for responses |
|
data: {"result": [...], "id": 123} |
|
data: {"method": "progress", "params": {...}}</div> |
|
</div> |
|
</div> |
|
|
|
<div class="explanation"> |
|
<h3>🎯 What You'll Learn</h3> |
|
<p>Each section builds on the previous one, giving you hands-on experience with working examples. You'll understand not just the technical details, but the reasoning behind each approach and when to use them in your own projects.</p> |
|
</div> |
|
</div> |
|
|
|
<!-- Polling Section --> |
|
<div id="polling" class="section"> |
|
<h2>📡 HTTP Polling Strategies</h2> |
|
|
|
<div class="explanation"> |
|
<h3>The Challenge</h3> |
|
<p>HTTP was designed for one-way communication: client asks, server responds. But what if you need real-time updates? Polling was the first solution - repeatedly asking the server for new data.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>🔄 Simple Polling</h4> |
|
<div class="status inactive" id="simplePollingStatus">Inactive</div> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="startSimplePolling()">Start</button> |
|
<button class="secondary-btn" onclick="stopSimplePolling()">Stop</button> |
|
</div> |
|
<div style="text-align: center; margin: 10px 0; font-size: 0.9em; color: #4ecdc4;"> |
|
Polling every 3 seconds |
|
</div> |
|
<div class="log" id="simplePollingLog"></div> |
|
<div class="stats"> |
|
<div class="stat"> |
|
<div class="stat-number" id="simplePollingRequests">0</div> |
|
<div class="stat-label">Requests</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="simplePollingData">0</div> |
|
<div class="stat-label">Data Points</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="simplePollingEfficiency">0%</div> |
|
<div class="stat-label">Efficiency</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>⏳ Long Polling</h4> |
|
<div class="status inactive" id="longPollingStatus">Inactive</div> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="startLongPolling()">Start</button> |
|
<button class="secondary-btn" onclick="stopLongPolling()">Stop</button> |
|
</div> |
|
<div style="text-align: center; margin: 10px 0; font-size: 0.9em; color: #4ecdc4;"> |
|
Waits for data or 8s timeout |
|
</div> |
|
<div class="log" id="longPollingLog"></div> |
|
<div class="stats"> |
|
<div class="stat"> |
|
<div class="stat-number" id="longPollingRequests">0</div> |
|
<div class="stat-label">Requests</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="longPollingData">0</div> |
|
<div class="stat-label">Data Points</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="longPollingEfficiency">0%</div> |
|
<div class="stat-label">Efficiency</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="explanation"> |
|
<h3>🎮 Data Generator</h3> |
|
<p>Generate server-side events to see how different polling strategies respond:</p> |
|
<div class="controls"> |
|
<button class="accent-btn" onclick="generateData()">Generate Data Event</button> |
|
<button class="accent-btn" onclick="generateBurst()">Generate Burst (5 events)</button> |
|
<button class="accent-btn" onclick="toggleAutoGeneration()">Toggle Auto Generate</button> |
|
</div> |
|
<div style="text-align: center; margin: 10px 0; color: #ffd93d;" id="dataGeneratorStatus"> |
|
Click buttons to generate data events |
|
</div> |
|
</div> |
|
|
|
<table class="comparison-table"> |
|
<thead> |
|
<tr> |
|
<th>Strategy</th> |
|
<th>How it Works</th> |
|
<th>Pros</th> |
|
<th>Cons</th> |
|
<th>Best For</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
<tr> |
|
<td><strong>Simple Polling</strong></td> |
|
<td>Request every X seconds</td> |
|
<td>Simple, predictable</td> |
|
<td>Wasteful, delayed updates</td> |
|
<td>Infrequent updates</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Long Polling</strong></td> |
|
<td>Server waits to respond</td> |
|
<td>Efficient, lower latency</td> |
|
<td>Complex, resource intensive</td> |
|
<td>Real-time needs</td> |
|
</tr> |
|
</tbody> |
|
</table> |
|
</div> |
|
|
|
<!-- SSE Section --> |
|
<div id="sse" class="section"> |
|
<h2>⚡ Server-Sent Events (SSE)</h2> |
|
|
|
<div class="explanation"> |
|
<h3>The Revolution</h3> |
|
<p>SSE changed everything. Instead of the client constantly asking "got any updates?", the server can now say "I'll let you know when something happens." This creates a persistent connection where the server pushes data in real-time.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>🔌 SSE Connection</h4> |
|
<div class="status disconnected" id="sseStatus">Disconnected</div> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="connectSSE()">Connect SSE</button> |
|
<button class="secondary-btn" onclick="disconnectSSE()">Disconnect</button> |
|
</div> |
|
<div class="log" id="sseLog"></div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>📤 Send HTTP Requests</h4> |
|
<div class="explanation"> |
|
<p>In HTTP+SSE pattern, you send requests via HTTP and receive responses via the SSE stream.</p> |
|
</div> |
|
<div class="input-group"> |
|
<input type="text" id="sseMessageInput" placeholder="Enter JSON-RPC message..." value='{"jsonrpc": "2.0", "method": "ping", "id": 1}'> |
|
<button class="primary-btn" onclick="sendSSERequest()">Send Request</button> |
|
</div> |
|
<div class="controls"> |
|
<button class="accent-btn" onclick="simulateProgressUpdate()">Simulate Progress</button> |
|
<button class="accent-btn" onclick="simulateNotification()">Send Notification</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flow-diagram"> |
|
<strong>SSE Communication Flow:</strong> |
|
|
|
Client Server |
|
| | |
|
| 1. GET /events | |
|
| Accept: text/event-stream | |
|
|---------------------------------->| |
|
| | |
|
| 2. HTTP 200 OK | |
|
| Content-Type: text/event-stream | |
|
|<================================--| |
|
| | |
|
| 3. POST /api/action | |
|
| {request data} | |
|
|---------------------------------->| |
|
| | |
|
| 4. data: {response} | |
|
|<==================================| |
|
| | |
|
| 5. data: {notification} | |
|
|<==================================| |
|
</div> |
|
|
|
<div class="explanation"> |
|
<h3>✨ Key Benefits</h3> |
|
<ul> |
|
<li><strong>Real-time Updates:</strong> Server pushes data immediately when available</li> |
|
<li><strong>Efficient:</strong> No wasted requests when there's no data</li> |
|
<li><strong>Automatic Reconnection:</strong> Browser handles connection drops</li> |
|
<li><strong>HTTP Compatible:</strong> Works with existing web infrastructure</li> |
|
</ul> |
|
</div> |
|
</div> |
|
|
|
<!-- Comparison Section --> |
|
<div id="comparison" class="section"> |
|
<h2>🥊 REST vs HTTP+SSE</h2> |
|
|
|
<div class="explanation"> |
|
<h3>The Fundamental Difference</h3> |
|
<p>REST is great for traditional request-response patterns, but HTTP+SSE enables real-time, server-initiated communication. Let's see them in action side by side.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>🔄 Traditional REST</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="makeRestCall()">Make REST Call</button> |
|
<button class="accent-btn" onclick="startRestPolling()">Start Polling</button> |
|
<button class="secondary-btn" onclick="stopRestPolling()">Stop Polling</button> |
|
</div> |
|
<div class="log" id="restLog"></div> |
|
<div class="stats"> |
|
<div class="stat"> |
|
<div class="stat-number" id="restRequests">0</div> |
|
<div class="stat-label">Requests</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="restLatency">0ms</div> |
|
<div class="stat-label">Avg Latency</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>⚡ HTTP+SSE</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="establishSSEConnection()">Connect SSE</button> |
|
<button class="accent-btn" onclick="sendSSEAction()">Send Action</button> |
|
<button class="secondary-btn" onclick="closeSSEConnection()">Close Connection</button> |
|
</div> |
|
<div class="log" id="sseComparisonLog"></div> |
|
<div class="stats"> |
|
<div class="stat"> |
|
<div class="stat-number" id="sseEvents">0</div> |
|
<div class="stat-label">Events</div> |
|
</div> |
|
<div class="stat"> |
|
<div class="stat-number" id="sseLatency">0ms</div> |
|
<div class="stat-label">Avg Latency</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<table class="comparison-table"> |
|
<thead> |
|
<tr> |
|
<th>Aspect</th> |
|
<th>REST API</th> |
|
<th>HTTP+SSE</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
<tr> |
|
<td><strong>Communication</strong></td> |
|
<td>Request → Response</td> |
|
<td>Request → Response + Server Push</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Connection</strong></td> |
|
<td>Short-lived</td> |
|
<td>Persistent SSE stream</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Real-time</strong></td> |
|
<td>Requires polling</td> |
|
<td>Immediate server push</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Efficiency</strong></td> |
|
<td>High overhead for real-time</td> |
|
<td>Low overhead for real-time</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Scalability</strong></td> |
|
<td>Excellent (stateless)</td> |
|
<td>Good (connection limits)</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Use Case</strong></td> |
|
<td>CRUD operations</td> |
|
<td>Interactive, real-time apps</td> |
|
</tr> |
|
</tbody> |
|
</table> |
|
</div> |
|
|
|
<!-- JSON-RPC Section --> |
|
<div id="jsonrpc" class="section"> |
|
<h2>🔄 JSON-RPC 2.0 & Notifications</h2> |
|
|
|
<div class="explanation"> |
|
<h3>Structured Communication</h3> |
|
<p>JSON-RPC 2.0 brings structure to client-server communication. It defines a standard format for method calls, responses, and notifications. The key insight: messages with an ID expect a response, messages without an ID are notifications.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>📤 Send Requests & Notifications</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="sendJSONRPCRequest()">Send Request (with ID)</button> |
|
<button class="accent-btn" onclick="sendJSONRPCNotification()">Send Notification (no ID)</button> |
|
<button class="secondary-btn" onclick="clearJSONRPCLog()">Clear Log</button> |
|
</div> |
|
<div class="log" id="jsonrpcLog"></div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>🔔 Server Notifications</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="simulateFileChange()">File Changed</button> |
|
<button class="accent-btn" onclick="simulateServerProgress()">Progress Update</button> |
|
<button class="accent-btn" onclick="simulateStatusChange()">Status Change</button> |
|
</div> |
|
<div class="explanation"> |
|
<p>These are notifications the server sends to inform the client about events, without the client asking for them.</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flow-diagram"> |
|
<strong>JSON-RPC 2.0 Message Types:</strong> |
|
|
|
<strong>REQUEST (expects response):</strong> |
|
{ |
|
"jsonrpc": "2.0", |
|
"method": "search_files", |
|
"params": {"query": "README"}, |
|
"id": 123 |
|
} |
|
|
|
<strong>RESPONSE:</strong> |
|
{ |
|
"jsonrpc": "2.0", |
|
"result": ["README.md", "README.txt"], |
|
"id": 123 |
|
} |
|
|
|
<strong>NOTIFICATION (no response):</strong> |
|
{ |
|
"jsonrpc": "2.0", |
|
"method": "file_changed", |
|
"params": {"file": "main.js", "event": "modified"} |
|
} |
|
</div> |
|
|
|
<table class="comparison-table"> |
|
<thead> |
|
<tr> |
|
<th>Message Type</th> |
|
<th>Has ID?</th> |
|
<th>Expects Response?</th> |
|
<th>Purpose</th> |
|
<th>Example</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
<tr> |
|
<td><strong>Request</strong></td> |
|
<td>✅ Yes</td> |
|
<td>✅ Yes</td> |
|
<td>Ask server to do something</td> |
|
<td>search_files, get_data</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Response</strong></td> |
|
<td>✅ Yes (matches request)</td> |
|
<td>❌ No</td> |
|
<td>Answer to a request</td> |
|
<td>Results, errors</td> |
|
</tr> |
|
<tr> |
|
<td><strong>Notification</strong></td> |
|
<td>❌ No</td> |
|
<td>❌ No</td> |
|
<td>Inform about events</td> |
|
<td>file_changed, progress</td> |
|
</tr> |
|
</tbody> |
|
</table> |
|
</div> |
|
|
|
<!-- MCP Section --> |
|
<div id="mcp" class="section"> |
|
<h2>🏗️ MCP: Bringing It All Together</h2> |
|
|
|
<div class="explanation"> |
|
<h3>The Modern Approach</h3> |
|
<p>Model Context Protocol (MCP) combines all these concepts into a powerful system for AI applications. It uses HTTP for requests, SSE for responses and notifications, and JSON-RPC for structured communication.</p> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card"> |
|
<h4>🔌 MCP Session</h4> |
|
<div class="status disconnected" id="mcpStatus">Disconnected</div> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="startMCPSession()">Start MCP Session</button> |
|
<button class="secondary-btn" onclick="endMCPSession()">End Session</button> |
|
</div> |
|
<div class="log" id="mcpLog"></div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>🛠️ MCP Tools</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="callMCPTool('search_files')">Search Files</button> |
|
<button class="accent-btn" onclick="callMCPTool('git_status')">Git Status</button> |
|
<button class="accent-btn" onclick="callMCPTool('read_file')">Read File</button> |
|
</div> |
|
<div class="explanation"> |
|
<p>MCP tools are functions that the server exposes to the client. Each tool call is a JSON-RPC request that may result in streaming responses.</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="demo-card"> |
|
<h4>📊 MCP Operation with Progress</h4> |
|
<div class="controls"> |
|
<button class="primary-btn" onclick="startLongMCPOperation()">Start Long Operation</button> |
|
<button class="secondary-btn" onclick="cancelMCPOperation()">Cancel Operation</button> |
|
</div> |
|
<div class="progress-bar"> |
|
<div class="progress-fill" id="mcpProgress"></div> |
|
</div> |
|
<div style="text-align: center; margin: 10px 0; color: #4ecdc4;" id="mcpProgressText"> |
|
Ready to start operation |
|
</div> |
|
</div> |
|
|
|
<div class="flow-diagram"> |
|
<strong>MCP Communication Flow:</strong> |
|
|
|
1. Client establishes SSE connection to /mcp/events |
|
2. Client sends HTTP POST to /mcp/handshake |
|
3. Server responds via SSE with capabilities |
|
4. Client sends tool calls via HTTP POST |
|
5. Server sends responses via SSE (can be streaming) |
|
6. Server sends notifications via SSE (file changes, progress, etc.) |
|
</div> |
|
|
|
<div class="explanation"> |
|
<h3>🎯 Why MCP Uses HTTP+SSE</h3> |
|
<p>MCP chose HTTP+SSE over simple REST because AI applications need:</p> |
|
<ul> |
|
<li><strong>Streaming Responses:</strong> Large search results can be sent incrementally</li> |
|
<li><strong>Real-time Notifications:</strong> File changes, system events push immediately</li> |
|
<li><strong>Progress Updates:</strong> Long operations can report progress in real-time</li> |
|
<li><strong>Session Context:</strong> Maintain state across multiple interactions</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// Global state management |
|
let currentSection = 'overview'; |
|
let globalRequestId = 1; |
|
let dataQueue = []; |
|
let dataCounter = 0; |
|
let autoGenerating = false; |
|
|
|
// Component states |
|
let states = { |
|
simplePolling: false, |
|
longPolling: false, |
|
sseConnected: false, |
|
restPolling: false, |
|
mcpSession: false, |
|
mcpOperation: false |
|
}; |
|
|
|
// Statistics |
|
let stats = { |
|
simplePolling: { requests: 0, dataPoints: 0 }, |
|
longPolling: { requests: 0, dataPoints: 0 }, |
|
rest: { requests: 0, latency: [] }, |
|
sse: { events: 0, latency: [] } |
|
}; |
|
|
|
// Utility functions |
|
function showSection(sectionId) { |
|
// Hide all sections |
|
document.querySelectorAll('.section').forEach(section => { |
|
section.classList.remove('active'); |
|
}); |
|
|
|
// Show selected section |
|
document.getElementById(sectionId).classList.add('active'); |
|
|
|
// Update navigation |
|
document.querySelectorAll('.nav-button').forEach(btn => { |
|
btn.classList.remove('active'); |
|
}); |
|
event.target.classList.add('active'); |
|
|
|
currentSection = sectionId; |
|
} |
|
|
|
function addLogEntry(logId, message, type = 'system') { |
|
const log = document.getElementById(logId); |
|
if (!log) return; |
|
|
|
const entry = document.createElement('div'); |
|
entry.className = `log-entry ${type}`; |
|
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
entry.innerHTML = `<span class="timestamp">[${timestamp}]</span>${message}`; |
|
|
|
log.appendChild(entry); |
|
log.scrollTop = log.scrollHeight; |
|
|
|
// Keep only last 30 entries |
|
if (log.children.length > 30) { |
|
log.removeChild(log.firstChild); |
|
} |
|
} |
|
|
|
function updateStats() { |
|
// Simple polling stats |
|
if (document.getElementById('simplePollingRequests')) { |
|
document.getElementById('simplePollingRequests').textContent = stats.simplePolling.requests; |
|
document.getElementById('simplePollingData').textContent = stats.simplePolling.dataPoints; |
|
const efficiency = stats.simplePolling.requests > 0 ? |
|
Math.round((stats.simplePolling.dataPoints / stats.simplePolling.requests) * 100) : 0; |
|
document.getElementById('simplePollingEfficiency').textContent = efficiency + '%'; |
|
} |
|
|
|
// Long polling stats |
|
if (document.getElementById('longPollingRequests')) { |
|
document.getElementById('longPollingRequests').textContent = stats.longPolling.requests; |
|
document.getElementById('longPollingData').textContent = stats.longPolling.dataPoints; |
|
const efficiency = stats.longPolling.requests > 0 ? |
|
Math.round((stats.longPolling.dataPoints / stats.longPolling.requests) * 100) : 0; |
|
document.getElementById('longPollingEfficiency').textContent = efficiency + '%'; |
|
} |
|
|
|
// REST stats |
|
if (document.getElementById('restRequests')) { |
|
document.getElementById('restRequests').textContent = stats.rest.requests; |
|
const avgLatency = stats.rest.latency.length > 0 ? |
|
Math.round(stats.rest.latency.reduce((a, b) => a + b, 0) / stats.rest.latency.length) : 0; |
|
document.getElementById('restLatency').textContent = avgLatency + 'ms'; |
|
} |
|
|
|
// SSE stats |
|
if (document.getElementById('sseEvents')) { |
|
document.getElementById('sseEvents').textContent = stats.sse.events; |
|
const avgLatency = stats.sse.latency.length > 0 ? |
|
Math.round(stats.sse.latency.reduce((a, b) => a + b, 0) / stats.sse.latency.length) : 0; |
|
document.getElementById('sseLatency').textContent = avgLatency + 'ms'; |
|
} |
|
} |
|
|
|
// Data generation functions |
|
function generateData() { |
|
const data = { |
|
id: ++dataCounter, |
|
timestamp: new Date().toISOString(), |
|
message: `Data point ${dataCounter}`, |
|
value: Math.floor(Math.random() * 100) |
|
}; |
|
dataQueue.push(data); |
|
} |
|
|
|
function generateBurst() { |
|
for (let i = 0; i < 5; i++) { |
|
generateData(); |
|
} |
|
} |
|
|
|
function toggleAutoGeneration() { |
|
autoGenerating = !autoGenerating; |
|
const status = document.getElementById('dataGeneratorStatus'); |
|
|
|
if (autoGenerating) { |
|
status.textContent = 'Auto-generating data events...'; |
|
status.style.color = '#4ecdc4'; |
|
autoGenerateData(); |
|
} else { |
|
status.textContent = 'Auto-generation stopped'; |
|
status.style.color = '#ff6b6b'; |
|
} |
|
} |
|
|
|
function autoGenerateData() { |
|
if (!autoGenerating) return; |
|
|
|
// Generate data randomly (30% chance each 2 seconds) |
|
if (Math.random() < 0.3) { |
|
generateData(); |
|
} |
|
|
|
setTimeout(autoGenerateData, 2000); |
|
} |
|
|
|
// HTTP Polling implementations |
|
function startSimplePolling() { |
|
if (states.simplePolling) return; |
|
states.simplePolling = true; |
|
|
|
const statusEl = document.getElementById('simplePollingStatus'); |
|
statusEl.textContent = 'Active'; |
|
statusEl.className = 'status active'; |
|
|
|
addLogEntry('simplePollingLog', '🚀 Simple polling started', 'system'); |
|
|
|
function poll() { |
|
if (!states.simplePolling) return; |
|
|
|
addLogEntry('simplePollingLog', '📤 GET /api/data', 'request'); |
|
stats.simplePolling.requests++; |
|
|
|
setTimeout(() => { |
|
const newData = dataQueue.splice(0); |
|
|
|
if (newData.length > 0) { |
|
addLogEntry('simplePollingLog', `📥 Response: ${newData.length} items`, 'response'); |
|
stats.simplePolling.dataPoints += newData.length; |
|
newData.forEach(item => { |
|
addLogEntry('simplePollingLog', `📊 ${item.message}`, 'data'); |
|
}); |
|
} else { |
|
addLogEntry('simplePollingLog', '📥 Response: No new data', 'response'); |
|
} |
|
|
|
updateStats(); |
|
setTimeout(poll, 3000); |
|
}, 150); |
|
} |
|
|
|
poll(); |
|
} |
|
|
|
function stopSimplePolling() { |
|
states.simplePolling = false; |
|
const statusEl = document.getElementById('simplePollingStatus'); |
|
statusEl.textContent = 'Inactive'; |
|
statusEl.className = 'status inactive'; |
|
addLogEntry('simplePollingLog', '⏹️ Simple polling stopped', 'system'); |
|
} |
|
|
|
function startLongPolling() { |
|
if (states.longPolling) return; |
|
states.longPolling = true; |
|
|
|
const statusEl = document.getElementById('longPollingStatus'); |
|
statusEl.textContent = 'Active'; |
|
statusEl.className = 'status active'; |
|
|
|
addLogEntry('longPollingLog', '🚀 Long polling started', 'system'); |
|
|
|
function longPoll() { |
|
if (!states.longPolling) return; |
|
|
|
addLogEntry('longPollingLog', '📤 GET /api/data?wait=true', 'request'); |
|
stats.longPolling.requests++; |
|
|
|
const startTime = Date.now(); |
|
const timeout = 8000; |
|
|
|
function checkForData() { |
|
if (!states.longPolling) return; |
|
|
|
const elapsed = Date.now() - startTime; |
|
|
|
if (dataQueue.length > 0) { |
|
const newData = dataQueue.splice(0); |
|
addLogEntry('longPollingLog', `📥 Response: ${newData.length} items (${elapsed}ms)`, 'response'); |
|
stats.longPolling.dataPoints += newData.length; |
|
newData.forEach(item => { |
|
addLogEntry('longPollingLog', `📊 ${item.message}`, 'data'); |
|
}); |
|
updateStats(); |
|
setTimeout(longPoll, 100); |
|
} else if (elapsed >= timeout) { |
|
addLogEntry('longPollingLog', '📥 Response: Timeout', 'response'); |
|
updateStats(); |
|
setTimeout(longPoll, 100); |
|
} else { |
|
setTimeout(checkForData, 100); |
|
} |
|
} |
|
|
|
setTimeout(checkForData, 100); |
|
} |
|
|
|
longPoll(); |
|
} |
|
|
|
function stopLongPolling() { |
|
states.longPolling = false; |
|
const statusEl = document.getElementById('longPollingStatus'); |
|
statusEl.textContent = 'Inactive'; |
|
statusEl.className = 'status inactive'; |
|
addLogEntry('longPollingLog', '⏹️ Long polling stopped', 'system'); |
|
} |
|
|
|
// SSE implementations |
|
function connectSSE() { |
|
if (states.sseConnected) return; |
|
states.sseConnected = true; |
|
|
|
const statusEl = document.getElementById('sseStatus'); |
|
statusEl.textContent = 'Connected'; |
|
statusEl.className = 'status connected'; |
|
|
|
addLogEntry('sseLog', '🔌 Establishing SSE connection...', 'system'); |
|
|
|
setTimeout(() => { |
|
addLogEntry('sseLog', '✅ SSE connection established', 'system'); |
|
addLogEntry('sseLog', '📡 data: {"event": "connected", "sessionId": "abc123"}', 'incoming'); |
|
|
|
// Start receiving periodic notifications |
|
simulateSSENotifications(); |
|
}, 800); |
|
} |
|
|
|
function disconnectSSE() { |
|
states.sseConnected = false; |
|
const statusEl = document.getElementById('sseStatus'); |
|
statusEl.textContent = 'Disconnected'; |
|
statusEl.className = 'status disconnected'; |
|
addLogEntry('sseLog', '❌ SSE connection closed', 'system'); |
|
} |
|
|
|
function sendSSERequest() { |
|
if (!states.sseConnected) { |
|
addLogEntry('sseLog', '❌ Not connected! Connect first.', 'system'); |
|
return; |
|
} |
|
|
|
const input = document.getElementById('sseMessageInput'); |
|
let message = input.value.trim(); |
|
|
|
if (!message) return; |
|
|
|
try { |
|
const jsonMessage = JSON.parse(message); |
|
if (!jsonMessage.id) { |
|
jsonMessage.id = globalRequestId++; |
|
} |
|
|
|
addLogEntry('sseLog', `📤 HTTP POST: ${JSON.stringify(jsonMessage)}`, 'outgoing'); |
|
|
|
setTimeout(() => { |
|
const response = { |
|
jsonrpc: "2.0", |
|
result: jsonMessage.method === 'ping' ? 'pong' : 'Operation completed', |
|
id: jsonMessage.id |
|
}; |
|
addLogEntry('sseLog', `📡 SSE data: ${JSON.stringify(response)}`, 'incoming'); |
|
}, 600); |
|
|
|
} catch (error) { |
|
addLogEntry('sseLog', `❌ Invalid JSON: ${error.message}`, 'system'); |
|
} |
|
} |
|
|
|
function simulateProgressUpdate() { |
|
if (!states.sseConnected) return; |
|
|
|
for (let i = 0; i <= 100; i += 20) { |
|
setTimeout(() => { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "progress", |
|
params: { |
|
operation: "file_processing", |
|
percent: i, |
|
message: i === 100 ? "Complete!" : `Processing... ${i}%` |
|
} |
|
}; |
|
addLogEntry('sseLog', `📡 SSE notification: ${JSON.stringify(notification)}`, 'incoming'); |
|
}, (i / 20) * 500); |
|
} |
|
} |
|
|
|
function simulateNotification() { |
|
if (!states.sseConnected) return; |
|
|
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "file_changed", |
|
params: { |
|
file: "/project/src/main.js", |
|
event: "modified", |
|
timestamp: new Date().toISOString() |
|
} |
|
}; |
|
addLogEntry('sseLog', `📡 SSE notification: ${JSON.stringify(notification)}`, 'incoming'); |
|
} |
|
|
|
function simulateSSENotifications() { |
|
if (!states.sseConnected) return; |
|
|
|
setTimeout(() => { |
|
if (states.sseConnected) { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "server_status", |
|
params: {status: "ready", load: Math.random().toFixed(2)} |
|
}; |
|
addLogEntry('sseLog', `📡 SSE notification: ${JSON.stringify(notification)}`, 'incoming'); |
|
simulateSSENotifications(); |
|
} |
|
}, 8000 + Math.random() * 4000); |
|
} |
|
|
|
// REST vs SSE comparison |
|
function makeRestCall() { |
|
const startTime = Date.now(); |
|
addLogEntry('restLog', '📤 POST /api/search', 'request'); |
|
stats.rest.requests++; |
|
|
|
setTimeout(() => { |
|
const latency = Date.now() - startTime; |
|
stats.rest.latency.push(latency); |
|
addLogEntry('restLog', `📥 200 OK (${latency}ms)`, 'response'); |
|
addLogEntry('restLog', `📊 {"results": [{"name": "file1.txt"}, {"name": "file2.js"}]}`, 'data'); |
|
addLogEntry('restLog', '🔌 Connection closed', 'system'); |
|
updateStats(); |
|
}, 200 + Math.random() * 300); |
|
} |
|
|
|
function startRestPolling() { |
|
if (states.restPolling) return; |
|
states.restPolling = true; |
|
|
|
addLogEntry('restLog', '🔄 Starting REST polling...', 'system'); |
|
|
|
function restPoll() { |
|
if (!states.restPolling) return; |
|
|
|
const startTime = Date.now(); |
|
addLogEntry('restLog', '📤 GET /api/status', 'request'); |
|
stats.rest.requests++; |
|
|
|
setTimeout(() => { |
|
const latency = Date.now() - startTime; |
|
stats.rest.latency.push(latency); |
|
const hasData = Math.random() < 0.3; |
|
|
|
if (hasData) { |
|
addLogEntry('restLog', `📥 200 OK - Data available (${latency}ms)`, 'response'); |
|
} else { |
|
addLogEntry('restLog', `📥 200 OK - No data (${latency}ms)`, 'response'); |
|
} |
|
|
|
updateStats(); |
|
setTimeout(restPoll, 2000); |
|
}, 150 + Math.random() * 200); |
|
} |
|
|
|
restPoll(); |
|
} |
|
|
|
function stopRestPolling() { |
|
states.restPolling = false; |
|
addLogEntry('restLog', '⏹️ REST polling stopped', 'system'); |
|
} |
|
|
|
function establishSSEConnection() { |
|
if (states.sseConnected) return; |
|
|
|
addLogEntry('sseComparisonLog', '🔌 Establishing SSE connection...', 'system'); |
|
|
|
setTimeout(() => { |
|
states.sseConnected = true; |
|
addLogEntry('sseComparisonLog', '✅ SSE connection established', 'system'); |
|
stats.sse.events++; |
|
updateStats(); |
|
|
|
// Simulate periodic server events |
|
sseEventSimulator(); |
|
}, 500); |
|
} |
|
|
|
function sendSSEAction() { |
|
if (!states.sseConnected) { |
|
addLogEntry('sseComparisonLog', '❌ Not connected!', 'system'); |
|
return; |
|
} |
|
|
|
const startTime = Date.now(); |
|
addLogEntry('sseComparisonLog', '📤 POST /api/action', 'request'); |
|
|
|
setTimeout(() => { |
|
const latency = Date.now() - startTime; |
|
stats.sse.latency.push(latency); |
|
stats.sse.events++; |
|
addLogEntry('sseComparisonLog', `📡 SSE response (${latency}ms)`, 'incoming'); |
|
updateStats(); |
|
}, 50 + Math.random() * 100); |
|
} |
|
|
|
function closeSSEConnection() { |
|
states.sseConnected = false; |
|
addLogEntry('sseComparisonLog', '❌ SSE connection closed', 'system'); |
|
} |
|
|
|
function sseEventSimulator() { |
|
if (!states.sseConnected) return; |
|
|
|
setTimeout(() => { |
|
if (states.sseConnected) { |
|
const latency = 25 + Math.random() * 50; |
|
stats.sse.latency.push(latency); |
|
stats.sse.events++; |
|
addLogEntry('sseComparisonLog', `📡 SSE event (${Math.round(latency)}ms)`, 'incoming'); |
|
updateStats(); |
|
sseEventSimulator(); |
|
} |
|
}, 3000 + Math.random() * 4000); |
|
} |
|
|
|
// JSON-RPC implementations |
|
function sendJSONRPCRequest() { |
|
const request = { |
|
jsonrpc: "2.0", |
|
method: "search_files", |
|
params: {query: "README", path: "/project"}, |
|
id: globalRequestId++ |
|
}; |
|
|
|
addLogEntry('jsonrpcLog', `📤 REQUEST: ${JSON.stringify(request)}`, 'request'); |
|
|
|
setTimeout(() => { |
|
const response = { |
|
jsonrpc: "2.0", |
|
result: ["README.md", "README.txt"], |
|
id: request.id |
|
}; |
|
addLogEntry('jsonrpcLog', `📥 RESPONSE: ${JSON.stringify(response)}`, 'response'); |
|
}, 800); |
|
} |
|
|
|
function sendJSONRPCNotification() { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "client_ready", |
|
params: {timestamp: new Date().toISOString()} |
|
}; |
|
|
|
addLogEntry('jsonrpcLog', `📢 NOTIFICATION: ${JSON.stringify(notification)}`, 'notification'); |
|
addLogEntry('jsonrpcLog', 'ℹ️ No response expected (no ID field)', 'system'); |
|
} |
|
|
|
function simulateFileChange() { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "resources/updated", |
|
params: { |
|
uri: "file:///project/src/main.js", |
|
event: "modified", |
|
timestamp: new Date().toISOString() |
|
} |
|
}; |
|
addLogEntry('jsonrpcLog', `🔔 SERVER NOTIFICATION: ${JSON.stringify(notification)}`, 'notification'); |
|
} |
|
|
|
function simulateServerProgress() { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "progress", |
|
params: { |
|
operation: "indexing", |
|
percent: Math.floor(Math.random() * 100), |
|
message: "Indexing files..." |
|
} |
|
}; |
|
addLogEntry('jsonrpcLog', `📊 PROGRESS: ${JSON.stringify(notification)}`, 'notification'); |
|
} |
|
|
|
function simulateStatusChange() { |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "server/status", |
|
params: { |
|
status: ["ready", "busy", "idle"][Math.floor(Math.random() * 3)], |
|
load: Math.random().toFixed(2) |
|
} |
|
}; |
|
addLogEntry('jsonrpcLog', `🔄 STATUS: ${JSON.stringify(notification)}`, 'notification'); |
|
} |
|
|
|
function clearJSONRPCLog() { |
|
document.getElementById('jsonrpcLog').innerHTML = ''; |
|
} |
|
|
|
// MCP implementations |
|
function startMCPSession() { |
|
if (states.mcpSession) return; |
|
states.mcpSession = true; |
|
|
|
const statusEl = document.getElementById('mcpStatus'); |
|
statusEl.textContent = 'Connected'; |
|
statusEl.className = 'status connected'; |
|
|
|
addLogEntry('mcpLog', '🔌 Establishing MCP session...', 'system'); |
|
|
|
setTimeout(() => { |
|
addLogEntry('mcpLog', '📤 POST /mcp/handshake', 'request'); |
|
setTimeout(() => { |
|
addLogEntry('mcpLog', '📡 SSE: {"jsonrpc": "2.0", "result": {"capabilities": ["tools", "resources"]}}', 'incoming'); |
|
addLogEntry('mcpLog', '✅ MCP session established', 'system'); |
|
|
|
// Start MCP notifications |
|
mcpNotificationSimulator(); |
|
}, 600); |
|
}, 500); |
|
} |
|
|
|
function endMCPSession() { |
|
states.mcpSession = false; |
|
const statusEl = document.getElementById('mcpStatus'); |
|
statusEl.textContent = 'Disconnected'; |
|
statusEl.className = 'status disconnected'; |
|
addLogEntry('mcpLog', '❌ MCP session ended', 'system'); |
|
} |
|
|
|
function callMCPTool(toolName) { |
|
if (!states.mcpSession) { |
|
addLogEntry('mcpLog', '❌ No active session!', 'system'); |
|
return; |
|
} |
|
|
|
const request = { |
|
jsonrpc: "2.0", |
|
method: "tools/call", |
|
params: { |
|
name: toolName, |
|
arguments: getToolArguments(toolName) |
|
}, |
|
id: globalRequestId++ |
|
}; |
|
|
|
addLogEntry('mcpLog', `📤 HTTP POST: ${JSON.stringify(request)}`, 'request'); |
|
|
|
// Simulate streaming response |
|
setTimeout(() => { |
|
const response = { |
|
jsonrpc: "2.0", |
|
result: getToolResult(toolName), |
|
id: request.id |
|
}; |
|
addLogEntry('mcpLog', `📡 SSE response: ${JSON.stringify(response)}`, 'incoming'); |
|
}, 800); |
|
} |
|
|
|
function getToolArguments(toolName) { |
|
const args = { |
|
search_files: {query: "README", path: "/project"}, |
|
git_status: {}, |
|
read_file: {path: "/project/main.js"} |
|
}; |
|
return args[toolName] || {}; |
|
} |
|
|
|
function getToolResult(toolName) { |
|
const results = { |
|
search_files: {files: ["README.md", "package.json"]}, |
|
git_status: {status: "clean", branch: "main"}, |
|
read_file: {content: "// Main application file\nconsole.log('Hello World');"} |
|
}; |
|
return results[toolName] || {status: "completed"}; |
|
} |
|
|
|
function startLongMCPOperation() { |
|
if (states.mcpOperation) return; |
|
states.mcpOperation = true; |
|
|
|
const progressEl = document.getElementById('mcpProgress'); |
|
const textEl = document.getElementById('mcpProgressText'); |
|
|
|
addLogEntry('mcpLog', '🚀 Starting long operation...', 'system'); |
|
textEl.textContent = 'Operation in progress...'; |
|
|
|
let progress = 0; |
|
const interval = setInterval(() => { |
|
progress += 10; |
|
progressEl.style.width = progress + '%'; |
|
textEl.textContent = `Processing... ${progress}%`; |
|
|
|
// Send progress notification |
|
const notification = { |
|
jsonrpc: "2.0", |
|
method: "progress", |
|
params: { |
|
operation: "file_indexing", |
|
percent: progress, |
|
message: `Indexing files... ${progress}%` |
|
} |
|
}; |
|
addLogEntry('mcpLog', `📊 Progress: ${JSON.stringify(notification)}`, 'notification'); |
|
|
|
if (progress >= 100 || !states.mcpOperation) { |
|
clearInterval(interval); |
|
if (states.mcpOperation) { |
|
textEl.textContent = 'Operation completed!'; |
|
addLogEntry('mcpLog', '✅ Operation completed', 'system'); |
|
} |
|
states.mcpOperation = false; |
|
} |
|
}, 800); |
|
} |
|
|
|
function cancelMCPOperation() { |
|
states.mcpOperation = false; |
|
const progressEl = document.getElementById('mcpProgress'); |
|
const textEl = document.getElementById('mcpProgressText'); |
|
|
|
progressEl.style.width = '0%'; |
|
textEl.textContent = 'Operation cancelled'; |
|
addLogEntry('mcpLog', '❌ Operation cancelled', 'system'); |
|
} |
|
|
|
function mcpNotificationSimulator() { |
|
if (!states.mcpSession) return; |
|
|
|
setTimeout(() => { |
|
if (states.mcpSession) { |
|
const notifications = [ |
|
{ |
|
jsonrpc: "2.0", |
|
method: "resources/updated", |
|
params: {uri: "file:///project/src/main.js", event: "modified"} |
|
}, |
|
{ |
|
jsonrpc: "2.0", |
|
method: "tools/updated", |
|
params: {added: ["new_tool"], removed: []} |
|
} |
|
]; |
|
|
|
const notification = notifications[Math.floor(Math.random() * notifications.length)]; |
|
addLogEntry('mcpLog', `🔔 MCP notification: ${JSON.stringify(notification)}`, 'notification'); |
|
mcpNotificationSimulator(); |
|
} |
|
}, 10000 + Math.random() * 10000); |
|
} |
|
|
|
// Initialize the application |
|
function init() { |
|
updateStats(); |
|
|
|
// Add welcome messages to all logs |
|
addLogEntry('simplePollingLog', '💡 Click "Start" to begin simple polling', 'system'); |
|
addLogEntry('longPollingLog', '💡 Click "Start" to begin long polling', 'system'); |
|
addLogEntry('sseLog', '💡 Click "Connect SSE" to establish connection', 'system'); |
|
addLogEntry('restLog', '💡 Click buttons to see REST API behavior', 'system'); |
|
addLogEntry('sseComparisonLog', '💡 Click "Connect SSE" to start real-time communication', 'system'); |
|
addLogEntry('jsonrpcLog', '💡 Try sending requests and notifications', 'system'); |
|
addLogEntry('mcpLog', '💡 Click "Start MCP Session" to begin', 'system'); |
|
} |
|
|
|
// Initialize when page loads |
|
document.addEventListener('DOMContentLoaded', init); |
|
</script> |
|
</body> |
|
</html> |