# Directory Structure
```
├── .gitignore
├── Dockerfile
├── package.json
├── README.md
├── smithery.yaml
├── src
│ └── server.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | dist/
3 | .env
4 | *.log
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | [](https://mseep.ai/app/phialsbasement-zonos-tts-mcp)
2 |
3 | # Zonos MCP Integration
4 | [](https://smithery.ai/server/@PhialsBasement/zonos-tts-mcp)
5 |
6 | A Model Context Protocol integration for Zonos TTS, allowing Claude to generate speech directly.
7 |
8 | ## Setup
9 |
10 | ### Installing via Smithery
11 |
12 | To install Zonos TTS Integration for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@PhialsBasement/zonos-tts-mcp):
13 |
14 | ```bash
15 | npx -y @smithery/cli install @PhialsBasement/zonos-tts-mcp --client claude
16 | ```
17 |
18 | ### Manual installation
19 |
20 | 1. Make sure you have Zonos running with our API implementation ([PhialsBasement/zonos-api](https://github.com/PhialsBasement/Zonos-API))
21 |
22 | 2. Install dependencies:
23 | ```bash
24 | npm install @modelcontextprotocol/sdk axios
25 | ```
26 |
27 | 3. Configure PulseAudio access:
28 | ```bash
29 | # Your pulse audio should be properly configured for audio playback
30 | # The MCP server will automatically try to connect to your pulse server
31 | ```
32 |
33 | 4. Build the MCP server:
34 | ```bash
35 | npm run build
36 | # This will create the dist folder with the compiled server
37 | ```
38 |
39 | 5. Add to Claude's config file:
40 | Edit your Claude config file (usually in `~/.config/claude/config.json`) and add this to the `mcpServers` section:
41 |
42 | ```json
43 | "zonos-tts": {
44 | "command": "node",
45 | "args": [
46 | "/path/to/your/zonos-mcp/dist/server.js"
47 | ]
48 | }
49 | ```
50 |
51 | Replace `/path/to/your/zonos-mcp` with the actual path where you installed the MCP server.
52 |
53 | ## Using with Claude
54 |
55 | Once configured, Claude automatically knows how to use the `speak_response` tool:
56 |
57 | ```python
58 | speak_response(
59 | text="Your text here",
60 | language="en-us", # optional, defaults to en-us
61 | emotion="happy" # optional: "neutral", "happy", "sad", "angry"
62 | )
63 | ```
64 |
65 | ## Features
66 |
67 | - Text-to-speech through Claude
68 | - Multiple emotions support
69 | - Multi-language support
70 | - Proper audio playback through PulseAudio
71 |
72 | ## Requirements
73 |
74 | - Node.js
75 | - PulseAudio setup
76 | - Running instance of Zonos API (PhialsBasement/zonos-api)
77 | - Working audio output device
78 |
79 | ## Notes
80 |
81 | - Make sure both the Zonos API server and this MCP server are running
82 | - Audio playback requires proper PulseAudio configuration
83 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "./dist",
11 | "rootDir": "./src"
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules", "dist"]
15 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-tts",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "build": "tsc",
7 | "start": "node dist/server.js",
8 | "dev": "tsc --watch"
9 | },
10 | "dependencies": {
11 | "@gradio/client": "^0.12.1",
12 | "@modelcontextprotocol/sdk": "^1.0.0",
13 | "axios": "^1.7.9",
14 | "zod": "^3.22.4"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^20.0.0",
18 | "typescript": "^5.0.0"
19 | }
20 | }
21 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required: []
9 | properties: {}
10 | commandFunction:
11 | # A function that produces the CLI command to start the MCP on stdio.
12 | |-
13 | (config) => ({ command: 'node', args: ['dist/server.js'] })
14 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use the official Node.js image with npm
3 | FROM node:22.12-alpine AS builder
4 |
5 | # Set working directory
6 | WORKDIR /app
7 |
8 | # Copy package.json and package-lock.json to the working directory
9 | COPY package.json tsconfig.json ./
10 |
11 | # Install dependencies
12 | RUN npm install --ignore-scripts
13 |
14 | # Copy the TypeScript source files
15 | COPY src/ ./src
16 |
17 | # Compile TypeScript to JavaScript
18 | RUN npx tsc
19 |
20 | # Use a smaller Node.js image for the final build
21 | FROM node:22-alpine AS release
22 |
23 | # Set the working directory
24 | WORKDIR /app
25 |
26 | # Copy the compiled files and node_modules from the builder stage
27 | COPY --from=builder /app/dist /app/dist
28 | COPY --from=builder /app/node_modules /app/node_modules
29 | COPY --from=builder /app/package.json /app/package.json
30 |
31 | # Set environment variables required for the server
32 | ENV NODE_ENV=production
33 |
34 | # Expose any required ports (replace 3000 with the actual port if different)
35 | EXPOSE 3000
36 |
37 | # Run the server
38 | ENTRYPOINT ["node", "dist/server.js"]
39 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Polyfill a minimal global 'window' for Node.js (do this before any other imports)
2 | if (typeof global.window === "undefined") {
3 | (global as any).window = {
4 | location: {
5 | protocol: "http:",
6 | hostname: "localhost",
7 | port: "8000",
8 | href: "http://localhost:8000/"
9 | }
10 | };
11 | }
12 |
13 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15 | import { z } from "zod";
16 | import { exec } from "child_process";
17 | import { promisify } from "util";
18 | import axios from 'axios';
19 |
20 | const execAsync = promisify(exec);
21 | const API_BASE_URL = 'http://localhost:8000';
22 |
23 | type Emotion = "neutral" | "happy" | "sad" | "angry";
24 |
25 | interface EmotionParameters {
26 | happiness: number;
27 | sadness: number;
28 | disgust: number;
29 | fear: number;
30 | surprise: number;
31 | anger: number;
32 | other: number;
33 | neutral: number;
34 | }
35 |
36 | interface ZonosRequestParams {
37 | text: string;
38 | language: string;
39 | emotion: Emotion;
40 | }
41 |
42 | interface EmotionMap {
43 | [key: string]: EmotionParameters;
44 | }
45 |
46 | class TTSServer {
47 | private mcp: McpServer;
48 | private readonly emotionMap: EmotionMap;
49 |
50 | constructor() {
51 | this.mcp = new McpServer({
52 | name: "TTS MCP Server",
53 | version: "1.0.0",
54 | });
55 |
56 | this.emotionMap = {
57 | neutral: {
58 | happiness: 0.2,
59 | sadness: 0.2,
60 | anger: 0.2,
61 | disgust: 0.05,
62 | fear: 0.05,
63 | surprise: 0.1,
64 | other: 0.1,
65 | neutral: 0.8,
66 | },
67 | happy: {
68 | happiness: 1,
69 | sadness: 0.05,
70 | anger: 0.05,
71 | disgust: 0.05,
72 | fear: 0.05,
73 | surprise: 0.2,
74 | other: 0.1,
75 | neutral: 0.2,
76 | },
77 | sad: {
78 | happiness: 0.05,
79 | sadness: 1,
80 | anger: 0.05,
81 | disgust: 0.2,
82 | fear: 0.2,
83 | surprise: 0.05,
84 | other: 0.1,
85 | neutral: 0.2,
86 | },
87 | angry: {
88 | happiness: 0.05,
89 | sadness: 0.2,
90 | anger: 1,
91 | disgust: 0.4,
92 | fear: 0.2,
93 | surprise: 0.2,
94 | other: 0.1,
95 | neutral: 0.1,
96 | },
97 | };
98 |
99 | this.setupTools();
100 | }
101 |
102 | private setupTools(): void {
103 | this.mcp.tool(
104 | "speak_response",
105 | {
106 | text: z.string(),
107 | language: z.string().default("en-us"),
108 | emotion: z.enum(["neutral", "happy", "sad", "angry"]).default("neutral"),
109 | },
110 | async ({ text, language, emotion }: ZonosRequestParams) => {
111 | try {
112 | const emotionParams = this.emotionMap[emotion];
113 | console.log(`Converting to speech: "${text}" with ${emotion} emotion`);
114 |
115 | // Use new OpenAI-style endpoint
116 | const response = await axios.post(`${API_BASE_URL}/v1/audio/speech`, {
117 | model: "Zyphra/Zonos-v0.1-transformer",
118 | input: text,
119 | language: language,
120 | emotion: emotionParams,
121 | speed: 1.0,
122 | response_format: "wav" // Using WAV for better compatibility
123 | }, {
124 | responseType: 'arraybuffer'
125 | });
126 |
127 | // Save the audio response to a temporary file
128 | const tempAudioPath = `/tmp/tts_output_${Date.now()}.wav`;
129 | const fs = await import('fs/promises');
130 | await fs.writeFile(tempAudioPath, response.data);
131 |
132 | // Play the audio
133 | await this.playAudio(tempAudioPath);
134 |
135 | // Clean up the temporary file
136 | await fs.unlink(tempAudioPath);
137 |
138 | return {
139 | content: [
140 | {
141 | type: "text",
142 | text: `Successfully spoke: "${text}" with ${emotion} emotion`,
143 | },
144 | ],
145 | };
146 | } catch (error) {
147 | const errorMessage = error instanceof Error ? error.message : "Unknown error";
148 | console.error("TTS Error:", errorMessage);
149 | if (axios.isAxiosError(error) && error.response) {
150 | console.error("API Response:", error.response.data);
151 | }
152 | throw new Error(`TTS failed: ${errorMessage}`);
153 | }
154 | }
155 | );
156 | }
157 |
158 | private async playAudio(audioPath: string): Promise<void> {
159 | try {
160 | console.log("Playing audio from:", audioPath);
161 |
162 | switch (process.platform) {
163 | case "darwin":
164 | await execAsync(`afplay ${audioPath}`);
165 | break;
166 | case "linux":
167 | // Try paplay for PulseAudio
168 | const XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR || '/run/user/1000';
169 | const env = {
170 | ...process.env,
171 | PULSE_SERVER: `unix:${XDG_RUNTIME_DIR}/pulse/native`,
172 | PULSE_COOKIE: `${process.env.HOME}/.config/pulse/cookie`
173 | };
174 | await execAsync(`paplay ${audioPath}`, { env });
175 | break;
176 | case "win32":
177 | await execAsync(
178 | `powershell -c (New-Object Media.SoundPlayer '${audioPath}').PlaySync()`
179 | );
180 | break;
181 | default:
182 | throw new Error(`Unsupported platform: ${process.platform}`);
183 | }
184 | } catch (error) {
185 | const errorMessage = error instanceof Error ? error.message : "Unknown error";
186 | console.error("Audio playback error:", errorMessage);
187 | throw new Error(`Audio playback failed: ${errorMessage}`);
188 | }
189 | }
190 |
191 | public async start(): Promise<void> {
192 | const transport = new StdioServerTransport();
193 | await this.mcp.connect(transport);
194 | }
195 | }
196 |
197 | const server = new TTSServer();
198 | await server.start();
199 |
```