YouTube MCP Integration
by spolepaka
Verified
- ephor-youtube-mcp
- public
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YouTube MCP Client</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #c4302b;
}
.tool-section {
margin-bottom: 30px;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
}
.tool-form {
margin-bottom: 15px;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px;
margin: 5px 0 15px;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #c4302b;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #a52521;
}
.results {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-top: 15px;
white-space: pre-wrap;
overflow-x: auto;
}
.status {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.status.connecting {
color: #856404;
background-color: #fff3cd;
border: 1px solid #ffeeba;
}
.status.connected {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
}
.status.error {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
.retry-button {
background-color: #007bff;
color: white;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
.retry-button:hover {
background-color: #0069d9;
}
.video-result {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.video-thumbnail {
flex: 0 0 160px;
margin-right: 15px;
}
.video-thumbnail img {
width: 160px;
height: 90px;
object-fit: cover;
}
.video-info {
flex: 1;
}
.video-title {
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
}
.video-channel {
color: #606060;
font-size: 14px;
margin-bottom: 5px;
}
.video-meta {
color: #606060;
font-size: 13px;
margin-bottom: 8px;
}
.video-description {
font-size: 14px;
color: #606060;
}
.transcript-line {
margin-bottom: 8px;
}
.transcript-time {
color: #606060;
font-size: 12px;
margin-right: 10px;
display: inline-block;
width: 60px;
}
.server-info {
margin-top: 5px;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<h1>YouTube MCP Client</h1>
<div id="connection-status" class="status connecting">Not connected to MCP server</div>
<div id="server-info" class="server-info"></div>
<div class="tool-section">
<h2>YouTube Search</h2>
<div class="tool-form">
<label for="search-query">Search Query:</label>
<input type="text" id="search-query" placeholder="Enter search query" value="javascript tutorial">
<label for="search-limit">Result Limit (1-10):</label>
<input type="number" id="search-limit" min="1" max="10" value="3">
<button id="search-button">Search</button>
</div>
<div id="search-results" class="results"></div>
</div>
<div class="tool-section">
<h2>Video Info</h2>
<div class="tool-form">
<label for="video-url">Video URL or ID:</label>
<input type="text" id="video-url" placeholder="Enter YouTube URL or video ID" value="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
<button id="info-button">Get Info</button>
</div>
<div id="info-results" class="results"></div>
</div>
<div class="tool-section">
<h2>Video Transcript</h2>
<div class="tool-form">
<label for="transcript-url">Video URL or ID:</label>
<input type="text" id="transcript-url" placeholder="Enter YouTube URL or video ID" value="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
<button id="transcript-button">Get Transcript</button>
</div>
<div id="transcript-results" class="results"></div>
</div>
<div class="tool-section">
<h2>API Test</h2>
<div class="tool-form">
<button id="test-button">Test API (Main Server)</button>
<button id="session-button">Test Session (Main Server)</button>
<hr>
<button id="test-server-button">Test API (Test Server)</button>
<button id="session-server-button">Test Session (Test Server)</button>
</div>
<div id="test-results" class="results"></div>
</div>
<script>
let sessionId = null;
let eventSource = null;
// Update the connection status with visual indicator
function updateConnectionStatus(message, type = 'connecting') {
const status = document.getElementById('connection-status');
status.textContent = message;
status.className = `status ${type}`;
// Add retry button if there's an error
if (type === 'error' && !document.getElementById('retry-button')) {
const retryButton = document.createElement('button');
retryButton.id = 'retry-button';
retryButton.className = 'retry-button';
retryButton.textContent = 'Retry Connection';
retryButton.onclick = initializeConnection;
status.appendChild(retryButton);
} else if (type !== 'error') {
// Remove retry button if not an error
const retryButton = document.getElementById('retry-button');
if (retryButton) {
retryButton.remove();
}
}
}
// Get a session ID and then connect to SSE
async function initializeConnection() {
updateConnectionStatus('Connecting to SSE endpoint...', 'connecting');
try {
// No need to get a session ID first now, we can directly connect to SSE
connectToSSE();
} catch (error) {
updateConnectionStatus(`Error: ${error.message}`, 'error');
console.error('Connection error:', error);
}
}
// Connect to SSE endpoint with session ID
function connectToSSE() {
try {
// Close any existing connection
if (eventSource) {
eventSource.close();
}
updateConnectionStatus('Connecting to server...', 'connecting');
// Connect without requiring a session ID
eventSource = new EventSource(`http://localhost:3000/sse`);
eventSource.onopen = () => {
updateConnectionStatus('Connected to MCP server', 'connected');
console.log('SSE connection established');
};
eventSource.onmessage = (event) => {
console.log('Received message:', event.data);
};
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
updateConnectionStatus('Error connecting to MCP server. Click retry to reconnect.', 'error');
eventSource.close();
};
} catch (error) {
console.error('Error creating EventSource:', error);
updateConnectionStatus(`Error creating SSE connection: ${error.message}`, 'error');
}
}
// Helper function to send tool calls
async function callTool(name, args) {
// Check if SSE connection is active
if (!eventSource || eventSource.readyState !== 1) {
// If not active, try to reconnect
updateConnectionStatus('Connection lost or not established. Reconnecting...', 'connecting');
connectToSSE();
// Wait a moment for the connection to establish
await new Promise(resolve => setTimeout(resolve, 1000));
// If still not connected, throw error
if (!eventSource || eventSource.readyState !== 1) {
throw new Error('Could not establish SSE connection. Please try again later.');
}
}
// No need for session ID in the URL now
const response = await fetch(`http://localhost:3000/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call_tool',
params: {
name,
arguments: args
},
id: Date.now()
})
});
const result = await response.json();
if (!response.ok) {
// If we get a 404 for the session, try to reconnect
if (response.status === 404) {
updateConnectionStatus('Session expired. Reconnecting...', 'connecting');
await initializeConnection();
return callTool(name, args); // Retry the call
}
const errorMessage = result?.error?.message || `HTTP error: ${response.status}`;
throw new Error(errorMessage);
}
if (result.error) {
throw new Error(result.error.message || 'Unknown error');
}
return result.result;
}
// YouTube Search
document.getElementById('search-button').addEventListener('click', async () => {
const query = document.getElementById('search-query').value;
const limit = parseInt(document.getElementById('search-limit').value) || 3;
const resultsDiv = document.getElementById('search-results');
if (!query) {
resultsDiv.textContent = 'Please enter a search query';
return;
}
try {
resultsDiv.textContent = 'Searching...';
const result = await callTool('youtube_search', { query, limit });
if (!result.content || !result.content[0] || !result.content[0].text) {
throw new Error('Invalid response format');
}
const videos = JSON.parse(result.content[0].text);
resultsDiv.innerHTML = '';
if (!Array.isArray(videos) || videos.length === 0) {
resultsDiv.textContent = 'No videos found';
return;
}
videos.forEach(video => {
const videoEl = document.createElement('div');
videoEl.className = 'video-result';
videoEl.innerHTML = `
<div class="video-thumbnail">
<a href="${video.url}" target="_blank">
<img src="${video.thumbnailUrl}" alt="${video.title}">
</a>
</div>
<div class="video-info">
<div class="video-title"><a href="${video.url}" target="_blank">${video.title}</a></div>
<div class="video-channel">
<a href="${video.channel.url}" target="_blank">${video.channel.name}</a>
</div>
<div class="video-meta">${video.viewCount || ''} ${video.publishedTime || ''}</div>
<div class="video-description">${video.description || ''}</div>
</div>
`;
resultsDiv.appendChild(videoEl);
});
} catch (error) {
resultsDiv.textContent = `Error: ${error.message}`;
}
});
// Video Info
document.getElementById('info-button').addEventListener('click', async () => {
const input = document.getElementById('video-url').value;
const resultsDiv = document.getElementById('info-results');
if (!input) {
resultsDiv.textContent = 'Please enter a video URL or ID';
return;
}
try {
resultsDiv.textContent = 'Fetching video info...';
const result = await callTool('youtube_get_video_info', { input });
if (!result.content || !result.content[0] || !result.content[0].text) {
throw new Error('Invalid response format');
}
const videoInfo = JSON.parse(result.content[0].text);
if (videoInfo.error) {
throw new Error(videoInfo.error);
}
resultsDiv.innerHTML = `
<div class="video-result">
<div class="video-thumbnail">
<a href="${videoInfo.url}" target="_blank">
<img src="${videoInfo.thumbnailUrl}" alt="${videoInfo.title}">
</a>
</div>
<div class="video-info">
<div class="video-title"><a href="${videoInfo.url}" target="_blank">${videoInfo.title}</a></div>
<div class="video-channel">
<a href="${videoInfo.channel.url}" target="_blank">${videoInfo.channel.name}</a>
</div>
<div class="video-meta">${videoInfo.viewCount || ''} ${videoInfo.publishDate || ''}</div>
<div class="video-description">${videoInfo.description || ''}</div>
</div>
</div>
`;
} catch (error) {
resultsDiv.textContent = `Error: ${error.message}`;
}
});
// Video Transcript
document.getElementById('transcript-button').addEventListener('click', async () => {
const input = document.getElementById('transcript-url').value;
const resultsDiv = document.getElementById('transcript-results');
if (!input) {
resultsDiv.textContent = 'Please enter a video URL or ID';
return;
}
try {
resultsDiv.textContent = 'Fetching transcript...';
const result = await callTool('youtube_get_transcript', { input });
if (!result.content || !result.content[0] || !result.content[0].text) {
throw new Error('Invalid response format');
}
const data = JSON.parse(result.content[0].text);
if (data.error) {
throw new Error(data.error);
}
resultsDiv.innerHTML = `
<h3>${data.videoInfo.title}</h3>
<p>Channel: ${data.videoInfo.channel.name}</p>
<p>Duration: ${formatDuration(data.videoInfo.duration)}</p>
<hr>
<div class="transcript-container">
${data.transcript.map(line => `
<div class="transcript-line">
<span class="transcript-time">${formatTime(line.time)}</span>
<span class="transcript-text">${line.text}</span>
</div>
`).join('')}
</div>
`;
} catch (error) {
resultsDiv.textContent = `Error: ${error.message}`;
}
});
// Helper to format duration
function formatDuration(seconds) {
if (!seconds) return '';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Helper to format time
function formatTime(seconds) {
if (!seconds) return '';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Check server status
async function checkServerStatus() {
try {
const response = await fetch('http://localhost:3000/status');
if (!response.ok) {
throw new Error(`Server status error: ${response.status}`);
}
const status = await response.json();
const serverInfo = document.getElementById('server-info');
serverInfo.innerHTML = `
<strong>Server:</strong> ${status.status} |
<strong>Active connections:</strong> ${status.activeConnections} |
<strong>Available tools:</strong> ${status.tools.map(t => t.name).join(', ')}
`;
return true;
} catch (error) {
console.error('Status check error:', error);
const serverInfo = document.getElementById('server-info');
serverInfo.innerHTML = `<span style="color: #721c24;">Server status check failed. ${error.message}</span>`;
return false;
}
}
// Check status initially and periodically
checkServerStatus();
setInterval(checkServerStatus, 10000); // Check every 10 seconds
// Add test button handlers
document.getElementById('test-button').addEventListener('click', async () => {
const resultsDiv = document.getElementById('test-results');
resultsDiv.textContent = 'Testing main server API...';
try {
const response = await fetch('http://localhost:3000/test');
const data = await response.json();
resultsDiv.textContent = `Main server test result: ${JSON.stringify(data)}`;
} catch (error) {
resultsDiv.textContent = `Error with main server: ${error.message}`;
}
});
document.getElementById('session-button').addEventListener('click', async () => {
const resultsDiv = document.getElementById('test-results');
resultsDiv.textContent = 'Testing main server session endpoint...';
try {
const response = await fetch('http://localhost:3000/session');
const data = await response.json();
resultsDiv.textContent = `Main server session result: ${JSON.stringify(data)}`;
} catch (error) {
resultsDiv.textContent = `Error with main server: ${error.message}`;
}
});
document.getElementById('test-server-button').addEventListener('click', async () => {
const resultsDiv = document.getElementById('test-results');
resultsDiv.textContent = 'Testing test server API...';
try {
const response = await fetch('http://localhost:3001/test');
const data = await response.json();
resultsDiv.textContent = `Test server result: ${JSON.stringify(data)}`;
} catch (error) {
resultsDiv.textContent = `Error with test server: ${error.message}`;
}
});
document.getElementById('session-server-button').addEventListener('click', async () => {
const resultsDiv = document.getElementById('test-results');
resultsDiv.textContent = 'Testing test server session endpoint...';
try {
const response = await fetch('http://localhost:3001/session');
const data = await response.json();
resultsDiv.textContent = `Test server session result: ${JSON.stringify(data)}`;
} catch (error) {
resultsDiv.textContent = `Error with test server: ${error.message}`;
}
});
// Initialize connection
initializeConnection();
</script>
</body>
</html>