# 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 | [](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 |
```