openai-elevenlabs-tts-mcp-server
Version:
MCP server that converts text to speech using OpenAI for transformation and ElevenLabs for voice synthesis
251 lines (250 loc) • 10.4 kB
JavaScript
/**
* ElevenLabs TTS MCP Server
*
* This server provides a tool to convert text into speech using OpenAI for text transformation
* and ElevenLabs for text-to-speech conversion.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import { OpenAI } from "openai";
import axios from "axios";
import fs from "fs-extra";
import path from "path";
import { createHash } from "crypto";
import { fileURLToPath } from "url";
// Environment variables for API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
// Check if API keys are provided
if (!OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY environment variable is required");
}
if (!ELEVENLABS_API_KEY) {
throw new Error("ELEVENLABS_API_KEY environment variable is required");
}
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: OPENAI_API_KEY
});
// Constants
const VOICE_ID = "Ey0CGgf72TcJDRR6P7ol"; // Default voice ID
const OUTPUT_FORMAT = "mp3_44100_128"; // Default output format
// Get the directory where the script is located
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Go up one level to get the project root directory
const PROJECT_ROOT = path.resolve(__dirname, '..');
// Set the output directory to be configurable with fallback to current working directory
const OUTPUT_DIR = process.env.TTS_OUTPUT_DIR || path.join(process.cwd(), "outputs");
console.error(`[Setup] Project root: ${PROJECT_ROOT}`);
console.error(`[Setup] Output directory: ${OUTPUT_DIR}`);
// Ensure output directory exists
fs.ensureDirSync(OUTPUT_DIR);
// Create an MCP server
const server = new Server({
name: "openai-elevenlabs-tts-mcp-server",
version: "0.1.2",
}, {
capabilities: {
tools: {},
},
});
/**
* Generate a hash for the input text to use as part of the filename
*/
function generateHash(text) {
return createHash("md5").update(text).digest("hex").substring(0, 8);
}
/**
* Check if the input is a file path and read its content if it is
*/
async function getTextContent(input) {
// Check if input is a .txt file path
if (input.endsWith(".txt")) {
try {
if (await fs.pathExists(input)) {
console.error(`[Info] Reading file: ${input}`);
return await fs.readFile(input, "utf-8");
}
}
catch (error) {
console.error(`[Error] Failed to read file: ${error}`);
}
}
else if (input.endsWith(".md")) {
// Explicitly reject Markdown files
throw new McpError(ErrorCode.InvalidParams, "Markdown files are not supported. Please provide a .txt file instead.");
}
// Return the input as is if it's not a file or if reading failed
return input;
}
/**
* Transform text using OpenAI to create a light-hearted and adult fun script
*/
async function transformTextWithOpenAI(text) {
console.error(`[OpenAI] Transforming text with GPT-4o`);
try {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: "You are a comedy writer who transforms text to sound exactly like a mix of Mitch Hedberg and Donald Trump. Your task is to rewrite the provided text in Mitch Hedberg's distinctive deadpan, one-liner style with his unique cadence and delivery, while incorporating Trump's repetitive phrases, exaggerated superlatives, and self-referential style. For Mitch Hedberg elements: use short, unexpected observations, trail off sentences, add 'you know' and 'man' frequently, include his signature pauses and 'uhhh', and use his laid-back delivery style. For Trump elements: use phrases like 'tremendous', 'believe me', 'the best', exaggerate everything, repeat key points, and add self-congratulatory asides. IMPORTANT: Your output MUST be in plain text format only. DO NOT use any Markdown formatting, symbols, or syntax. Use natural language elements like CAPITAL LETTERS for emphasis, ellipses (...) for pauses, and line breaks for natural speech pauses. Include Hedberg's signature 'uhhh' and Trump's 'you know what' throughout. The output should be ready for direct text-to-speech conversion without any special formatting."
},
{
role: "user",
content: text
}
],
temperature: 0.7,
});
const transformedText = response.choices[0]?.message?.content;
if (!transformedText) {
throw new Error("Failed to transform text with OpenAI");
}
return transformedText;
}
catch (error) {
console.error(`[OpenAI Error] ${error}`);
throw new McpError(ErrorCode.InternalError, `OpenAI API error: ${error}`);
}
}
/**
* Convert text to speech using ElevenLabs API
*/
async function convertTextToSpeech(text) {
console.error(`[ElevenLabs] Converting text to speech`);
try {
const response = await axios({
method: "post",
url: `https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`,
headers: {
"Accept": "audio/mpeg",
"Content-Type": "application/json",
"xi-api-key": ELEVENLABS_API_KEY
},
data: {
text: text,
model_id: "eleven_monolingual_v1",
voice_settings: {
stability: 0.5,
similarity_boost: 0.75
},
output_format: OUTPUT_FORMAT
},
responseType: "arraybuffer"
});
return Buffer.from(response.data);
}
catch (error) {
console.error(`[ElevenLabs Error] ${error}`);
throw new McpError(ErrorCode.InternalError, `ElevenLabs API error: ${error}`);
}
}
/**
* Save audio data to a file
*/
async function saveAudioFile(audioData, originalText) {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const hash = generateHash(originalText);
const filename = `tts_${timestamp}_${hash}.mp3`;
const outputPath = path.join(OUTPUT_DIR, filename);
console.error(`[File] Saving audio to: ${outputPath}`);
try {
await fs.writeFile(outputPath, audioData);
return outputPath;
}
catch (error) {
console.error(`[File Error] ${error}`);
throw new McpError(ErrorCode.InternalError, `Failed to save audio file: ${error}`);
}
}
/**
* Handler that lists available tools.
* Exposes a single "generate_audio" tool that converts text to speech.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "generate_audio",
description: "Convert text to speech using OpenAI for transformation and ElevenLabs for TTS",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "Text content or path to a .txt file to convert to speech. Markdown files are not supported."
}
},
required: ["text"]
}
}
]
};
});
/**
* Handler for the generate_audio tool.
* Processes the text, transforms it with OpenAI, converts it to speech with ElevenLabs,
* and saves the audio file.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "generate_audio": {
const inputText = String(request.params.arguments?.text || "");
if (!inputText) {
throw new McpError(ErrorCode.InvalidParams, "Text parameter is required");
}
try {
// Get text content (from input or file)
const textContent = await getTextContent(inputText);
console.error(`[Process] Processing text of length: ${textContent.length}`);
// Transform text with OpenAI
const transformedText = await transformTextWithOpenAI(textContent);
console.error(`[Process] Transformed text of length: ${transformedText.length}`);
// Convert text to speech with ElevenLabs
const audioData = await convertTextToSpeech(transformedText);
console.error(`[Process] Generated audio of size: ${audioData.length} bytes`);
// Save audio file
const outputPath = await saveAudioFile(audioData, textContent);
return {
content: [{
type: "text",
text: JSON.stringify({
status: "success",
message: "Audio generated successfully",
outputPath: outputPath,
originalTextLength: textContent.length,
transformedTextLength: transformedText.length,
audioSizeBytes: audioData.length
}, null, 2)
}]
};
}
catch (error) {
console.error(`[Error] ${error}`);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to generate audio: ${error}`);
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, "Unknown tool");
}
});
/**
* Start the server using stdio transport.
*/
async function main() {
console.error("[Setup] Starting ElevenLabs TTS MCP Server");
console.error(`[Setup] Output directory: ${OUTPUT_DIR}`);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[Setup] Server connected");
}
main().catch((error) => {
console.error("[Fatal Error]:", error);
process.exit(1);
});