#
tokens: 5810/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── mcp_config.json
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | howler.md
6 | memory.db
7 | vector_store.db
8 | output/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # <span style="color: #FF69B4;">📢 Blabber-MCP</span> <span style="color: #ADD8E6;">🗣️</span>
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@pinkpixel-dev/blabber-mcp)](https://smithery.ai/server/@pinkpixel-dev/blabber-mcp)
  4 | 
  5 | <span style="color: #90EE90;">An MCP server that gives your LLMs a voice using OpenAI's Text-to-Speech API!</span> 🔊
  6 | 
  7 | ---
  8 | 
  9 | ## <span style="color: #FFD700;">✨ Features</span>
 10 | 
 11 | *   **Text-to-Speech:** Converts input text into high-quality spoken audio.
 12 | *   **Voice Selection:** Choose from various OpenAI voices (`alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`).
 13 | *   **Model Selection:** Use standard (`tts-1`) or high-definition (`tts-1-hd`) models.
 14 | *   **Format Options:** Get audio output in `mp3`, `opus`, `aac`, or `flac`.
 15 | *   **File Saving:** Saves the generated audio to a local file.
 16 | *   **Optional Playback:** Automatically play the generated audio using a configurable system command.
 17 | *   **Configurable Defaults:** Set a default voice via configuration.
 18 | 
 19 | ---
 20 | 
 21 | ## <span style="color: #FFA07A;">🔧 Configuration</span>
 22 | 
 23 | To use this server, you need to add its configuration to your MCP client's settings file (e.g., `mcp_settings.json`).
 24 | 
 25 | 1.  **Get OpenAI API Key:** You need an API key from [OpenAI](https://platform.openai.com/api-keys).
 26 | 2.  **Add to MCP Settings:** Add the following block to the `mcpServers` object in your settings file, replacing `"YOUR_OPENAI_API_KEY"` with your actual key.
 27 | 
 28 | ```json
 29 | {
 30 |   "mcpServers": {
 31 |     "blabber-mcp": {
 32 |       "command": "node",
 33 |       "args": ["/full/path/to/blabber-mcp/build/index.js"], (IMPORTANT: Use the full, absolute path to the built index.js file)
 34 |       "env": {
 35 |         "OPENAI_API_KEY": "YOUR_OPENAI_API_KEY",
 36 |         "AUDIO_PLAYER_COMMAND": "xdg-open", (Optional: Command to play audio (e.g., "cvlc", "vlc", "mpv", "ffplay", "afplay", "xdg-open"; defaults to "cvlc")
 37 |         "DEFAULT_TTS_VOICE": "nova" (Optional: Set default voice (alloy, echo, fable, onyx, nova, shimmer); defaults to nova)
 38 |       },
 39 |       "disabled": false,
 40 |       "alwaysAllow": []
 41 |     }
 42 |   }
 43 | }
 44 | ```
 45 | 
 46 | <span style="color: #FF6347;">**Important:**</span> Make sure the `args` path points to the correct location of the `build/index.js` file within your `blabber-mcp` project directory. Use the full absolute path.
 47 | 
 48 | ---
 49 | 
 50 | ## <span style="color: #87CEEB;">🚀 Usage</span>
 51 | 
 52 | Once configured and running, you can use the `text_to_speech` tool via your MCP client.
 53 | 
 54 | **Tool:** `text_to_speech`
 55 | **Server:** `blabber-mcp` (or the key you used in the config)
 56 | 
 57 | **Arguments:**
 58 | 
 59 | *   `input` (string, **required**): The text to synthesize.
 60 | *   `voice` (string, optional): The voice to use (`alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`). Defaults to the `DEFAULT_TTS_VOICE` set in config, or `nova`.
 61 | *   `model` (string, optional): The model (`tts-1`, `tts-1-hd`). Defaults to `tts-1`.
 62 | *   `response_format` (string, optional): Audio format (`mp3`, `opus`, `aac`, `flac`). Defaults to `mp3`.
 63 | *   `play` (boolean, optional): Set to `true` to automatically play the audio after saving. Defaults to `false`.
 64 | 
 65 | **Example Tool Call (with playback):**
 66 | 
 67 | ```xml
 68 | <use_mcp_tool>
 69 |   <server_name>blabber-mcp</server_name>
 70 |   <tool_name>text_to_speech</tool_name>
 71 |   <arguments>
 72 |   {
 73 |     "input": "Hello from Blabber MCP!",
 74 |     "voice": "shimmer",
 75 |     "play": true
 76 |   }
 77 |   </arguments>
 78 | </use_mcp_tool>
 79 | ```
 80 | 
 81 | **Output:**
 82 | 
 83 | The tool saves the audio file to the `output/` directory within the `blabber-mcp` project folder and returns a JSON response like this:
 84 | 
 85 | ```json
 86 | {
 87 |   "message": "Audio saved successfully. Playback initiated using command: cvlc",
 88 |   "filePath": "path/to/speech_1743908694848.mp3", 
 89 |   "format": "mp3",
 90 |   "voiceUsed": "shimmer"
 91 | }
 92 | ```
 93 | 
 94 | ---
 95 | 
 96 | ## <span style="color: #98FB98;">📜 License</span>
 97 | 
 98 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
 99 | 
100 | ---
101 | 
102 | ## <span style="color: #BA55D3;">🕒 Changelog</span>
103 | 
104 | See the [CHANGELOG.md](CHANGELOG.md) file for details on version history.
105 | 
106 | ---
107 | 
108 | <p align="center">Made with ❤️ by Pink Pixel</p>
109 | 
```

--------------------------------------------------------------------------------
/mcp_config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "mcpServers": {
 3 |     "openai-tts": {
 4 |       "command": "node",
 5 |       "args": ["/path/to/blabber-mcp/build/index.js"],
 6 |       "env": {
 7 |         "OPENAI_API_KEY": "YOUR_OPENAI_API_KEY",
 8 |         "AUDIO_PLAYER_COMMAND": "cvlc",
 9 |         "DEFAULT_TTS_VOICE": "nova" 
10 |       },
11 |       "disabled": false,
12 |       "alwaysAllow": []
13 |     }
14 |   }
15 | }
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@pinkpixel/blabber-mcp",
 3 |   "version": "0.1.2",
 4 |   "description": "An MCP server that gives a voice to LLMs",
 5 |   "private": false,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "blabber-mcp": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 |     "prepare": "npm run build",
16 |     "watch": "tsc --watch",
17 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18 |   },
19 |   "dependencies": {
20 |     "@modelcontextprotocol/sdk": "0.6.0",
21 |     "openai": "^4.91.1"
22 |   },
23 |   "devDependencies": {
24 |     "@types/node": "^20.11.24",
25 |     "typescript": "^5.3.3"
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
 2 | # syntax=docker/dockerfile:1
 3 | 
 4 | # Builder stage
 5 | FROM node:lts-alpine AS builder
 6 | WORKDIR /app
 7 | # Install build dependencies and source
 8 | COPY package.json package-lock.json tsconfig.json ./
 9 | COPY src ./src
10 | RUN apk add --no-cache git python3 make g++ \
11 |  && npm install \
12 |  && npm run build
13 | 
14 | # Final stage
15 | FROM node:lts-alpine
16 | WORKDIR /app
17 | # Copy built code and production dependencies
18 | COPY --from=builder /app/build ./build
19 | COPY --from=builder /app/node_modules ./node_modules
20 | COPY --from=builder /app/package.json ./package.json
21 | 
22 | # Ensure output directory exists
23 | RUN mkdir -p /app/output
24 | 
25 | ENV NODE_ENV=production
26 | ENTRYPOINT ["node", "build/index.js"]
27 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   commandFunction:
 6 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
 7 |     |-
 8 |     (config) => ({ command: 'node', args: ['build/index.js'], env: { OPENAI_API_KEY: config.openaiApiKey, AUDIO_PLAYER_COMMAND: config.audioPlayerCommand, DEFAULT_TTS_VOICE: config.defaultTtsVoice } })
 9 |   configSchema:
10 |     # JSON Schema defining the configuration options for the MCP.
11 |     type: object
12 |     required:
13 |       - openaiApiKey
14 |     properties:
15 |       openaiApiKey:
16 |         type: string
17 |         description: OpenAI API key for authentication
18 |       audioPlayerCommand:
19 |         type: string
20 |         default: xdg-open
21 |         description: Command to play audio
22 |       defaultTtsVoice:
23 |         type: string
24 |         default: nova
25 |         description: Default TTS voice
26 |   exampleConfig:
27 |     openaiApiKey: YOUR_OPENAI_API_KEY
28 |     audioPlayerCommand: xdg-open
29 |     defaultTtsVoice: nova
30 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # <span style="color: #FF69B4;">🕒 Changelog</span>
 2 | 
 3 | All notable changes to the **Blabber-MCP** project will be documented in this file.
 4 | 
 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 7 | 
 8 | ---
 9 | 
10 | ## <span style="color: #ADD8E6;">[0.1.2] - 2025-04-05</span>
11 | 
12 | ### <span style="color: #90EE90;">✨ Added</span>
13 | 
14 | *   Configurable default voice via `DEFAULT_TTS_VOICE` environment variable.
15 | 
16 | ## <span style="color: #FFD700;">[0.1.1] - 2025-04-05</span>
17 | 
18 | ### <span style="color: #90EE90;">✨ Added</span>
19 | 
20 | *   Optional automatic playback of generated audio via `play: true` parameter.
21 | *   Configurable audio player command via `AUDIO_PLAYER_COMMAND` environment variable (defaults to `xdg-open`).
22 | *   Server now saves audio to `output/` directory and returns file path instead of base64 data.
23 | 
24 | ## <span style="color: #FFA07A;">[0.1.0] - 2025-04-05</span>
25 | 
26 | ### <span style="color: #90EE90;">✨ Added</span>
27 | 
28 | *   Initial Blabber-MCP server setup.
29 | *   `text_to_speech` tool using OpenAI TTS API.
30 | *   Support for selecting voice, model, and response format.
31 | *   Requires `OPENAI_API_KEY` environment variable.
32 | *   Basic project structure (`README.md`, `LICENSE`, `CHANGELOG.md`).
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 |   ErrorCode,
 10 | } from "@modelcontextprotocol/sdk/types.js";
 11 | import OpenAI from "openai";
 12 | import { APIError } from "openai/error.js";
 13 | import fs from 'fs';
 14 | import path from 'path';
 15 | import { fileURLToPath } from 'url';
 16 | import { exec } from 'child_process';
 17 | 
 18 | // --- Configuration ---
 19 | const API_KEY = process.env.OPENAI_API_KEY;
 20 | if (!API_KEY) {
 21 |   console.error("Error: OPENAI_API_KEY environment variable is not set.");
 22 |   process.exit(1);
 23 | }
 24 | 
 25 | const AUDIO_PLAYER_COMMAND = process.env.AUDIO_PLAYER_COMMAND || 'xdg-open';
 26 | 
 27 | // Define allowed voices
 28 | const ALLOWED_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] as const;
 29 | type AllowedVoice = typeof ALLOWED_VOICES[number];
 30 | 
 31 | // Read default voice from env var, validate, and set default
 32 | let DEFAULT_VOICE: AllowedVoice = "nova"; 
 33 | const configuredDefaultVoice = process.env.DEFAULT_TTS_VOICE;
 34 | if (configuredDefaultVoice && (ALLOWED_VOICES as readonly string[]).includes(configuredDefaultVoice)) {
 35 |     DEFAULT_VOICE = configuredDefaultVoice as AllowedVoice;
 36 |     console.error(`Using configured default voice: ${DEFAULT_VOICE}`);
 37 | } else if (configuredDefaultVoice) {
 38 |     console.error(`Warning: Invalid DEFAULT_TTS_VOICE "${configuredDefaultVoice}" provided. Using default "alloy".`);
 39 | } else {
 40 |     console.error(`Using default voice: ${DEFAULT_VOICE}`);
 41 | }
 42 | 
 43 | 
 44 | const __filename = fileURLToPath(import.meta.url);
 45 | const __dirname = path.dirname(__filename);
 46 | const OUTPUT_DIR = path.resolve(__dirname, '..', 'output');
 47 | 
 48 | const openai = new OpenAI({
 49 |   apiKey: API_KEY,
 50 | });
 51 | 
 52 | // --- MCP Server Setup ---
 53 | const server = new Server(
 54 |   {
 55 |     name: "@pink/pixel/blabber-mcp",
 56 |     version: "0.1.2", 
 57 |   },
 58 |   {
 59 |     capabilities: {
 60 |       tools: {},
 61 |     },
 62 |   }
 63 | );
 64 | 
 65 | // --- Tool Definition ---
 66 | const TEXT_TO_SPEECH_TOOL_NAME = "text_to_speech";
 67 | 
 68 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 69 |   return {
 70 |     tools: [
 71 |       {
 72 |         name: TEXT_TO_SPEECH_TOOL_NAME,
 73 |         description: `Converts text into spoken audio using OpenAI TTS (default voice: ${DEFAULT_VOICE}), saves it to a file, and optionally plays it.`, // Updated description
 74 |         inputSchema: {
 75 |           type: "object",
 76 |           properties: {
 77 |             input: {
 78 |               type: "string",
 79 |               description: "The text to synthesize into speech.",
 80 |             },
 81 |             voice: {
 82 |               type: "string",
 83 |               description: `Optional: The voice to use. Overrides the configured default (${DEFAULT_VOICE}).`,
 84 |               enum: [...ALLOWED_VOICES], // Use the defined constant
 85 |             },
 86 |             model: {
 87 |               type: "string",
 88 |               description: "The TTS model to use.",
 89 |               enum: ["tts-1", "tts-1-hd"],
 90 |               default: "tts-1",
 91 |             },
 92 |             response_format: {
 93 |               type: "string",
 94 |               description: "The format of the audio response.",
 95 |               enum: ["mp3", "opus", "aac", "flac"],
 96 |               default: "mp3",
 97 |             },
 98 |             play: {
 99 |               type: "boolean",
100 |               description: "Whether to automatically play the generated audio file.",
101 |               default: false,
102 |             }
103 |           },
104 |           required: ["input"],
105 |         },
106 |       },
107 |     ],
108 |   };
109 | });
110 | 
111 | // --- Tool Implementation ---
112 | 
113 | type TextToSpeechArgs = {
114 |   input: string;
115 |   voice?: AllowedVoice; // Use the specific type
116 |   model?: "tts-1" | "tts-1-hd";
117 |   response_format?: "mp3" | "opus" | "aac" | "flac";
118 |   play?: boolean;
119 | };
120 | 
121 | // Updated type guard
122 | function isValidTextToSpeechArgs(args: any): args is TextToSpeechArgs {
123 |   return (
124 |     typeof args === "object" &&
125 |     args !== null &&
126 |     typeof args.input === "string" &&
127 |     (args.voice === undefined || (ALLOWED_VOICES as readonly string[]).includes(args.voice)) && // Validate against allowed voices
128 |     (args.model === undefined || ["tts-1", "tts-1-hd"].includes(args.model)) &&
129 |     (args.response_format === undefined || ["mp3", "opus", "aac", "flac"].includes(args.response_format)) &&
130 |     (args.play === undefined || typeof args.play === 'boolean')
131 |   );
132 | }
133 | 
134 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
135 |   if (request.params.name !== TEXT_TO_SPEECH_TOOL_NAME) {
136 |     throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
137 |   }
138 | 
139 |   if (!isValidTextToSpeechArgs(request.params.arguments)) {
140 |     throw new McpError(ErrorCode.InvalidParams, "Invalid arguments for text_to_speech tool.");
141 |   }
142 | 
143 |   const {
144 |     input,
145 |     // Use voice from args if provided, otherwise use the configured DEFAULT_VOICE
146 |     voice = DEFAULT_VOICE,
147 |     model = "tts-1",
148 |     response_format = "mp3",
149 |     play = false,
150 |   } = request.params.arguments;
151 | 
152 |   // Ensure the final voice is valid (handles case where default might somehow be invalid, though unlikely with validation above)
153 |   const finalVoice: AllowedVoice = (ALLOWED_VOICES as readonly string[]).includes(voice) ? voice : DEFAULT_VOICE;
154 | 
155 | 
156 |   let playbackMessage = "";
157 | 
158 |   try {
159 |     if (!fs.existsSync(OUTPUT_DIR)) {
160 |       fs.mkdirSync(OUTPUT_DIR, { recursive: true });
161 |       console.error(`Created output directory: ${OUTPUT_DIR}`);
162 |     }
163 | 
164 |     console.error(`Generating speech with voice: ${finalVoice}`); // Log the voice being used
165 | 
166 |     const speechResponse = await openai.audio.speech.create({
167 |       model: model,
168 |       voice: finalVoice, // Use the validated final voice
169 |       input: input,
170 |       response_format: response_format,
171 |     });
172 | 
173 |     const audioBuffer = Buffer.from(await speechResponse.arrayBuffer());
174 |     const timestamp = Date.now();
175 |     const filename = `speech_${timestamp}.${response_format}`;
176 |     const filePath = path.join(OUTPUT_DIR, filename);
177 |     const relativeFilePath = path.relative(process.cwd(), filePath);
178 | 
179 |     fs.writeFileSync(filePath, audioBuffer);
180 |     console.error(`Audio saved to: ${filePath}`);
181 | 
182 |     if (play) {
183 |       const command = `${AUDIO_PLAYER_COMMAND} "${filePath}"`;
184 |       console.error(`Attempting to play audio with command: ${command}`);
185 |       exec(command, (error, stdout, stderr) => {
186 |         if (error) console.error(`Playback Error: ${error.message}`);
187 |         if (stderr) console.error(`Playback Stderr: ${stderr}`);
188 |         if (stdout) console.error(`Playback stdout: ${stdout}`);
189 |       });
190 |       playbackMessage = ` Playback initiated using command: ${AUDIO_PLAYER_COMMAND}.`;
191 |     }
192 | 
193 |     return {
194 |       content: [
195 |         {
196 |           type: "text",
197 |           text: JSON.stringify({
198 |             message: `Audio saved successfully.${playbackMessage}`,
199 |             filePath: relativeFilePath,
200 |             format: response_format,
201 |             voiceUsed: finalVoice, // Inform client which voice was actually used
202 |           }),
203 |           mimeType: "application/json",
204 |         },
205 |       ],
206 |     };
207 |   } catch (error) {
208 |     let errorMessage = "Failed to generate speech.";
209 |     if (error instanceof APIError) {
210 |       errorMessage = `OpenAI API Error (${error.status}): ${error.message}`;
211 |     } else if (error instanceof Error) {
212 |       errorMessage = error.message;
213 |     }
214 |     console.error(`[${TEXT_TO_SPEECH_TOOL_NAME} Error]`, errorMessage, error);
215 |     return {
216 |         content: [{ type: "text", text: errorMessage }],
217 |         isError: true
218 |     }
219 |   }
220 | });
221 | 
222 | // --- Server Start ---
223 | async function main() {
224 |   const transport = new StdioServerTransport();
225 |   server.onerror = (error) => console.error("[MCP Error]", error);
226 |   process.on('SIGINT', async () => {
227 |       console.error("Received SIGINT, shutting down server...");
228 |       await server.close();
229 |       process.exit(0);
230 |   });
231 |   await server.connect(transport);
232 |   console.error("OpenAI TTS MCP server running on stdio");
233 | }
234 | 
235 | main().catch((error) => {
236 |   console.error("Server failed to start:", error);
237 |   process.exit(1);
238 | });
239 | 
```