Skip to main content
Glama

TTS-MCP

by nakamurau1
mcp-server.ts9.2 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { OpenAI } from "openai"; import * as z from "zod"; import { promises as fs } from "fs"; import path from "path"; import os from "os"; // 一時ディレクトリのパスを取得するために使用 import playSound from "play-sound"; import { MCPServerConfig, OpenAIVoice, OpenAIOutputFormat, OpenAITTSModel } from "./types"; import { validateVoice, validateModel, validateFormat } from "./utils"; // プレイヤーの初期化 const player = playSound({}); // ログファイルパス let logFile = path.join(process.cwd(), 'tts-mcp.log'); /** * ログファイルにメッセージを追加します * @param {string} message - ログメッセージ */ async function logToFile(message: string): Promise<void> { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; try { await fs.appendFile(logFile, logMessage); } catch (error) { // ログ書き込みエラーは無視(エラー処理のループを避けるため) } } /** * OpenAI APIのクライアントを初期化します * @param {string} apiKey - OpenAI APIキー * @returns {OpenAI} OpenAIクライアントインスタンス */ export function initializeClient(apiKey: string): OpenAI { if (!apiKey) { throw new Error('OpenAI APIキーが設定されていません。--api-keyオプションか環境変数OPENAI_API_KEYを設定してください。'); } return new OpenAI({ apiKey: apiKey }); } interface TTSPlayOptions { text: string; model: OpenAITTSModel; voice: OpenAIVoice; speed: number; format: OpenAIOutputFormat; instructions?: string; apiKey: string; } interface TTSPlayResult { duration: string; textLength: number; } /** * テキストを音声に変換して再生します * @param {TTSPlayOptions} options - 変換オプション * @returns {Promise<TTSPlayResult>} 処理結果 */ async function textToSpeechAndPlay(options: TTSPlayOptions): Promise<TTSPlayResult> { const client = initializeClient(options.apiKey); let tempFilePath: string | null = null; try { await logToFile('音声生成開始...'); // 入力の検証 const safeVoice = validateVoice(options.voice); const safeModel = validateModel(options.model); const safeFormat = validateFormat(options.format); const response = await client.audio.speech.create({ model: safeModel, voice: safeVoice, input: options.text, speed: options.speed, response_format: safeFormat, ...(options.instructions ? { instructions: options.instructions } : {}) }); // 音声データを取得 const buffer = Buffer.from(await response.arrayBuffer()); // 直接一時ファイルパスを生成 tempFilePath = path.join(os.tmpdir(), `speech_${Date.now()}.${safeFormat}`); // バッファをファイルに書き込む await fs.writeFile(tempFilePath, buffer); await logToFile(`音声ファイルを作成しました: ${tempFilePath}`); // 再生開始時間を記録 const startTime = Date.now(); await logToFile(`音声を再生します: ${tempFilePath}`); // 音声を再生(Promise化) await new Promise<void>((resolve, reject) => { player.play(tempFilePath!, (err: Error | null) => { if (err) reject(err); else resolve(); }); }); // 再生時間を計算(秒単位) const duration = ((Date.now() - startTime) / 1000).toFixed(1); await logToFile(`音声の再生が完了しました(再生時間: ${duration}秒)`); // 一時ファイルの削除を試みる(任意) try { await fs.unlink(tempFilePath); await logToFile(`一時ファイルを削除しました: ${tempFilePath}`); } catch (cleanupError) { await logToFile(`一時ファイルの削除に失敗しました: ${(cleanupError as Error).message}`); // 削除に失敗しても処理は続行 } return { duration, textLength: options.text.length }; } catch (error: any) { // エラーが発生した場合、一時ファイルが存在していれば削除を試みる if (tempFilePath) { try { await fs.unlink(tempFilePath); } catch (cleanupError) { // 削除エラーは無視 } } // APIエラーをより詳細にログ記録 if (error.response) { const apiError = error.response.data; await logToFile(`OpenAI API エラー:`); await logToFile(`- ステータス: ${error.response.status}`); await logToFile(`- メッセージ: ${apiError.error?.message || 'エラー詳細なし'}`); await logToFile(`- タイプ: ${apiError.error?.type || 'エラータイプなし'}`); // 元のエラーもログに記録 await logToFile(`生のレスポンスデータ: ${JSON.stringify(error.response.data)}`); } else { // 一般的なエラー await logToFile(`エラー: ${error.message}`); } throw error; } } /** * MCPサーバーを作成して設定します * @param {MCPServerConfig} config - サーバー設定 * @returns {Promise<McpServer>} 設定済みMCPサーバー */ async function createMcpServer(config: MCPServerConfig): Promise<McpServer> { // カスタムログファイルがあれば設定 if (config.logFile) { logFile = config.logFile; } // ログファイルを初期化 await logToFile('---------------------------------------'); await logToFile(`MCPサーバーを初期化しています...`); await logToFile(`設定: モデル=${config.model}, 音声=${config.voice}, フォーマット=${config.format}`); // MCPサーバーを作成 const server = new McpServer({ name: "tts-mcp", version: "1.1.0", // サポートする機能を明示的に定義 capabilities: { tools: {} } }); // テキスト音声変換と再生ツールを追加 server.tool( "text-to-speech", "Converts text to speech and plays it using OpenAI's TTS API", { text: z.string().describe("The text content to be converted to speech"), speed: z.number().min(0.25).max(4.0).optional().default(1.0).describe("Speech speed factor (0.25 to 4.0, default: 1.0)"), instructions: z.string().optional().describe("Optional instructions to guide the speech generation (e.g. emotions, style)"), }, async (params) => { try { const result = await textToSpeechAndPlay({ text: params.text, model: config.model, voice: config.voice, speed: params.speed, format: config.format, instructions: params.instructions, apiKey: config.apiKey }); return { content: [ { type: "text", text: `テキストを音声で再生しました(再生時間: ${result.duration}秒)` } ], metadata: { duration: result.duration, text_length: result.textLength } }; } catch (error) { return { content: [ { type: "text", text: `エラー: 音声の生成または再生に失敗しました` }, { type: "text", text: `詳細: ${(error as Error).message}` }, { type: "text", text: `対処方法: OpenAI APIキーの確認、テキスト内容の確認、または別のボイスやモデルの使用をお試しください。` } ], isError: true }; } } ); return server; } /** * MCPサーバーを起動します * @param {MCPServerConfig} config - サーバー設定 * @returns {Promise<void>} */ export async function startMcpServer(config: MCPServerConfig): Promise<void> { // 入力値の検証 const safeModel = validateModel(config.model); const safeVoice = validateVoice(config.voice); const safeFormat = validateFormat(config.format); // 検証済みの値で設定を上書き const validatedConfig: MCPServerConfig = { ...config, model: safeModel, voice: safeVoice, format: safeFormat }; // stderr経由でコンテキスト情報を出力 process.stderr.write(`モデル=${safeModel}, 音声=${safeVoice}, フォーマット=${safeFormat}\n`); try { // サーバーを作成 const server = await createMcpServer(validatedConfig); // STDIOトランスポートを使用 const transport = new StdioServerTransport(); // サーバーを接続して開始 await server.connect(transport); await logToFile("MCPサーバーが起動しました"); } catch (error) { await logToFile(`MCPサーバー起動エラー: ${(error as Error).message}`); // stderrにもエラーを出力 process.stderr.write(`エラー: ${(error as Error).message}\n`); throw error; } }

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/nakamurau1/tts-mcp'

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