Skip to main content
Glama

Remote MCP Server (Authless)

index.ts•28.2 kB
import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { env } from "cloudflare:workers"; import type { D1Database, Ai } from "@cloudflare/workers-types"; export interface Env { AI: Ai; DB: D1Database; PODCAST_BUCKET: R2Bucket; // R2 bucket for audio storage } function getEnv<Env>() { return env as Env; } /* extends McpAgent to create our podcast server with MeloTTS audio-only experience. */ export class RedirectMCP extends McpAgent { server = new McpServer({ name: "AI Podcast Generator", version: "2.0.0", }); async init() { // PRIMARY TOOL: Generate audio podcast (MeloTTS only) this.server.tool( "generate_audio_podcast", { topic: z.string().describe("A topic to generate an AI audio podcast overview about"), duration: z.enum(["short", "medium", "long"]).optional().default("medium").describe("Podcast duration: short (45s), medium (1min), long (75s)"), }, async ({ topic, duration }) => { const env = getEnv<Env>(); const baseUrl = "https://podcaster.lizziepika.workers.dev"; try { // Step 1: Generate 1-minute overview script optimized for MeloTTS const getDurationConfig = (duration: string) => { switch (duration) { case "short": return { targetWords: 120, targetChars: 720, description: "45-second overview", promptWords: "120-140 words", maxTokens: 800 }; case "long": return { targetWords: 180, targetChars: 1080, description: "75-second overview", promptWords: "180-200 words", maxTokens: 1200 }; default: // medium return { targetWords: 150, targetChars: 900, description: "1-minute overview", promptWords: "150-170 words", maxTokens: 1000 }; } }; const config = getDurationConfig(duration); const scriptMessages = [ { role: "system", content: `You are a professional podcast script writer specializing in concise, impactful 1-minute overviews. Create engaging, conversational podcast scripts that are exactly ${config.promptWords} long when read aloud. CRITICAL REQUIREMENTS FOR 1-MINUTE OVERVIEWS: - Write EXACTLY ${config.promptWords} of spoken content - This is an OVERVIEW format - hit the key highlights only - Use conversational, engaging tone like a real podcast host - Structure: Quick hook (10%), Essential overview content (80%), Strong wrap-up (10%) - NO formatting, asterisks, brackets, or production notes - Write ONLY the words that will be spoken aloud - Focus on the most important/interesting facts - Give listeners the essential overview in 1 minute - Make it feel complete despite being brief IMPORTANT: This is a 1-minute overview, not a deep dive. Give the essential information that someone needs to know about this topic. The script should sound natural when read aloud and take approximately ${config.description} to speak.`, }, { role: "user", content: `Write a focused ${config.promptWords} podcast overview script about "${topic}". Structure for 1-minute overview: 1. Compelling hook that introduces the topic (${Math.round(config.targetWords * 0.1)} words) 2. Essential overview content (${Math.round(config.targetWords * 0.8)} words): - What this topic is and why it matters - The most important facts or insights - Key benefits or applications people should know - One memorable example or surprising fact 3. Strong conclusion that wraps up the overview (${Math.round(config.targetWords * 0.1)} words) Remember: This is an OVERVIEW - give listeners the essential information they need to understand this topic in just 1 minute. Make it informative but accessible. Write ONLY the spoken words - no production notes. TARGET: EXACTLY ${config.promptWords} for a complete 1-minute overview.`, }, ]; console.log(`Generating ${duration} duration overview script (target: ${config.targetWords} words) for topic: ${topic}`); // Script generation with proper max_tokens let scriptResponse: any = null; let attempts = 0; const maxAttempts = 3; while (!scriptResponse?.response && attempts < maxAttempts) { attempts++; console.log(`Script generation attempt ${attempts}/${maxAttempts}`); try { scriptResponse = await env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", { messages: scriptMessages, max_tokens: config.maxTokens, temperature: 0.7, stream: false }); if (scriptResponse?.response) { console.log(`āœ… Script generated successfully on attempt ${attempts}`); console.log(`Generated tokens: ~${Math.ceil(scriptResponse.response.length / 4)} tokens`); break; } } catch (error) { console.warn(`Script generation attempt ${attempts} failed:`, error); if (attempts < maxAttempts) { const delay = Math.pow(2, attempts) * 1000; console.log(`Waiting ${delay}ms before retry...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } if (!scriptResponse?.response) { throw new Error(`Failed to generate podcast script after ${maxAttempts} attempts. Cloudflare AI service may be temporarily unavailable.`); } let fullScript = scriptResponse.response; const wordCount = fullScript.split(/\s+/).length; console.log(`Generated script: ${fullScript.length} characters, ${wordCount} words (target: ${config.targetWords})`); // Clean script for audio const audioScript = fullScript .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1') .replace(/\[([^\]]+)\]/g, '$1') .replace(/\(([^)]+)\)/g, '') .replace(/Host:|Narrator:|Speaker:/gi, '') .replace(/Episode \d+:/gi, '') .replace(/Music fades in|Music fades out|Music plays|Sound effect|Audio clip/gi, '') .replace(/\n{3,}/g, '\n\n') .replace(/\s+/g, ' ') .trim(); console.log(`Cleaned script for audio: ${audioScript.length} characters, ${audioScript.split(/\s+/).length} words`); // Step 2: Generate audio using MeloTTS ONLY console.log("=== MELOTTS AUDIO GENERATION ==="); console.log(`Script length: ${audioScript.length} characters, ${audioScript.split(/\s+/).length} words`); let audioDataUrl = ""; let audioGenerationSuccess = false; let audioDebugInfo = ["Starting MeloTTS overview audio generation"]; try { console.log("šŸŽµ Using MeloTTS for complete 1-minute overview audio generation"); console.log("šŸŽµ Overview script optimized to fit completely in 1-minute audio"); console.log("Generating complete overview script audio with MeloTTS..."); console.log(`Complete script: ${audioScript.length} chars, ${audioScript.split(/\s+/).length} words`); audioDebugInfo.push(`Complete script: ${audioScript.length} chars, ${audioScript.split(/\s+/).length} words`); try { console.log("Making MeloTTS request for complete overview script..."); const meloResponse = await Promise.race([ env.AI.run('@cf/myshell-ai/melotts', { prompt: audioScript, lang: 'en', }), new Promise((_, reject) => setTimeout(() => reject(new Error('MeloTTS timeout')), 45000) ) ]); let meloAudioData = ""; // Handle both response formats from MeloTTS if (meloResponse && meloResponse instanceof Uint8Array && meloResponse.length > 1000) { meloAudioData = btoa(String.fromCharCode(...meloResponse)); console.log(`āœ… MeloTTS returned Uint8Array: ${meloResponse.length} bytes`); } else if (meloResponse && typeof meloResponse === 'object' && 'audio' in meloResponse) { const audioData = (meloResponse as any).audio; if (typeof audioData === 'string' && audioData.length > 1000) { meloAudioData = audioData; console.log(`āœ… MeloTTS returned audio object: ${audioData.length} chars`); } } if (meloAudioData) { audioDataUrl = `data:audio/mp3;base64,${meloAudioData}`; audioGenerationSuccess = true; const scriptWords = audioScript.split(/\s+/).length; const estimatedDuration = Math.round(scriptWords / 2.5); console.log(`āœ… COMPLETE SUCCESS! ${scriptWords}-word overview → ${estimatedDuration}s audio`); console.log(`Base64 audio length: ${meloAudioData.length} characters`); console.log(`Overview perfectly fits in 1-minute MeloTTS audio!`); audioDebugInfo.push(`āœ… Complete overview: ${scriptWords} words → ${estimatedDuration}s`); audioDebugInfo.push(`āœ… Perfect fit: Complete overview in complete audio`); audioDebugInfo.push(`āœ… Base64 length: ${meloAudioData.length} chars`); } else { console.log(`āŒ MeloTTS returned insufficient audio data`); audioDebugInfo.push(`āŒ Insufficient audio data from MeloTTS`); } } catch (meloError) { console.error("āŒ MeloTTS request failed:", meloError); audioDebugInfo.push(`āŒ MeloTTS error: ${meloError instanceof Error ? meloError.message : String(meloError)}`); } } catch (audioError) { console.error("āŒ MeloTTS generation failed:", audioError); audioDebugInfo.push(`āŒ Generation failed: ${audioError instanceof Error ? audioError.message : String(audioError)}`); } // Final status if (audioGenerationSuccess) { const scriptWords = audioScript.split(/\s+/).length; const estimatedDuration = Math.round(scriptWords / 2.5); console.log(`āœ… MELOTTS SUCCESS!`); console.log(`Overview: ${scriptWords} words (perfect for 1-minute audio)`); console.log(`Audio: Complete overview in ~${estimatedDuration} seconds`); console.log(`Result: 100% overview coverage in audio`); console.log(`Data: ${audioDataUrl.length} chars base64`); audioDebugInfo.push("āœ… MeloTTS complete overview audio generated"); audioDebugInfo.push(`100% overview coverage: ${scriptWords} words`); audioDebugInfo.push(`Duration: ~${estimatedDuration} seconds`); } else { console.log("āŒ MELOTTS FAILED: Audio generation unsuccessful"); console.log("šŸ’” Check: Cloudflare AI service status and connectivity"); audioDebugInfo.push("āŒ MeloTTS generation failed"); audioDebugInfo.push("Check Cloudflare AI service status"); } console.log("=== MELOTTS GENERATION COMPLETE ==="); // Step 3: Generate slug // Step 3: Generate AI-powered creative slug console.log("Generating creative AI slug..."); const generateCreativeSlug = async (topic: string, duration: string) => { // Create AI prompt to generate creative, podcast-style slug const slugPrompt = [ { role: "system", content: `You are a creative podcast slug generator. Generate a short, catchy, memorable slug (2-4 words) for a podcast episode that's engaging and professional. RULES: - Use only lowercase letters, numbers, and hyphens - 2-4 words maximum - Be creative and catchy, not just the topic name - Think like a professional podcast producer - Make it sound intriguing and clickable - Avoid generic words like "episode", "podcast", "show" - Focus on the essence or most interesting aspect of the topic Examples: - Topic: "Machine Learning" → "ai-revolution" or "smart-machines" or "future-minds" - Topic: "Cooking Tips" → "kitchen-secrets" or "chef-hacks" or "flavor-master" - Topic: "Space Exploration" → "cosmic-journey" or "star-hunters" or "void-pioneers" Generate ONLY the slug, nothing else.` }, { role: "user", content: `Generate a creative podcast slug for: "${topic}"` } ]; try { const slugResponse = await env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", { messages: slugPrompt, max_tokens: 50, temperature: 0.8, stream: false }); if (slugResponse?.response) { let aiSlug = slugResponse.response .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .trim(); // Ensure it's not empty and not too long if (aiSlug && aiSlug.length >= 3 && aiSlug.length <= 30) { console.log(`āœ… AI generated creative slug: ${aiSlug}`); return aiSlug; } } } catch (error) { console.warn("AI slug generation failed, falling back to topic-based slug:", error); } // Fallback to creative topic-based slug with random elements const fallbackSlug = topic.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .substring(0, 20); // Add creative prefix/suffix for uniqueness const creativePrefixes = ['explore', 'discover', 'deep', 'inside', 'beyond', 'next', 'future', 'smart', 'hidden', 'secret']; const creativeSuffixes = ['insights', 'revealed', 'decoded', 'explained', 'mastery', 'secrets', 'guide', 'journey', 'story', 'truth']; const usePrefix = Math.random() > 0.5; const randomWords = usePrefix ? creativePrefixes : creativeSuffixes; const randomWord = randomWords[Math.floor(Math.random() * randomWords.length)]; const creativeSlug = usePrefix ? `${randomWord}-${fallbackSlug}` : `${fallbackSlug}-${randomWord}`; console.log(`āœ… Generated creative fallback slug: ${creativeSlug}`); return creativeSlug; }; let slug = await generateCreativeSlug(topic, duration); // Check for uniqueness and add timestamp if needed try { const existingSlug = await env.DB.prepare("SELECT slug FROM podcasts WHERE slug = ?").bind(slug).first(); if (existingSlug) { // Add short random suffix instead of full timestamp const randomSuffix = Math.random().toString(36).substring(2, 6); slug = `${slug}-${randomSuffix}`; console.log(`Slug already exists, using unique variant: ${slug}`); } } catch (e) { console.warn("Could not check for existing slug:", e); } try { const existingSlug = await env.DB.prepare("SELECT slug FROM podcasts WHERE slug = ?").bind(slug).first(); if (existingSlug) { const timestamp = Date.now(); slug = `${slug}-${timestamp}`; console.log(`Slug already exists, using unique slug: ${slug}`); } } catch (e) { console.warn("Could not check for existing slug:", e); } const finalUrl = `${baseUrl}/${slug}`; console.log(`Final URL will be: ${finalUrl}`); // Step 4: Upload audio to R2 and save metadata to database try { if (!fullScript || fullScript.trim().length < 100) { throw new Error("Generated script is too short or empty"); } const finalWordCount = fullScript.split(/\s+/).length; const estimatedDuration = Math.round(finalWordCount / 150 * 60); console.log(`Saving podcast: ${finalWordCount} words, ~${estimatedDuration} seconds estimated duration`); console.log(`Audio generation success: ${audioGenerationSuccess}`); let audioUrl = ""; // Upload audio to R2 if we have it if (audioGenerationSuccess && audioDataUrl) { try { const base64Audio = audioDataUrl.replace(/^data:audio\/mp3;base64,/, ''); const binaryAudio = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0)); const audioKey = `podcasts/${slug}.mp3`; console.log(`Uploading audio to R2: ${audioKey}, size: ${binaryAudio.length} bytes`); await env.PODCAST_BUCKET.put(audioKey, binaryAudio, { httpMetadata: { contentType: 'audio/mpeg', cacheControl: 'public, max-age=31536000', }, customMetadata: { topic: topic, slug: slug, uploadedAt: new Date().toISOString(), }, }); audioUrl = `https://podcaster.lizziepika.workers.dev/audio/${audioKey}`; console.log(`āœ… Audio uploaded to R2: ${audioUrl}`); audioDebugInfo.push(`āœ… Uploaded to R2: ${binaryAudio.length} bytes`); } catch (r2Error) { console.error("āŒ R2 upload failed:", r2Error); audioDebugInfo.push(`āŒ R2 upload failed: ${r2Error instanceof Error ? r2Error.message : String(r2Error)}`); audioGenerationSuccess = false; } } else { console.log("No audio data to upload to R2"); audioDebugInfo.push("No audio data generated"); } // Ensure database columns exist try { await env.DB.prepare("ALTER TABLE podcasts ADD COLUMN script TEXT").run(); } catch (e) { /* Column exists */ } try { await env.DB.prepare("ALTER TABLE podcasts ADD COLUMN audio_url TEXT").run(); } catch (e) { /* Column exists */ } // Save metadata to D1 database console.log(`Saving to database: script=${fullScript.length} chars, audio_url=${audioUrl}`); const stmt = env.DB.prepare(` INSERT INTO podcasts (topic, slug, url, created_at, script, audio_url) VALUES (?, ?, ?, datetime('now'), ?, ?) `); await stmt.bind( `${config.description}: ${topic}`, slug, finalUrl, fullScript, audioUrl ).run(); console.log(`āœ… Database insert completed`); // Verification try { const verifyStmt = env.DB.prepare("SELECT topic, slug, LENGTH(script) as script_length, audio_url FROM podcasts WHERE slug = ?"); const savedRecord = await verifyStmt.bind(slug).first(); if (savedRecord) { console.log(`āœ… Verification successful:`); console.log(` - Topic: ${savedRecord.topic}`); console.log(` - Script length: ${savedRecord.script_length} chars`); console.log(` - Audio URL: ${savedRecord.audio_url || 'NULL'}`); } else { console.error("āŒ CRITICAL: No record found after save!"); } } catch (verifyError) { console.error("āŒ Verification query failed:", verifyError); } } catch (dbError) { console.error("āŒ Database save failed:", dbError); throw new Error(`Database save failed: ${dbError instanceof Error ? dbError.message : String(dbError)}`); } // Step 5: Generate success message const finalWordCount = fullScript.split(/\s+/).length; const estimatedMinutes = Math.round(finalWordCount / 150); let successMessage = `šŸŽ§ Your ${config.description} overview podcast about "${topic}" is ready!\n\nšŸ“Š Details:\n• ${finalWordCount} words (perfect 1-minute overview)\n• Professional script generated with max_tokens: ${config.maxTokens}\n• Complete overview fits perfectly in MeloTTS audio\n• Essential information in focused format\n\nšŸ”— Listen now: ${finalUrl}`; if (audioGenerationSuccess) { const scriptWords = fullScript.split(/\s+/).length; const estimatedDuration = Math.round(scriptWords / 2.5); successMessage += `\n\nšŸŽµ Audio Status: āœ… COMPLETE OVERVIEW AUDIO!\n• Generated from your complete ${scriptWords}-word overview script\n• Perfect match: 100% script coverage in audio\n• Estimated duration: ~${estimatedDuration} seconds\n• Audio data: ${audioDataUrl.length} chars\n• Complete overview - nothing missing!`; } else { successMessage += `\n\nšŸŽµ Audio Status: āŒ MeloTTS generation failed\n• Debug: ${audioDebugInfo.slice(-2).join(' | ')}\n• Check Cloudflare AI service status\n• Overview script is optimized for 1-minute audio\n• Full overview script available for reading`; } successMessage += `\n\nšŸŽ‰ Don't forget to check out the bonus content surprise!`; // Add debug info to console for troubleshooting console.log("=== FINAL MESSAGE DEBUG ==="); console.log(`Audio success: ${audioGenerationSuccess}`); console.log(`Audio data length: ${audioDataUrl.length}`); console.log(`Debug info: ${audioDebugInfo.join(' | ')}`); console.log(`Max tokens used: ${config.maxTokens}`); console.log(`MeloTTS: Complete overview script in 1-minute audio`); console.log(`Perfect overview: 100% script coverage`); console.log("==========================="); return { content: [ { type: "text", text: successMessage } ] }; } catch (error) { console.error("Failed to create audio podcast:", error); console.error("Full error details:", error instanceof Error ? error.stack : String(error)); let errorMessage = `āŒ Error generating ${duration} podcast about "${topic}": `; if (error instanceof Error) { if (error.message.includes('3040') || error.message.includes('Capacity temporarily exceeded')) { errorMessage += `Cloudflare AI service is temporarily at capacity (error 3040). Try again in 1-2 minutes.`; } else if (error.message.includes('timeout')) { errorMessage += `AI service timed out. Try using a shorter duration ("short") or retry in a moment.`; } else if (error.message.includes('SQLITE_TOOBIG') || error.message.includes('too big') || error.message.includes('size limits')) { errorMessage += `Generated content is too large for database storage. D1 has size limits per column.`; } else { errorMessage += error.message; } } else { errorMessage += String(error); } if (error instanceof Error && (error.message.includes('SQLITE_TOOBIG') || error.message.includes('size limits'))) { errorMessage += `\n\nšŸ’” Solutions:\n• Use "short" duration for smaller content\n• Try a more specific topic (less broad)\n• The system will automatically truncate oversized content`; } else { errorMessage += `\n\nšŸ’” Suggestions:\n• Wait 1-2 minutes and try again\n• Try "short" duration for faster generation\n• Check Cloudflare AI service status`; } return { content: [ { type: "text", text: errorMessage } ] }; } } ); // UTILITY TOOL: Test and validate audio data in database this.server.tool( "test_audio_data", { slug: z.string().describe("Podcast slug to test audio data for"), }, async ({ slug }) => { try { const env = getEnv<Env>(); const stmt = env.DB.prepare(` SELECT topic, slug, LENGTH(audio_data) as audio_length, SUBSTR(audio_data, 1, 100) as audio_preview FROM podcasts WHERE slug = ? `); const result = await stmt.bind(slug).first(); if (!result) { return { content: [ { type: "text", text: `āŒ No podcast found with slug: ${slug}` } ] }; } const audioLength = result.audio_length as number; const audioPreview = result.audio_preview as string; let status = ""; if (!audioLength || audioLength === 0) { status = "āŒ No audio data saved"; } else if (!audioPreview || !audioPreview.startsWith('data:audio')) { status = `āŒ Invalid audio format (${audioLength} chars)`; } else if (audioLength < 1000) { status = `āš ļø Audio data too short (${audioLength} chars)`; } else { status = `āœ… Valid audio data (${audioLength} chars)`; } return { content: [ { type: "text", text: `šŸ” Audio Test Results for "${result.topic}":\n\n${status}\n• Preview: ${audioPreview || 'NULL'}\n• Length: ${audioLength} characters\n\nIf audio is invalid, regenerate the podcast to fix it.` } ] }; } catch (error) { console.error("Failed to test audio data:", error); return { content: [ { type: "text", text: "Failed to test audio data from database." } ] }; } } ); this.server.tool( "list_recent_podcasts", { limit: z.number().optional().default(10).describe("Number of recent podcasts to retrieve (default: 10)"), }, async ({ limit }) => { try { const env = getEnv<Env>(); const stmt = env.DB.prepare(` SELECT topic, slug, url, created_at, script, audio_data FROM podcasts ORDER BY created_at DESC LIMIT ? `); const result = await stmt.bind(limit).all(); const podcasts = result.results || []; if (podcasts.length === 0) { return { content: [ { type: "text", text: "No podcasts have been generated yet. Use generate_audio_podcast to create your first one!" } ] }; } const podcastList = podcasts.map((p: any) => { const date = new Date(p.created_at).toLocaleString(); const wordCount = p.script ? (p.script as string).split(/\s+/).length : 0; const duration = wordCount > 0 ? `~${Math.round(wordCount / 150)}min` : 'Unknown'; const hasAudio = p.audio_data ? ' šŸŽµ' : ''; const hasScript = p.script ? ' šŸ“' : ''; return `šŸŽ§ ${p.topic}${hasAudio}${hasScript} (${duration}) - ${p.url} (${date})`; }).join('\n'); return { content: [ { type: "text", text: `šŸ“» Recent Podcasts (${podcasts.length}):\n\n${podcastList}\n\nšŸŽ§ = Podcast • šŸŽµ = Has audio • šŸ“ = Has script • Duration based on ~150 words/minute` } ] }; } catch (error) { console.error("Failed to retrieve podcasts:", error); return { content: [ { type: "text", text: "Failed to retrieve podcast list from database." } ] }; } } ); } } // Export as Durable Object export { RedirectMCP as MyMCP }; // MAIN WORKER EXPORT HANDLER export default { fetch(request: Request, env: any, ctx: ExecutionContext) { const url = new URL(request.url); // Filter out favicon.ico and other non-podcast requests if (url.pathname === "/favicon.ico" || url.pathname.includes(".ico") || url.pathname.includes(".png") || url.pathname.includes(".jpg")) { return new Response("", { status: 404 }); } /** * SERVER-SENT EVENTS (SSE) ENDPOINT */ if (url.pathname === "/sse" || url.pathname === "/sse/message") { return RedirectMCP.serveSSE("/sse").fetch(request, env, ctx); } /** * MCP ENDPOINT */ if (url.pathname === "/mcp") { return RedirectMCP.serve("/mcp").fetch(request, env, ctx); } return new Response(`šŸŽ™ļø AI Podcast MCP Server v2.0 - Complete Overview in Complete Audio! šŸŽ§ MELOTTS OVERVIEW AUDIO FLOW: • Short (45s): 120-140 words, focused overview (max_tokens: 800) • Medium (1min): 150-170 words, complete overview (max_tokens: 1000) • Long (75s): 180-200 words, comprehensive overview (max_tokens: 1200) šŸ› ļø Available Tools: • generate_audio_podcast(topic, duration?) - Creates complete 1-minute overviews - duration: "short" | "medium" | "long" (default: "medium") • list_recent_podcasts(limit?) - Shows generated overview content šŸŽµ MeloTTS Complete Overview Strategy: • Scripts optimized for 1-minute overview format • Perfect match: 100% overview script in 100% audio • Essential information delivery in 1 minute • Complete listening experience with no missing content • Focused on key highlights and takeaways šŸ“Š Content Quality: • Professional overview structure for 1-minute format • Conversational tone optimized for quick consumption • Essential facts and key insights • Complete overview scripts with complete audio coverage • āœ… COMPLETE OVERVIEWS - Essential info in 1 minute! • āœ… COMPLETE AUDIO - 100% script coverage guaranteed! • āœ… PERFECT MATCH - Overview length optimized for audio length! āš™ļø Setup: • No API keys required - uses Cloudflare's reliable MeloTTS • Consistent, reliable audio generation • Optimized for complete overview-to-audio matching šŸ’” Perfect solution: Essential overview content in complete audio coverage! Connect Claude to /mcp endpoint to access complete overview podcast generation!`, { status: 200, headers: { 'Content-Type': 'text/plain' } }); }, };

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/elizabethsiegle/remote-mcp-server-authless-rickroll'

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