index.html•34.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP Protocol Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0a0a0a;
color: #e0e0e0;
padding: 2rem;
padding-bottom: 100px;
}
.header {
text-align: center;
margin-bottom: 3rem;
}
.header h1 {
font-size: 3rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem;
}
.header p {
color: #888;
font-size: 1.2rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.card h2 {
color: #fff;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-number {
background: #667eea;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.info-box {
background: rgba(102, 126, 234, 0.1);
border: 1px solid #667eea;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.info-box h4 {
color: #667eea;
margin-bottom: 0.5rem;
}
.info-box p {
color: #ccc;
font-size: 0.9rem;
line-height: 1.5;
}
.columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-top: 1.5rem;
}
.column {
background: #0f0f0f;
border: 1px solid #333;
border-radius: 8px;
padding: 1.5rem;
}
.column-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
color: #fff;
font-weight: 600;
}
.method-badge {
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
}
.code-block {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
font-family: "Consolas", "Monaco", monospace;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.code-block pre {
margin: 0;
white-space: pre-wrap;
}
.headers-list {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 8px;
padding: 1rem;
}
.header-item {
display: flex;
gap: 1rem;
padding: 0.5rem 0;
border-bottom: 1px solid #222;
}
.header-item:last-child {
border-bottom: none;
}
.header-key {
color: #667eea;
font-weight: 600;
min-width: 150px;
font-family: monospace;
}
.header-value {
color: #e0e0e0;
font-family: monospace;
word-break: break-all;
}
.session-id-highlight {
background: rgba(102, 126, 234, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid #667eea;
}
.action-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem;
display: block;
margin-left: auto;
margin-right: auto;
}
.action-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
margin-left: 1rem;
}
.status-pending {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
}
.status-success {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.status-error {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
}
.sse-stream {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 8px;
padding: 1rem;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 0.8rem;
}
.sse-event {
padding: 0.5rem;
border-bottom: 1px solid #222;
animation: slideIn 0.3s ease;
}
.sse-event:last-child {
border-bottom: none;
}
.sse-event-type {
color: #667eea;
font-weight: bold;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.sse-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 400px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
max-height: 500px;
display: flex;
flex-direction: column;
}
.sse-panel h3 {
color: #fff;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.sse-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #f44336;
transition: all 0.3s ease;
}
.sse-indicator.active {
background: #4caf50;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
}
.sse-messages {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 8px;
padding: 1rem;
overflow-y: auto;
flex: 1;
font-family: monospace;
font-size: 0.8rem;
}
.sse-message {
padding: 0.5rem;
border-bottom: 1px solid #222;
animation: slideIn 0.3s ease;
}
.sse-message:last-child {
border-bottom: none;
}
.sse-controls {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
}
.sse-button {
flex: 1;
padding: 0.5rem;
border: 1px solid #667eea;
background: transparent;
color: #667eea;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.sse-button:hover:not(:disabled) {
background: #667eea;
color: white;
}
.sse-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.empty-response {
color: #666;
font-style: italic;
padding: 1rem;
}
</style>
</head>
<body>
<div class="header">
<h1>MCP Protocol Demo</h1>
<p>Model Context Protocol - Streamable HTTP Transport</p>
</div>
<div class="container">
<!-- Card 1: Initialize Request -->
<div class="card" id="card-1">
<h2>
<span class="step-number">1</span>
Initialize Request
</h2>
<div class="columns">
<div class="column">
<div class="column-header">
<span class="method-badge">POST</span>
<span>Request</span>
</div>
<div class="code-block">
<pre id="init-request-body">
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "MCP Demo Client",
"version": "1.0.0"
}
}
}</pre
>
</div>
<div class="headers-list">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">Accept:</span>
<span class="header-value"
>application/json, text/event-stream</span
>
</div>
</div>
</div>
<div class="column">
<div class="column-header">
<span>Response</span>
<span
class="status-indicator status-pending"
id="init-status"
style="display: none"
>
<span class="loading"></span>
Sending...
</span>
</div>
<div id="init-response-content">
<div class="empty-response">
No response yet. Click the button to send request.
</div>
</div>
</div>
</div>
<button class="action-button" onclick="sendInitialize()">
Send Initialize Request
</button>
</div>
<!-- Card 2: Initialized Notification -->
<div class="card" id="card-2">
<h2>
<span class="step-number">2</span>
Initialized Notification
</h2>
<div class="columns">
<div class="column">
<div class="column-header">
<span class="method-badge">POST</span>
<span>Request</span>
</div>
<div class="code-block">
<pre id="initialized-request-body">
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}</pre
>
</div>
<div class="headers-list">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">Accept:</span>
<span class="header-value"
>application/json, text/event-stream</span
>
</div>
<div class="header-item">
<span class="header-key">mcp-session-id:</span>
<span
class="header-value session-id-highlight"
id="initialized-session-id"
>Not yet available</span
>
</div>
</div>
</div>
<div class="column">
<div class="column-header">
<span>Response</span>
<span
class="status-indicator status-pending"
id="initialized-status"
style="display: none"
>
<span class="loading"></span>
Sending...
</span>
</div>
<div id="initialized-response-content">
<div class="empty-response">
No response yet. Initialize first, then send notification.
</div>
</div>
</div>
</div>
<button
class="action-button"
onclick="sendInitialized()"
id="initialized-button"
disabled
>
Send Initialized Notification
</button>
</div>
<!-- Card 3: Tool Call -->
<div class="card" id="card-3">
<h2>
<span class="step-number">3</span>
Tool Call - Add Function
</h2>
<div class="columns">
<div class="column">
<div class="column-header">
<span class="method-badge">POST</span>
<span>Request</span>
</div>
<div class="code-block">
<pre id="tool-request-body">
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {
"a": 5,
"b": 3
}
}
}</pre
>
</div>
<div class="headers-list">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">Accept:</span>
<span class="header-value"
>application/json, text/event-stream</span
>
</div>
<div class="header-item">
<span class="header-key">mcp-session-id:</span>
<span
class="header-value session-id-highlight"
id="tool-session-id"
>Not yet available</span
>
</div>
</div>
</div>
<div class="column">
<div class="column-header">
<span>Response</span>
<span
class="status-indicator status-pending"
id="tool-status"
style="display: none"
>
<span class="loading"></span>
Sending...
</span>
</div>
<div id="tool-response-content">
<div class="empty-response">
No response yet. Initialize first, then call tool.
</div>
</div>
</div>
</div>
<button class="action-button" onclick="sendToolCall()" id="tool-button">
Call Add Tool
</button>
</div>
<!-- Card 4: Custom Request -->
<div class="card" id="card-4" style="display: none">
<h2>
<span class="step-number">4</span>
Custom Request Example
</h2>
<div class="info-box">
<h4>Try Your Own Request</h4>
<p>
Experiment with other MCP methods like tools/list, resources/list,
or prompts/list.
</p>
</div>
<div class="columns">
<div class="column">
<div class="column-header">
<span class="method-badge">POST</span>
<span>Request</span>
</div>
<div
class="code-block"
contenteditable="true"
id="custom-request-body"
style="outline: none"
>
{ "jsonrpc": "2.0", "id": 4, "method": "tools/list", "params": {}
}
</div>
<div class="headers-list">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">Accept:</span>
<span class="header-value"
>application/json, text/event-stream</span
>
</div>
<div class="header-item">
<span class="header-key">mcp-session-id:</span>
<span
class="header-value session-id-highlight"
id="custom-session-id"
>Not yet available</span
>
</div>
</div>
</div>
<div class="column">
<div class="column-header">
<span>Response</span>
<span
class="status-indicator status-pending"
id="custom-status"
style="display: none"
>
<span class="loading"></span>
Sending...
</span>
</div>
<div id="custom-response-content">
<div class="empty-response">
No response yet. Initialize first, then send request.
</div>
</div>
</div>
</div>
<button
class="action-button"
onclick="sendCustomRequest()"
id="custom-button"
>
Send Custom Request
</button>
</div>
</div>
<!-- SSE Panel for GET requests -->
<div class="sse-panel">
<h3>
Server-Initiated Events (GET SSE)
<span class="sse-indicator" id="sse-indicator"></span>
</h3>
<div class="sse-messages" id="sse-messages">
<div style="color: #666; text-align: center; padding: 2rem">
No GET SSE connection active. Click "Start GET SSE" to begin
monitoring server-initiated events.
</div>
</div>
<div class="sse-controls">
<button
class="sse-button"
onclick="startSSE()"
id="sse-start-button"
disabled
>
Start GET SSE
</button>
<button
class="sse-button"
onclick="stopSSE()"
id="sse-stop-button"
disabled
>
Stop SSE
</button>
<button class="sse-button" onclick="clearSSE()">Clear</button>
</div>
</div>
<script>
let sessionId = null;
let sseSource = null;
const serverUrl = "http://localhost:8000/mcp/";
// Update session ID in all cards
function updateSessionId(id) {
sessionId = id;
document.getElementById("initialized-session-id").textContent =
id || "Not yet available";
document.getElementById("tool-session-id").textContent =
id || "Not yet available";
document.getElementById("custom-session-id").textContent =
id || "Not yet available";
// Enable buttons after initialization
// if (id) {
document.getElementById("initialized-button").disabled = false;
document.getElementById("tool-button").disabled = false;
document.getElementById("custom-button").disabled = false;
// document.getElementById("sse-start-button").disabled = false;
// }
}
// Add SSE message to panel (for GET SSE)
function addSSEMessage(message) {
const container = document.getElementById("sse-messages");
const messageDiv = document.createElement("div");
messageDiv.className = "sse-message";
messageDiv.textContent = message;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
// Display response with headers and body/stream
function displayResponse(cardId, headers, body, isSSE = false) {
const contentDiv = document.getElementById(
`${cardId}-response-content`
);
// Filter out date-related headers
const filteredHeaders = {};
for (const [key, value] of headers.entries()) {
if (!key.toLowerCase().includes("date")) {
filteredHeaders[key] = value;
}
}
// Build response HTML
let html = '<div class="headers-list">';
for (const [key, value] of Object.entries(filteredHeaders)) {
const isSessionId = key.toLowerCase() === "mcp-session-id";
html += `
<div class="header-item">
<span class="header-key">${key}:</span>
<span class="header-value ${
isSessionId ? "session-id-highlight" : ""
}">${value}</span>
</div>
`;
}
html += "</div>";
if (isSSE) {
html +=
'<div class="sse-stream" id="' + cardId + '-sse-stream"></div>';
} else {
html += '<div class="code-block"><pre>' + body + "</pre></div>";
}
contentDiv.innerHTML = html;
}
// Add SSE event to response stream display
function addSSEEvent(cardId, eventType, data) {
const streamDiv = document.getElementById(`${cardId}-sse-stream`);
if (streamDiv) {
const eventDiv = document.createElement("div");
eventDiv.className = "sse-event";
eventDiv.innerHTML = `<span class="sse-event-type">${eventType}:</span> ${data}`;
streamDiv.appendChild(eventDiv);
streamDiv.scrollTop = streamDiv.scrollHeight;
}
}
// Handle SSE Response from POST requests
async function handleSSEResponse(cardId, response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
addSSEEvent(cardId, "stream", "SSE connection established");
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last incomplete line in the buffer
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.substring(6);
if (data) {
addSSEEvent(cardId, "data", data);
}
} else if (line.startsWith("event: ")) {
const eventType = line.substring(7);
addSSEEvent(cardId, "event", eventType);
} else if (line === "") {
// Empty line signals end of an event
}
}
}
addSSEEvent(cardId, "stream", "SSE connection closed");
} catch (error) {
addSSEEvent(cardId, "error", error.message);
}
}
// Send Initialize Request
async function sendInitialize() {
const statusEl = document.getElementById("init-status");
statusEl.style.display = "inline-flex";
statusEl.className = "status-indicator status-pending";
statusEl.innerHTML = '<span class="loading"></span> Sending...';
try {
const response = await fetch(serverUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {
roots: {
listChanged: true,
},
},
clientInfo: {
name: "MCP Demo Client",
version: "1.0.0",
},
},
}),
});
// Get session ID from response headers
const responseSessionId = response.headers.get("mcp-session-id");
updateSessionId(responseSessionId);
// if (responseSessionId) {
// }
// Handle response based on content type
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/event-stream")) {
displayResponse("init", response.headers, "", true);
handleSSEResponse("init", response);
} else {
const data = await response.json();
displayResponse(
"init",
response.headers,
JSON.stringify(data, null, 2)
);
}
statusEl.className = "status-indicator status-success";
statusEl.innerHTML = "✓ Success";
} catch (error) {
statusEl.className = "status-indicator status-error";
statusEl.innerHTML = "✗ Error";
displayResponse("init", new Headers(), "Error: " + error.message);
}
}
// Send Initialized Notification
async function sendInitialized() {
const statusEl = document.getElementById("initialized-status");
statusEl.style.display = "inline-flex";
statusEl.className = "status-indicator status-pending";
statusEl.innerHTML = '<span class="loading"></span> Sending...';
try {
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
};
if (sessionId) {
headers["mcp-session-id"] = sessionId;
}
const response = await fetch(serverUrl, {
method: "POST",
headers: headers,
body: JSON.stringify({
jsonrpc: "2.0",
method: "notifications/initialized",
params: {},
}),
});
if (response.status === 202) {
displayResponse(
"initialized",
response.headers,
"HTTP 202 Accepted - Notification acknowledged"
);
} else {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/event-stream")) {
displayResponse("initialized", response.headers, "", true);
handleSSEResponse("initialized", response);
} else {
const data = await response.text();
displayResponse(
"initialized",
response.headers,
data || "Empty response"
);
}
}
statusEl.className = "status-indicator status-success";
statusEl.innerHTML = "✓ Success";
} catch (error) {
statusEl.className = "status-indicator status-error";
statusEl.innerHTML = "✗ Error";
displayResponse(
"initialized",
new Headers(),
"Error: " + error.message
);
}
}
// Send Tool Call
async function sendToolCall() {
const statusEl = document.getElementById("tool-status");
statusEl.style.display = "inline-flex";
statusEl.className = "status-indicator status-pending";
statusEl.innerHTML = '<span class="loading"></span> Sending...';
try {
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
};
if (sessionId) {
headers["mcp-session-id"] = sessionId;
}
const response = await fetch(serverUrl, {
method: "POST",
headers: headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: {
_meta: {
progressToken: "abc",
},
name: "add",
arguments: {
a: 5,
b: 3,
},
},
}),
});
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/event-stream")) {
displayResponse("tool", response.headers, "", true);
handleSSEResponse("tool", response);
} else {
const data = await response.json();
displayResponse(
"tool",
response.headers,
JSON.stringify(data, null, 2)
);
}
statusEl.className = "status-indicator status-success";
statusEl.innerHTML = "✓ Success";
} catch (error) {
statusEl.className = "status-indicator status-error";
statusEl.innerHTML = "✗ Error";
displayResponse("tool", new Headers(), "Error: " + error.message);
}
}
// Send Custom Request
async function sendCustomRequest() {
const statusEl = document.getElementById("custom-status");
statusEl.style.display = "inline-flex";
statusEl.className = "status-indicator status-pending";
statusEl.innerHTML = '<span class="loading"></span> Sending...';
try {
const requestBody = document.getElementById(
"custom-request-body"
).textContent;
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
};
if (sessionId) {
headers["mcp-session-id"] = sessionId;
}
const response = await fetch(serverUrl, {
method: "POST",
headers: headers,
body: requestBody,
});
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/event-stream")) {
displayResponse("custom", response.headers, "", true);
handleSSEResponse("custom", response);
} else {
const data = await response.json();
displayResponse(
"custom",
response.headers,
JSON.stringify(data, null, 2)
);
}
statusEl.className = "status-indicator status-success";
statusEl.innerHTML = "✓ Success";
} catch (error) {
statusEl.className = "status-indicator status-error";
statusEl.innerHTML = "✗ Error";
displayResponse("custom", new Headers(), "Error: " + error.message);
}
}
let sseReader = null;
let sseAbortController = null;
// Start GET SSE Connection
async function startSSE() {
if (sseAbortController) {
sseAbortController.abort();
}
document.getElementById("sse-messages").innerHTML = "";
sseAbortController = new AbortController();
const headers = {
Accept: "text/event-stream",
};
if (sessionId) {
headers["mcp-session-id"] = sessionId;
}
document.getElementById("sse-indicator").classList.add("active");
document.getElementById("sse-start-button").disabled = true;
document.getElementById("sse-stop-button").disabled = false;
addSSEMessage("GET SSE connection established");
try {
const response = await fetch(serverUrl, {
method: "GET",
headers: headers,
signal: sseAbortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
sseReader = reader;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last incomplete line in the buffer
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.substring(6);
if (data) {
try {
const parsed = JSON.parse(data);
if (parsed.method) {
if (parsed.method.includes("notification")) {
addSSEMessage("Server Notification: " + data);
} else {
addSSEMessage("Server Request: " + data);
}
} else {
addSSEMessage("Message: " + data);
}
} catch {
addSSEMessage("Message: " + data);
}
}
} else if (line.startsWith("event: ")) {
const eventType = line.substring(7);
addSSEMessage("Event Type: " + eventType);
} else if (line === "") {
// Empty line signals end of an event
}
}
}
addSSEMessage("GET SSE connection closed normally");
} catch (error) {
if (error.name !== "AbortError") {
addSSEMessage("Connection error: " + error.message);
}
} finally {
stopSSE();
}
}
// Stop SSE Connection
function stopSSE() {
if (sseAbortController) {
sseAbortController.abort();
sseAbortController = null;
}
if (sseReader) {
sseReader.cancel();
sseReader = null;
}
document.getElementById("sse-indicator").classList.remove("active");
document.getElementById("sse-start-button").disabled = sessionId
? false
: true;
document.getElementById("sse-stop-button").disabled = true;
addSSEMessage("GET SSE connection closed");
}
</script>
</body>
</html>