luvvoice-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ export interface TTSParams {
2
+ text: string;
3
+ voice_id: string;
4
+ rate?: number;
5
+ pitch?: number;
6
+ volume?: number;
7
+ }
8
+ export interface TTSResponse {
9
+ success: boolean;
10
+ audio_url?: string;
11
+ audio_data?: string;
12
+ message?: string;
13
+ credits_consumed: number;
14
+ credits_remaining: number;
15
+ duration_ms?: number;
16
+ error?: string;
17
+ }
18
+ export interface Voice {
19
+ voice_id: string;
20
+ name: string;
21
+ gender: string;
22
+ language: string;
23
+ language_name: string;
24
+ }
25
+ export interface VoicesResponse {
26
+ success: boolean;
27
+ voices: Voice[];
28
+ total_voices: number;
29
+ languages_supported: number;
30
+ }
31
+ export declare class LuvVoiceClient {
32
+ private token;
33
+ private baseUrl;
34
+ constructor(token: string, baseUrl?: string);
35
+ textToSpeech(params: TTSParams): Promise<TTSResponse>;
36
+ listVoices(): Promise<VoicesResponse>;
37
+ }
@@ -0,0 +1,34 @@
1
+ const DEFAULT_BASE_URL = 'https://luvvoice.com/api/v1';
2
+ export class LuvVoiceClient {
3
+ token;
4
+ baseUrl;
5
+ constructor(token, baseUrl) {
6
+ this.token = token;
7
+ this.baseUrl = baseUrl || DEFAULT_BASE_URL;
8
+ }
9
+ async textToSpeech(params) {
10
+ const res = await fetch(`${this.baseUrl}/text-to-speech`, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Authorization': `Bearer ${this.token}`,
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: JSON.stringify(params),
17
+ });
18
+ if (!res.ok && res.status !== 402) {
19
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
20
+ }
21
+ return res.json();
22
+ }
23
+ async listVoices() {
24
+ const res = await fetch(`${this.baseUrl}/text-to-speech?action=voices`, {
25
+ headers: {
26
+ 'Authorization': `Bearer ${this.token}`,
27
+ },
28
+ });
29
+ if (!res.ok) {
30
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
31
+ }
32
+ return res.json();
33
+ }
34
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createServer } from './server.js';
4
+ const args = process.argv.slice(2);
5
+ if (args.includes('--http')) {
6
+ startHttpServer();
7
+ }
8
+ else {
9
+ startStdioServer();
10
+ }
11
+ async function startStdioServer() {
12
+ const server = createServer();
13
+ const transport = new StdioServerTransport();
14
+ await server.connect(transport);
15
+ console.error('LuvVoice MCP Server running on stdio');
16
+ }
17
+ async function startHttpServer() {
18
+ const { default: express } = await import('express');
19
+ const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
20
+ const { randomUUID } = await import('node:crypto');
21
+ const app = express();
22
+ app.use(express.json());
23
+ // Stateful session management
24
+ const sessions = new Map();
25
+ app.post('/mcp', async (req, res) => {
26
+ const sessionId = req.headers['mcp-session-id'];
27
+ // Existing session
28
+ if (sessionId && sessions.has(sessionId)) {
29
+ const session = sessions.get(sessionId);
30
+ await session.transport.handleRequest(req, res, req.body);
31
+ return;
32
+ }
33
+ // New session
34
+ const server = createServer();
35
+ const transport = new StreamableHTTPServerTransport({
36
+ sessionIdGenerator: () => randomUUID(),
37
+ });
38
+ await server.connect(transport);
39
+ // Store session after connection (transport now has sessionId)
40
+ const newSessionId = transport.sessionId;
41
+ if (newSessionId) {
42
+ sessions.set(newSessionId, { server, transport });
43
+ }
44
+ await transport.handleRequest(req, res, req.body);
45
+ });
46
+ app.get('/mcp', async (req, res) => {
47
+ const sessionId = req.headers['mcp-session-id'];
48
+ if (sessionId && sessions.has(sessionId)) {
49
+ const session = sessions.get(sessionId);
50
+ await session.transport.handleRequest(req, res);
51
+ return;
52
+ }
53
+ res.status(400).json({ error: 'Invalid or missing session. Send an initialize request first via POST.' });
54
+ });
55
+ app.delete('/mcp', async (req, res) => {
56
+ const sessionId = req.headers['mcp-session-id'];
57
+ if (sessionId && sessions.has(sessionId)) {
58
+ const session = sessions.get(sessionId);
59
+ await session.transport.handleRequest(req, res);
60
+ sessions.delete(sessionId);
61
+ return;
62
+ }
63
+ res.status(400).json({ error: 'Invalid or missing session.' });
64
+ });
65
+ const port = parseInt(process.env.PORT || '3000', 10);
66
+ app.listen(port, () => {
67
+ console.error(`LuvVoice MCP Server (Streamable HTTP) listening on http://localhost:${port}/mcp`);
68
+ });
69
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,259 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { LuvVoiceClient } from './api-client.js';
4
+ export function createServer() {
5
+ const token = process.env.LUVVOICE_API_TOKEN;
6
+ if (!token) {
7
+ console.error('Error: LUVVOICE_API_TOKEN environment variable is required.');
8
+ console.error('Get your token at: https://luvvoice.com/dashboard/api-tokens');
9
+ process.exit(1);
10
+ }
11
+ const client = new LuvVoiceClient(token, process.env.LUVVOICE_API_BASE_URL);
12
+ const server = new McpServer({
13
+ name: 'luvvoice',
14
+ version: '1.0.0',
15
+ });
16
+ // --- Tools ---
17
+ server.registerTool('text_to_speech', {
18
+ title: 'Text to Speech',
19
+ description: 'Convert text into natural-sounding speech using AI voices. ' +
20
+ 'Supports 70+ languages and 200+ voices. Returns an audio URL and/or base64-encoded MP3 audio data, ' +
21
+ 'along with credits consumed and remaining.',
22
+ inputSchema: {
23
+ text: z
24
+ .string()
25
+ .max(20000)
26
+ .describe('The text to convert to speech (max 20,000 characters)'),
27
+ voice_id: z
28
+ .string()
29
+ .describe('Voice ID, e.g. "voice-001" (Jenny, English Female). Use the list_voices tool to discover available voices.'),
30
+ rate: z
31
+ .number()
32
+ .min(-50)
33
+ .max(50)
34
+ .optional()
35
+ .describe('Speech rate/speed (-50 to 50, default 0). Positive = faster, negative = slower.'),
36
+ pitch: z
37
+ .number()
38
+ .min(-50)
39
+ .max(50)
40
+ .optional()
41
+ .describe('Speech pitch (-50 to 50, default 0). Positive = higher, negative = lower.'),
42
+ volume: z
43
+ .number()
44
+ .min(-50)
45
+ .max(50)
46
+ .optional()
47
+ .describe('Speech volume (-50 to 50, default 0). Positive = louder, negative = quieter.'),
48
+ },
49
+ }, async ({ text, voice_id, rate, pitch, volume }) => {
50
+ try {
51
+ const result = await client.textToSpeech({
52
+ text,
53
+ voice_id,
54
+ rate,
55
+ pitch,
56
+ volume,
57
+ });
58
+ if (!result.success) {
59
+ return {
60
+ content: [
61
+ {
62
+ type: 'text',
63
+ text: `Error: ${result.error || result.message || 'Text-to-speech conversion failed'}`,
64
+ },
65
+ ],
66
+ isError: true,
67
+ };
68
+ }
69
+ const content = [];
70
+ if (result.audio_data) {
71
+ content.push({
72
+ type: 'audio',
73
+ data: result.audio_data,
74
+ mimeType: 'audio/mpeg',
75
+ });
76
+ }
77
+ let summary = '';
78
+ if (result.audio_url) {
79
+ summary += `Audio URL: ${result.audio_url}\n`;
80
+ }
81
+ summary += `Credits consumed: ${result.credits_consumed} | Remaining: ${result.credits_remaining}`;
82
+ if (result.duration_ms) {
83
+ summary += ` | Processing: ${result.duration_ms}ms`;
84
+ }
85
+ content.push({ type: 'text', text: summary });
86
+ return { content };
87
+ }
88
+ catch (err) {
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text',
93
+ text: `Request failed: ${err instanceof Error ? err.message : String(err)}`,
94
+ },
95
+ ],
96
+ isError: true,
97
+ };
98
+ }
99
+ });
100
+ server.registerTool('list_voices', {
101
+ title: 'List Available Voices',
102
+ description: 'Get all available TTS voices. Optionally filter by language code (e.g. "en-US", "zh-CN", "ja-JP") ' +
103
+ 'or gender ("Female", "Male"). Returns voice IDs needed for text_to_speech.',
104
+ inputSchema: {
105
+ language: z
106
+ .string()
107
+ .optional()
108
+ .describe('Filter by language code, e.g. "en-US", "zh-CN", "ja-JP", "es-ES". Leave empty for all voices.'),
109
+ gender: z
110
+ .enum(['Female', 'Male'])
111
+ .optional()
112
+ .describe('Filter by gender: "Female" or "Male". Leave empty for all.'),
113
+ },
114
+ }, async ({ language, gender }) => {
115
+ try {
116
+ const result = await client.listVoices();
117
+ if (!result.success) {
118
+ return {
119
+ content: [{ type: 'text', text: 'Failed to retrieve voice list.' }],
120
+ isError: true,
121
+ };
122
+ }
123
+ let voices = result.voices;
124
+ if (language) {
125
+ voices = voices.filter((v) => v.language.toLowerCase().includes(language.toLowerCase()));
126
+ }
127
+ if (gender) {
128
+ voices = voices.filter((v) => v.gender === gender);
129
+ }
130
+ const header = `Found ${voices.length} voice(s)${language ? ` for "${language}"` : ''}${gender ? ` (${gender})` : ''} — ${result.total_voices} total voices across ${result.languages_supported} languages\n\n`;
131
+ const voiceList = voices
132
+ .map((v) => `${v.voice_id}: ${v.name} (${v.gender}) - ${v.language_name} [${v.language}]`)
133
+ .join('\n');
134
+ return {
135
+ content: [{ type: 'text', text: header + voiceList }],
136
+ };
137
+ }
138
+ catch (err) {
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Request failed: ${err instanceof Error ? err.message : String(err)}`,
144
+ },
145
+ ],
146
+ isError: true,
147
+ };
148
+ }
149
+ });
150
+ // --- Resources ---
151
+ server.registerResource('api-docs', 'luvvoice://api-docs', {
152
+ title: 'LuvVoice API Documentation',
153
+ description: 'Complete documentation for the LuvVoice Text-to-Speech API',
154
+ mimeType: 'text/markdown',
155
+ }, async (uri) => ({
156
+ contents: [
157
+ {
158
+ uri: uri.href,
159
+ text: API_DOCS,
160
+ },
161
+ ],
162
+ }));
163
+ server.registerResource('voices', 'luvvoice://voices', {
164
+ title: 'Available Voices',
165
+ description: 'Complete list of all available TTS voices with IDs, names, genders, and languages',
166
+ mimeType: 'application/json',
167
+ }, async (uri) => {
168
+ const result = await client.listVoices();
169
+ return {
170
+ contents: [
171
+ {
172
+ uri: uri.href,
173
+ text: JSON.stringify(result, null, 2),
174
+ },
175
+ ],
176
+ };
177
+ });
178
+ return server;
179
+ }
180
+ const API_DOCS = `# LuvVoice API Documentation
181
+
182
+ ## Overview
183
+ LuvVoice is an AI-powered text-to-speech service supporting 70+ languages and 200+ voices.
184
+
185
+ ## Base URL
186
+ \`https://luvvoice.com/api/v1/text-to-speech\`
187
+
188
+ ## Authentication
189
+ All requests require a Bearer token in the Authorization header:
190
+ \`Authorization: Bearer YOUR_API_TOKEN\`
191
+
192
+ Get your token at: https://luvvoice.com/dashboard/api-tokens
193
+ API access requires a Pro or Enterprise plan.
194
+
195
+ ## Endpoints
196
+
197
+ ### POST /text-to-speech
198
+ Convert text to speech.
199
+
200
+ **Request Body (JSON):**
201
+ | Parameter | Type | Required | Description |
202
+ |-----------|--------|----------|---------------------------------------|
203
+ | text | string | Yes | Text to convert (max 20,000 chars) |
204
+ | voice_id | string | Yes | Voice ID, e.g. "voice-001" |
205
+ | rate | number | No | Speech speed (-50 to 50, default 0) |
206
+ | pitch | number | No | Speech pitch (-50 to 50, default 0) |
207
+ | volume | number | No | Speech volume (-50 to 50, default 0) |
208
+
209
+ **Success Response (200):**
210
+ \`\`\`json
211
+ {
212
+ "success": true,
213
+ "audio_url": "https://cdn.luvvoice.com/audio/generated_123.mp3",
214
+ "audio_data": "<base64-encoded-mp3>",
215
+ "credits_consumed": 42,
216
+ "credits_remaining": 9958,
217
+ "duration_ms": 1234
218
+ }
219
+ \`\`\`
220
+
221
+ ### GET /text-to-speech?action=voices
222
+ Get all available voices.
223
+
224
+ **Response:**
225
+ \`\`\`json
226
+ {
227
+ "success": true,
228
+ "voices": [
229
+ { "voice_id": "voice-001", "name": "Jenny", "gender": "Female", "language": "en-US", "language_name": "English (United States)" }
230
+ ],
231
+ "total_voices": 290,
232
+ "languages_supported": 85
233
+ }
234
+ \`\`\`
235
+
236
+ ## Popular Voices
237
+ - voice-001: Jenny (Female, English US)
238
+ - voice-002: Guy (Male, English US)
239
+ - voice-050: Alvaro (Male, Spanish)
240
+ - voice-093: Xiaoxiao (Female, Chinese)
241
+ - voice-111: Katja (Female, German)
242
+ - voice-120: Nanami (Female, Japanese)
243
+
244
+ ## Rate Limits
245
+ - 10 requests per minute
246
+ - 5 requests per second (burst protection)
247
+ - 20 requests per 10 seconds
248
+ - Max 20,000 characters per request
249
+
250
+ ## Error Codes
251
+ | Code | Description |
252
+ |------|------------------------------------------|
253
+ | 400 | Bad Request - invalid parameters |
254
+ | 401 | Unauthorized - invalid API token |
255
+ | 402 | Payment Required - insufficient credits |
256
+ | 403 | Forbidden - not a Pro/Enterprise user |
257
+ | 429 | Too Many Requests - rate limit exceeded |
258
+ | 500 | Internal Server Error |
259
+ `;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "luvvoice-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for LuvVoice Text-to-Speech API - Convert text to natural-sounding speech with 200+ AI voices in 70+ languages",
5
+ "type": "module",
6
+ "bin": {
7
+ "luvvoice-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc && node -e \"const fs=require('fs');const f='dist/index.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f));fs.chmodSync(f,0o755)\"",
14
+ "dev": "tsc --watch",
15
+ "start": "node dist/index.js",
16
+ "start:http": "node dist/index.js --http"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "text-to-speech",
22
+ "tts",
23
+ "ai-voice",
24
+ "luvvoice"
25
+ ],
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.12.0",
29
+ "express": "^4.21.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/express": "^5.0.0",
34
+ "@types/node": "^22.0.0",
35
+ "typescript": "^5.7.0"
36
+ }
37
+ }