Skip to main content
Glama

Readwise MCP Server

by IAmAlexander
video-features.html28.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Readwise MCP - Video Features Demo</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; } h1, h2, h3 { color: #2c3e50; } .container { display: flex; flex-wrap: wrap; gap: 20px; } .panel { flex: 1; min-width: 300px; border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .video-list { max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 10px; border-radius: 4px; } .video-item { padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; } .video-item:hover { background-color: #f5f5f5; } .video-item.active { background-color: #e3f2fd; } .transcript { max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 10px; border-radius: 4px; } .transcript-item { padding: 5px; border-bottom: 1px solid #f5f5f5; display: flex; } .transcript-item:hover { background-color: #f9f9f9; } .timestamp { min-width: 60px; color: #666; cursor: pointer; } .timestamp:hover { color: #1976d2; text-decoration: underline; } .text { flex: 1; } .highlight-list { max-height: 300px; overflow-y: auto; border: 1px solid #eee; padding: 10px; border-radius: 4px; } .highlight-item { padding: 10px; border-bottom: 1px solid #eee; margin-bottom: 5px; background-color: #fffde7; border-radius: 4px; } .highlight-timestamp { font-weight: bold; color: #1976d2; cursor: pointer; } .highlight-text { margin: 5px 0; } .highlight-note { font-style: italic; color: #666; font-size: 0.9em; border-left: 3px solid #ccc; padding-left: 10px; margin-top: 5px; } button { background-color: #1976d2; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; margin: 5px 0; } button:hover { background-color: #1565c0; } input, textarea { width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; } .form-group { margin-bottom: 15px; } .video-player { width: 100%; margin-bottom: 15px; } .progress-bar { height: 20px; background-color: #e0e0e0; border-radius: 10px; margin: 10px 0; overflow: hidden; } .progress-fill { height: 100%; background-color: #4caf50; width: 0%; transition: width 0.3s ease; } .error { color: #d32f2f; background-color: #ffebee; padding: 10px; border-radius: 4px; margin: 10px 0; } .success { color: #388e3c; background-color: #e8f5e9; padding: 10px; border-radius: 4px; margin: 10px 0; } .hidden { display: none; } .search-container { display: flex; align-items: center; margin-bottom: 10px; } </style> </head> <body> <h1>Readwise MCP - Video Features Demo</h1> <div id="auth-panel" class="panel"> <h2>Authentication</h2> <div class="form-group"> <label for="api-token">Readwise API Token:</label> <input type="password" id="api-token" placeholder="Enter your Readwise API token"> </div> <button id="auth-button">Authenticate</button> <div id="auth-status"></div> </div> <div id="main-content" class="hidden"> <div class="container"> <div class="panel"> <h2>Your Videos</h2> <button id="load-videos">Load Videos</button> <div id="video-list" class="video-list"></div> <div id="pagination"> <button id="load-more" class="hidden">Load More</button> </div> </div> <div class="panel"> <h2>Video Details</h2> <div id="video-details"> <p>Select a video to view details</p> </div> <div id="video-player-container" class="hidden"> <h3>Video Player</h3> <div id="video-player"></div> <div class="progress-bar"> <div id="progress-fill" class="progress-fill"></div> </div> <div id="playback-position"> Position: <span id="current-position">0:00</span> / <span id="total-duration">0:00</span> (<span id="progress-percentage">0%</span>) </div> <button id="save-position">Save Position</button> <button id="load-position">Load Saved Position</button> </div> </div> </div> <div class="container"> <div class="panel"> <h2>Transcript</h2> <div class="search-container"> <input type="text" id="transcript-search" class="search-input" placeholder="Search in transcript..."> <div class="search-options"> <label class="search-option"> <input type="checkbox" id="case-sensitive"> Case sensitive </label> <label class="search-option"> <input type="checkbox" id="whole-word"> Whole word </label> </div> <div class="navigation-controls"> <button id="prev-match" class="navigation-button" disabled>Previous</button> <button id="next-match" class="navigation-button" disabled>Next</button> <span id="match-count"></span> </div> </div> <div id="transcript" class="transcript"> <p>Select a video to view transcript</p> </div> </div> <div class="panel"> <h2>Highlights</h2> <button id="load-highlights" class="hidden">Load Highlights</button> <div id="highlight-list" class="highlight-list"> <p>Select a video to view highlights</p> </div> <h3>Create Highlight</h3> <div id="create-highlight-form" class="hidden"> <div class="form-group"> <label for="highlight-text">Highlight Text:</label> <textarea id="highlight-text" rows="3" placeholder="Enter highlight text"></textarea> </div> <div class="form-group"> <label for="highlight-timestamp">Timestamp:</label> <input type="text" id="highlight-timestamp" placeholder="e.g., 1:45"> </div> <div class="form-group"> <label for="highlight-note">Note (optional):</label> <textarea id="highlight-note" rows="2" placeholder="Add a note to your highlight"></textarea> </div> <button id="create-highlight-button">Create Highlight</button> </div> </div> </div> </div> <script> // Configuration const API_BASE_URL = 'http://localhost:3000'; let apiToken = ''; let currentVideo = null; let nextPageCursor = null; let player = null; let currentTranscript = null; let currentSearchIndex = -1; let searchMatches = []; // DOM Elements const authPanel = document.getElementById('auth-panel'); const mainContent = document.getElementById('main-content'); const apiTokenInput = document.getElementById('api-token'); const authButton = document.getElementById('auth-button'); const authStatus = document.getElementById('auth-status'); const loadVideosButton = document.getElementById('load-videos'); const videoList = document.getElementById('video-list'); const loadMoreButton = document.getElementById('load-more'); const videoDetails = document.getElementById('video-details'); const videoPlayerContainer = document.getElementById('video-player-container'); const videoPlayer = document.getElementById('video-player'); const progressFill = document.getElementById('progress-fill'); const currentPositionEl = document.getElementById('current-position'); const totalDurationEl = document.getElementById('total-duration'); const progressPercentageEl = document.getElementById('progress-percentage'); const savePositionButton = document.getElementById('save-position'); const loadPositionButton = document.getElementById('load-position'); const transcript = document.getElementById('transcript'); const loadHighlightsButton = document.getElementById('load-highlights'); const highlightList = document.getElementById('highlight-list'); const createHighlightForm = document.getElementById('create-highlight-form'); const highlightText = document.getElementById('highlight-text'); const highlightTimestamp = document.getElementById('highlight-timestamp'); const highlightNote = document.getElementById('highlight-note'); const createHighlightButton = document.getElementById('create-highlight-button'); const transcriptSearchInput = document.getElementById('transcript-search'); const caseSensitiveCheckbox = document.getElementById('case-sensitive'); const wholeWordCheckbox = document.getElementById('whole-word'); const prevMatchButton = document.getElementById('prev-match'); const nextMatchButton = document.getElementById('next-match'); const matchCount = document.getElementById('match-count'); // Helper Functions function showError(element, message) { const errorDiv = document.createElement('div'); errorDiv.className = 'error'; errorDiv.textContent = message; element.innerHTML = ''; element.appendChild(errorDiv); } function showSuccess(element, message) { const successDiv = document.createElement('div'); successDiv.className = 'success'; successDiv.textContent = message; element.innerHTML = ''; element.appendChild(successDiv); } function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } function parseTimestamp(timestamp) { if (!timestamp) return 0; // Handle format like "1:45" const parts = timestamp.split(':'); if (parts.length === 2) { return parseInt(parts[0]) * 60 + parseInt(parts[1]); } // Handle format like "1m45s" const minutesMatch = timestamp.match(/(\d+)m/); const secondsMatch = timestamp.match(/(\d+)s/); let seconds = 0; if (minutesMatch) seconds += parseInt(minutesMatch[1]) * 60; if (secondsMatch) seconds += parseInt(secondsMatch[1]); return seconds; } // API Functions async function makeApiRequest(endpoint, method = 'GET', data = null) { try { const options = { method, headers: { 'Authorization': `Token ${apiToken}`, 'Content-Type': 'application/json' } }; if (data) { options.body = JSON.stringify(data); } const response = await fetch(`${API_BASE_URL}${endpoint}`, options); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'API request failed'); } return await response.json(); } catch (error) { console.error('API Error:', error); throw error; } } async function checkAuth() { try { const response = await makeApiRequest('/status'); showSuccess(authStatus, 'Authentication successful!'); authPanel.classList.add('hidden'); mainContent.classList.remove('hidden'); return true; } catch (error) { showError(authStatus, 'Authentication failed: ' + error.message); return false; } } async function loadVideos() { try { videoList.innerHTML = '<p>Loading videos...</p>'; const endpoint = nextPageCursor ? `/videos?limit=10&pageCursor=${nextPageCursor}` : '/videos?limit=10'; const data = await makeApiRequest(endpoint); if (data.results.length === 0) { videoList.innerHTML = '<p>No videos found in your Readwise library.</p>'; return; } if (nextPageCursor === null) { videoList.innerHTML = ''; } data.results.forEach(video => { const videoItem = document.createElement('div'); videoItem.className = 'video-item'; videoItem.dataset.id = video.id; videoItem.innerHTML = ` <strong>${video.title}</strong> <div>Source: ${video.url}</div> <div>Added: ${new Date(video.created_at).toLocaleDateString()}</div> `; videoItem.addEventListener('click', () => loadVideoDetails(video.id)); videoList.appendChild(videoItem); }); nextPageCursor = data.nextPageCursor; if (nextPageCursor) { loadMoreButton.classList.remove('hidden'); } else { loadMoreButton.classList.add('hidden'); } } catch (error) { showError(videoList, 'Failed to load videos: ' + error.message); } } async function loadVideoDetails(videoId) { try { // Highlight selected video document.querySelectorAll('.video-item').forEach(item => { item.classList.remove('active'); }); document.querySelector(`.video-item[data-id="${videoId}"]`)?.classList.add('active'); videoDetails.innerHTML = '<p>Loading video details...</p>'; transcript.innerHTML = '<p>Loading transcript...</p>'; highlightList.innerHTML = '<p>Loading highlights...</p>'; const data = await makeApiRequest(`/video/${videoId}`); currentVideo = data; // Display video details videoDetails.innerHTML = ` <h3>${data.title}</h3> <p><strong>Author:</strong> ${data.author || 'Unknown'}</p> <p><strong>Source:</strong> <a href="${data.url}" target="_blank">${data.url}</a></p> <p><strong>Added:</strong> ${new Date(data.created_at).toLocaleDateString()}</p> <p><strong>Tags:</strong> ${data.tags?.join(', ') || 'None'}</p> `; // Setup YouTube player if it's a YouTube video if (data.url && (data.url.includes('youtube.com') || data.url.includes('youtu.be'))) { setupYouTubePlayer(data.url); } else { videoPlayerContainer.classList.add('hidden'); } // Display transcript loadTranscript(videoId); // Load highlights loadHighlights(videoId); // Show highlight form createHighlightForm.classList.remove('hidden'); loadHighlightsButton.classList.remove('hidden'); } catch (error) { showError(videoDetails, 'Failed to load video details: ' + error.message); transcript.innerHTML = '<p>Failed to load transcript.</p>'; highlightList.innerHTML = '<p>Failed to load highlights.</p>'; } } async function loadTranscript(videoId) { const transcriptElement = document.getElementById('transcript'); transcriptElement.innerHTML = '<p>Loading transcript...</p>'; try { const response = await fetch(`${API_BASE_URL}/videos/${videoId}/transcript`, { headers: { 'Authorization': `Token ${apiToken}` } }); if (!response.ok) { throw new Error('Failed to load transcript'); } const transcript = await response.json(); currentTranscript = transcript; renderTranscript(transcript); } catch (error) { transcriptElement.innerHTML = `<p class="error">Error loading transcript: ${error.message}</p>`; } } function renderTranscript(transcript) { const transcriptElement = document.getElementById('transcript'); transcriptElement.innerHTML = transcript.map(segment => ` <div class="transcript-segment"> <div class="timestamp">${formatTimestamp(segment.start)}</div> <div class="content"> ${segment.speaker ? `<div class="speaker">${segment.speaker}</div>` : ''} <div class="text">${segment.text}</div> ${segment.confidence ? `<div class="confidence">Confidence: ${(segment.confidence * 100).toFixed(1)}%</div>` : ''} </div> </div> `).join(''); } async function loadHighlights(videoId) { try { const data = await makeApiRequest(`/video/${videoId}/highlights`); if (!data.results || data.results.length === 0) { highlightList.innerHTML = '<p>No highlights found for this video.</p>'; return; } highlightList.innerHTML = ''; data.results.forEach(highlight => { const highlightItem = document.createElement('div'); highlightItem.className = 'highlight-item'; let content = ''; if (highlight.timestamp) { content += `<div class="highlight-timestamp" data-timestamp="${highlight.timestamp}">${highlight.timestamp}</div>`; } content += `<div class="highlight-text">${highlight.text}</div>`; if (highlight.note) { content += `<div class="highlight-note">${highlight.note}</div>`; } highlightItem.innerHTML = content; // Add click event to timestamp highlightList.appendChild(highlightItem); }); // Add click events to timestamps document.querySelectorAll('.highlight-timestamp').forEach(el => { el.addEventListener('click', () => { if (player) { const seconds = parseTimestamp(el.dataset.timestamp); player.seekTo(seconds); player.playVideo(); } }); }); } catch (error) { showError(highlightList, 'Failed to load highlights: ' + error.message); } } async function createHighlight() { try { if (!currentVideo) { showError(createHighlightForm, 'No video selected.'); return; } const text = highlightText.value.trim(); const timestamp = highlightTimestamp.value.trim(); const note = highlightNote.value.trim(); if (!text) { showError(createHighlightForm, 'Highlight text is required.'); return; } if (!timestamp) { showError(createHighlightForm, 'Timestamp is required.'); return; } const data = { text, timestamp, note: note || undefined }; await makeApiRequest(`/video/${currentVideo.id}/highlight`, 'POST', data); showSuccess(createHighlightForm, 'Highlight created successfully!'); // Clear form highlightText.value = ''; highlightTimestamp.value = ''; highlightNote.value = ''; // Reload highlights loadHighlights(currentVideo.id); } catch (error) { showError(createHighlightForm, 'Failed to create highlight: ' + error.message); } } async function savePosition() { try { if (!currentVideo || !player) { showError(videoPlayerContainer, 'No video playing.'); return; } const position = player.getCurrentTime(); const duration = player.getDuration(); if (isNaN(position) || isNaN(duration)) { showError(videoPlayerContainer, 'Invalid position or duration.'); return; } const data = { position, duration }; await makeApiRequest(`/video/${currentVideo.id}/position`, 'POST', data); showSuccess(videoPlayerContainer, 'Position saved successfully!'); } catch (error) { showError(videoPlayerContainer, 'Failed to save position: ' + error.message); } } async function loadPosition() { try { if (!currentVideo || !player) { showError(videoPlayerContainer, 'No video playing.'); return; } const data = await makeApiRequest(`/video/${currentVideo.id}/position`); if (data.position) { player.seekTo(data.position); showSuccess(videoPlayerContainer, `Resumed from ${formatTime(data.position)} (${data.percentage}%)`); } else { showSuccess(videoPlayerContainer, 'No saved position found.'); } } catch (error) { showError(videoPlayerContainer, 'Failed to load position: ' + error.message); } } function setupYouTubePlayer(url) { // Extract video ID from URL let videoId; if (url.includes('youtube.com')) { const urlParams = new URLSearchParams(new URL(url).search); videoId = urlParams.get('v'); } else if (url.includes('youtu.be')) { videoId = url.split('/').pop(); } if (!videoId) { videoPlayerContainer.classList.add('hidden'); return; } videoPlayerContainer.classList.remove('hidden'); // Load YouTube API if not already loaded if (!window.YT) { const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onYouTubeIframeAPIReady = () => { createYouTubePlayer(videoId); }; } else { createYouTubePlayer(videoId); } } function createYouTubePlayer(videoId) { // Clear previous player videoPlayer.innerHTML = ''; // Create player div const playerDiv = document.createElement('div'); playerDiv.id = 'youtube-player'; videoPlayer.appendChild(playerDiv); // Create player player = new YT.Player('youtube-player', { height: '360', width: '640', videoId: videoId, events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } }); } function onPlayerReady(event) { // Player is ready const duration = player.getDuration(); totalDurationEl.textContent = formatTime(duration); // Update current time every second setInterval(() => { if (player && player.getCurrentTime) { const currentTime = player.getCurrentTime(); const duration = player.getDuration(); const percentage = Math.round((currentTime / duration) * 100); currentPositionEl.textContent = formatTime(currentTime); progressPercentageEl.textContent = `${percentage}%`; progressFill.style.width = `${percentage}%`; // Update timestamp input if empty if (!highlightTimestamp.value) { highlightTimestamp.value = formatTime(currentTime); } } }, 1000); } function onPlayerStateChange(event) { // Handle player state changes } // Event Listeners authButton.addEventListener('click', () => { apiToken = apiTokenInput.value.trim(); if (!apiToken) { showError(authStatus, 'API token is required'); return; } checkAuth(); }); loadVideosButton.addEventListener('click', () => { nextPageCursor = null; loadVideos(); }); loadMoreButton.addEventListener('click', loadVideos); loadHighlightsButton.addEventListener('click', () => { if (currentVideo) { loadHighlights(currentVideo.id); } }); createHighlightButton.addEventListener('click', createHighlight); savePositionButton.addEventListener('click', savePosition); loadPositionButton.addEventListener('click', loadPosition); transcriptSearchInput.addEventListener('input', performSearch); caseSensitiveCheckbox.addEventListener('change', performSearch); wholeWordCheckbox.addEventListener('change', performSearch); // Check for stored token const storedToken = localStorage.getItem('readwise_api_token'); if (storedToken) { apiTokenInput.value = storedToken; apiToken = storedToken; checkAuth(); } function performSearch() { if (!currentTranscript) return; const query = transcriptSearchInput.value.trim(); if (!query) { clearSearch(); return; } const options = { caseSensitive: caseSensitiveCheckbox.checked, wholeWord: wholeWordCheckbox.checked }; searchMatches = []; currentTranscript.forEach((segment, index) => { const text = segment.text; const matches = findMatches(text, query, options); if (matches.length > 0) { searchMatches.push({ segmentIndex: index, matches }); } }); updateSearchUI(); } function findMatches(text, query, options) { const regex = new RegExp( options.wholeWord ? `\\b${escapeRegExp(query)}\\b` : escapeRegExp(query), options.caseSensitive ? 'g' : 'gi' ); const matches = []; let match; while ((match = regex.exec(text)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length }); } return matches; } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function updateSearchUI() { if (searchMatches.length === 0) { clearSearch(); return; } currentSearchIndex = 0; updateMatchCount(); updateNavigationButtons(); highlightCurrentMatch(); } function clearSearch() { searchMatches = []; currentSearchIndex = -1; matchCount.textContent = ''; prevMatchButton.disabled = true; nextMatchButton.disabled = true; clearHighlights(); } function updateMatchCount() { matchCount.textContent = `${currentSearchIndex + 1} of ${searchMatches.length} matches`; } function updateNavigationButtons() { prevMatchButton.disabled = currentSearchIndex <= 0; nextMatchButton.disabled = currentSearchIndex >= searchMatches.length - 1; } function highlightCurrentMatch() { clearHighlights(); if (currentSearchIndex >= 0 && currentSearchIndex < searchMatches.length) { const match = searchMatches[currentSearchIndex]; const segment = currentTranscript[match.segmentIndex]; const text = segment.text; const matchObj = match.matches[0]; const highlightedText = text.substring(0, matchObj.start) + `<span class="highlight">${text.substring(matchObj.start, matchObj.end)}</span>` + text.substring(matchObj.end); const segmentElement = transcript.children[match.segmentIndex]; segmentElement.querySelector('.text').innerHTML = highlightedText; segmentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } function clearHighlights() { if (!currentTranscript) return; const segments = Array.from(transcript.children); segments.forEach((segment, index) => { const text = segment.querySelector('.text'); if (text) { text.innerHTML = segment.textContent; } }); } prevMatchButton.addEventListener('click', () => { if (currentSearchIndex > 0) { currentSearchIndex--; updateMatchCount(); updateNavigationButtons(); highlightCurrentMatch(); } }); nextMatchButton.addEventListener('click', () => { if (currentSearchIndex < searchMatches.length - 1) { currentSearchIndex++; updateMatchCount(); updateNavigationButtons(); highlightCurrentMatch(); } }); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IAmAlexander/readwise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server