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' }
});
},
};