<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streamable HTTP MCP Test Client</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.connected {
background-color: #d4edda;
color: #155724;
}
.disconnected {
background-color: #f8d7da;
color: #721c24;
}
.connecting {
background-color: #fff3cd;
color: #856404;
}
.session-info {
background-color: #e9ecef;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-bottom: 1px solid #e9ecef;
}
.error {
color: #dc3545;
}
.success {
color: #28a745;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>Streamable HTTP MCP Test Client</h1>
<div id="status" class="status disconnected">
Status: <span id="statusText">Disconnected</span>
</div>
<div id="sessionInfo" class="session-info" style="display: none;">
Session ID: <span id="sessionId">None</span>
</div>
<div>
<button id="connectBtn">Initialize Session</button>
<button id="streamBtn" disabled>Start Stream</button>
<button id="testToolsBtn" disabled>Test Tools</button>
<button id="disconnectBtn" disabled>Disconnect</button>
<button id="clearBtn">Clear Log</button>
</div>
<h2>Activity Log</h2>
<div id="log" class="log"></div>
</div>
<script type="module">
let sessionId = null;
let eventSource = null;
const statusEl = document.getElementById('status');
const statusTextEl = document.getElementById('statusText');
const sessionInfoEl = document.getElementById('sessionInfo');
const sessionIdEl = document.getElementById('sessionId');
const logEl = document.getElementById('log');
const connectBtn = document.getElementById('connectBtn');
const streamBtn = document.getElementById('streamBtn');
const testToolsBtn = document.getElementById('testToolsBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const clearBtn = document.getElementById('clearBtn');
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
}
function setStatus(status, text) {
statusEl.className = `status ${status}`;
statusTextEl.textContent = text;
}
function updateSessionInfo(newSessionId) {
sessionId = newSessionId;
if (sessionId) {
sessionIdEl.textContent = sessionId;
sessionInfoEl.style.display = 'block';
} else {
sessionInfoEl.style.display = 'none';
}
}
async function sendMessage(message, includeSession = true) {
try {
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
};
if (includeSession && sessionId) {
headers['mcp-session-id'] = sessionId;
}
const response = await fetch('http://127.0.0.1:3000/mcp', {
method: 'POST',
headers: headers,
body: JSON.stringify(message)
});
// Check if we got a new session ID
const newSessionId = response.headers.get('mcp-session-id');
if (newSessionId && newSessionId !== sessionId) {
updateSessionInfo(newSessionId);
log(`New session ID: ${newSessionId}`, 'success');
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
// Check content type to determine how to handle the response
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/event-stream')) {
// Handle SSE response
log('Received SSE stream response', 'info');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data) {
try {
const parsed = JSON.parse(data);
log(`SSE data: ${JSON.stringify(parsed, null, 2)}`, 'success');
return parsed; // Return first JSON-RPC response
} catch (e) {
log(`SSE data: ${data}`);
}
}
}
}
}
} else {
// Handle regular JSON response
const result = await response.json();
log(`Response: ${JSON.stringify(result, null, 2)}`, 'success');
return result;
}
} catch (error) {
log(`Error sending message: ${error.message}`, 'error');
return null;
}
}
async function initialize() {
log('Sending initialize request...');
const message = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'browser-test-client',
version: '1.0.0'
}
}
};
const result = await sendMessage(message, false); // Don't include session for initialization
if (result) {
connectBtn.disabled = true;
streamBtn.disabled = false;
testToolsBtn.disabled = false;
disconnectBtn.disabled = false;
setStatus('connected', 'Connected');
}
}
async function startStream() {
if (!sessionId) {
log('No session ID available', 'error');
return;
}
log('Starting SSE stream...');
// EventSource doesn't support custom headers, so we use query parameter
eventSource = new EventSource(`http://127.0.0.1:3000/mcp?sessionId=${sessionId}`);
eventSource.onopen = (event) => {
log('SSE stream opened', 'success');
streamBtn.textContent = 'Stop Stream';
};
eventSource.onmessage = (event) => {
log(`Stream message: ${event.data}`);
try {
const data = JSON.parse(event.data);
log(`Parsed: ${JSON.stringify(data, null, 2)}`);
} catch (e) {
// Not JSON, just log as is
}
};
eventSource.onerror = (event) => {
log('SSE stream error', 'error');
if (eventSource.readyState === EventSource.CLOSED) {
log('SSE stream closed');
streamBtn.textContent = 'Start Stream';
}
};
}
async function listTools() {
log('Requesting tool list...');
const message = {
jsonrpc: '2.0',
id: 2,
method: 'tools/list',
params: {}
};
await sendMessage(message);
}
async function testSequentialThinking() {
log('Testing sequential thinking tool...');
// First thought
await sendMessage({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'sequentialthinking',
arguments: {
thought: 'Testing Streamable HTTP transport from browser',
nextThoughtNeeded: true,
thoughtNumber: 1,
totalThoughts: 3
}
}
});
// Second thought
await sendMessage({
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: {
name: 'sequentialthinking',
arguments: {
thought: 'The Streamable HTTP connection is working well',
nextThoughtNeeded: true,
thoughtNumber: 2,
totalThoughts: 3
}
}
});
// Final thought
await sendMessage({
jsonrpc: '2.0',
id: 5,
method: 'tools/call',
params: {
name: 'sequentialthinking',
arguments: {
thought: 'Successfully tested the new transport protocol',
nextThoughtNeeded: false,
thoughtNumber: 3,
totalThoughts: 3
}
}
});
}
async function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
streamBtn.textContent = 'Start Stream';
}
if (sessionId) {
log('Sending disconnect request...');
try {
const response = await fetch('http://127.0.0.1:3000/mcp', {
method: 'DELETE',
headers: {
'mcp-session-id': sessionId
}
});
if (response.ok) {
log('Disconnected successfully', 'success');
}
} catch (error) {
log(`Error disconnecting: ${error.message}`, 'error');
}
}
updateSessionInfo(null);
connectBtn.disabled = false;
streamBtn.disabled = true;
testToolsBtn.disabled = true;
disconnectBtn.disabled = true;
setStatus('disconnected', 'Disconnected');
}
connectBtn.addEventListener('click', initialize);
streamBtn.addEventListener('click', () => {
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
eventSource.close();
eventSource = null;
streamBtn.textContent = 'Start Stream';
log('SSE stream stopped');
} else {
startStream();
}
});
testToolsBtn.addEventListener('click', async () => {
testToolsBtn.disabled = true;
await listTools();
await new Promise(resolve => setTimeout(resolve, 1000));
await testSequentialThinking();
testToolsBtn.disabled = false;
});
disconnectBtn.addEventListener('click', disconnect);
clearBtn.addEventListener('click', () => {
logEl.innerHTML = '';
log('Log cleared');
});
// Initial log
log('Streamable HTTP MCP Test Client ready');
log('This client uses the new Streamable HTTP transport protocol');
</script>
</body>
</html>