cronygen-mcp-server 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.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # CronyGen MCP Server
2
+
3
+ Model Context Protocol server for CronyGen Text-to-Speech.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @cronygen/mcp-server
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Set your API key:
14
+
15
+ ```bash
16
+ export CRONYGEN_API_KEY="crony_sk_your_api_key"
17
+ ```
18
+
19
+ ## Usage with Claude Desktop
20
+
21
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "cronygen": {
27
+ "command": "cronygen-mcp",
28
+ "env": {
29
+ "CRONYGEN_API_KEY": "crony_sk_your_api_key"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Available Tools
37
+
38
+ ### generate_speech
39
+
40
+ Generate speech audio from text. Supports default and cloned voices.
41
+
42
+ ```
43
+ Generate speech for: "Hello, this is a test of CronyGen TTS."
44
+ ```
45
+
46
+ ### list_voices
47
+
48
+ List available default and cloned voices.
49
+
50
+ ```
51
+ List all available voices for TTS.
52
+ ```
53
+
54
+ ### get_generation
55
+
56
+ Get details about a previous generation.
57
+
58
+ ### list_generations
59
+
60
+ List previous TTS generations with pagination.
61
+
62
+ ### delete_generation
63
+
64
+ Delete a TTS generation by ID.
65
+
66
+ ### get_billing
67
+
68
+ Check credits balance and usage.
69
+
70
+ ### get_user
71
+
72
+ Get current authenticated user profile.
73
+
74
+ ## Examples
75
+
76
+ ```
77
+ User: Generate speech saying "Welcome to CronyGen" and save it to ~/Desktop/welcome.wav
78
+
79
+ Claude: I'll generate that speech for you using CronyGen TTS.
80
+
81
+ [Uses generate_speech tool]
82
+
83
+ Audio generated and saved to /Users/you/Desktop/welcome.wav (duration: 2.3s)
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ Full documentation at [zingotron.com/docs/mcp](https://zingotron.com/docs/mcp)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CronyGen MCP Server
4
+ *
5
+ * Model Context Protocol server exposing CronyGen TTS as tools.
6
+ * Allows AI agents to generate speech, manage voices, and check billing.
7
+ */
8
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CronyGen MCP Server
4
+ *
5
+ * Model Context Protocol server exposing CronyGen TTS as tools.
6
+ * Allows AI agents to generate speech, manage voices, and check billing.
7
+ */
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+ const API_BASE = process.env.CRONYGEN_API_URL || "https://api.zingotron.com";
14
+ const API_KEY = process.env.CRONYGEN_API_KEY || "";
15
+ // ============================================================================
16
+ // API Client
17
+ // ============================================================================
18
+ async function apiRequest(method, endpoint, body) {
19
+ const response = await fetch(`${API_BASE}${endpoint}`, {
20
+ method,
21
+ headers: {
22
+ Authorization: API_KEY.startsWith('crony_sk_') ? API_KEY : `Bearer ${API_KEY}`,
23
+ "Content-Type": "application/json",
24
+ },
25
+ body: body ? JSON.stringify(body) : undefined,
26
+ });
27
+ const data = (await response.json());
28
+ if (!response.ok || !data.success) {
29
+ throw new Error(data.error?.message || `API request failed: ${response.status}`);
30
+ }
31
+ return data.data;
32
+ }
33
+ async function pollGeneration(generationId, timeoutMs = 300000) {
34
+ const startTime = Date.now();
35
+ while (Date.now() - startTime < timeoutMs) {
36
+ const result = (await apiRequest("GET", `/v1/tts/jobs/${generationId}`));
37
+ if (result.status === "completed") {
38
+ return result;
39
+ }
40
+ if (result.status === "failed") {
41
+ throw new Error(result.error_message || "Generation failed");
42
+ }
43
+ await new Promise((resolve) => setTimeout(resolve, 1000));
44
+ }
45
+ throw new Error("Generation timed out");
46
+ }
47
+ // Helper: detect if a voice string looks like a UUID (cloned voice)
48
+ function isUUID(str) {
49
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
50
+ }
51
+ // ============================================================================
52
+ // Tool Definitions
53
+ // ============================================================================
54
+ const TOOLS = [
55
+ {
56
+ name: "generate_speech",
57
+ description: "Generate speech audio from text using CronyGen TTS. Returns the URL of the generated audio file.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ text: {
62
+ type: "string",
63
+ description: "The text to convert to speech",
64
+ },
65
+ voice: {
66
+ type: "string",
67
+ description: "Voice ID or slug. Use a slug like 'en-us-aria' for default voices, or a UUID for cloned voices.",
68
+ default: "en-us-aria",
69
+ },
70
+ output_path: {
71
+ type: "string",
72
+ description: "Optional local path to save the audio file",
73
+ },
74
+ },
75
+ required: ["text"],
76
+ },
77
+ },
78
+ {
79
+ name: "list_voices",
80
+ description: "List available voices including default system voices and user's cloned voices",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ type: {
85
+ type: "string",
86
+ enum: ["all", "default", "cloned"],
87
+ description: "Filter by voice type",
88
+ default: "all",
89
+ },
90
+ },
91
+ },
92
+ },
93
+ {
94
+ name: "get_generation",
95
+ description: "Get details about a previous TTS generation",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ generation_id: {
100
+ type: "string",
101
+ description: "The ID of the generation to retrieve",
102
+ },
103
+ },
104
+ required: ["generation_id"],
105
+ },
106
+ },
107
+ {
108
+ name: "list_generations",
109
+ description: "List previous TTS generations with pagination",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ page: {
114
+ type: "number",
115
+ description: "Page number (default: 1)",
116
+ default: 1,
117
+ },
118
+ limit: {
119
+ type: "number",
120
+ description: "Results per page (default: 20)",
121
+ default: 20,
122
+ },
123
+ },
124
+ },
125
+ },
126
+ {
127
+ name: "delete_generation",
128
+ description: "Delete a TTS generation by ID",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ generation_id: {
133
+ type: "string",
134
+ description: "The ID of the generation to delete",
135
+ },
136
+ },
137
+ required: ["generation_id"],
138
+ },
139
+ },
140
+ {
141
+ name: "get_billing",
142
+ description: "Get current billing information including credits balance and usage",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {},
146
+ },
147
+ },
148
+ {
149
+ name: "get_user",
150
+ description: "Get current authenticated user profile",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {},
154
+ },
155
+ },
156
+ ];
157
+ // ============================================================================
158
+ // Tool Handlers
159
+ // ============================================================================
160
+ async function handleGenerateSpeech(args) {
161
+ if (!args.text) {
162
+ throw new Error("text is required");
163
+ }
164
+ const voice = args.voice || "en-us-aria";
165
+ // Start generation
166
+ const result = (await apiRequest("POST", "/v1/tts/generate", {
167
+ text: args.text,
168
+ voice,
169
+ voice_type: isUUID(voice) ? "cloned" : "default",
170
+ }));
171
+ // Wait for completion
172
+ const generation = (await pollGeneration(result.generation_id));
173
+ // Optionally save to file
174
+ if (args.output_path) {
175
+ const audioResponse = await fetch(generation.output_audio_url);
176
+ const audioBuffer = await audioResponse.arrayBuffer();
177
+ const outputPath = path.resolve(args.output_path);
178
+ fs.writeFileSync(outputPath, Buffer.from(audioBuffer));
179
+ return `Audio generated and saved to ${outputPath} (duration: ${generation.output_duration_seconds.toFixed(1)}s)`;
180
+ }
181
+ return `Audio generated: ${generation.output_audio_url} (duration: ${generation.output_duration_seconds.toFixed(1)}s)`;
182
+ }
183
+ async function handleListVoices(args) {
184
+ const type = args.type || "all";
185
+ const voices = [];
186
+ if (type === "all" || type === "default") {
187
+ const defaults = (await apiRequest("GET", "/v1/voices/defaults"));
188
+ voices.push("## Default Voices\n");
189
+ if (Array.isArray(defaults)) {
190
+ for (const v of defaults) {
191
+ voices.push(`- **${v.name}** (${v.slug}) - ${v.gender}, ${v.accent}`);
192
+ }
193
+ }
194
+ }
195
+ if (type === "all" || type === "cloned") {
196
+ const response = (await apiRequest("GET", "/v1/voices"));
197
+ // API may return array directly or wrapped in {data: [...]}
198
+ const cloned = Array.isArray(response) ? response : response.data || [];
199
+ if (cloned.length > 0) {
200
+ voices.push("\n## Your Cloned Voices\n");
201
+ for (const v of cloned) {
202
+ voices.push(`- **${v.name}** (${v.id}) - ${v.status}`);
203
+ }
204
+ }
205
+ }
206
+ return voices.join("\n") || "No voices found.";
207
+ }
208
+ async function handleGetGeneration(args) {
209
+ const gen = (await apiRequest("GET", `/v1/tts/jobs/${args.generation_id}`));
210
+ return `Generation ${gen.id}:
211
+ - Status: ${gen.status}
212
+ - Input: ${gen.input_text_length} characters
213
+ - Duration: ${gen.output_duration_seconds?.toFixed(1) || "N/A"}s
214
+ - Cost: ${gen.cost_credits || "N/A"} credits
215
+ - Audio: ${gen.output_audio_url || "Not ready"}
216
+ - Created: ${gen.created_at}`;
217
+ }
218
+ async function handleListGenerations(args) {
219
+ const page = args.page || 1;
220
+ const limit = args.limit || 20;
221
+ const response = (await apiRequest("GET", `/v1/tts/generations?page=${page}&limit=${limit}`));
222
+ const gens = Array.isArray(response) ? response : [];
223
+ if (gens.length === 0) {
224
+ return "No generations found.";
225
+ }
226
+ const lines = [`## Generations (page ${page})\n`];
227
+ for (const gen of gens) {
228
+ lines.push(`- **${gen.id}** — ${gen.status} | ${gen.input_text_length} chars | ${gen.output_duration_seconds?.toFixed(1) || "?"}s | ${gen.created_at}`);
229
+ }
230
+ return lines.join("\n");
231
+ }
232
+ async function handleDeleteGeneration(args) {
233
+ await apiRequest("DELETE", `/v1/tts/generations/${args.generation_id}`);
234
+ return `Generation ${args.generation_id} deleted.`;
235
+ }
236
+ async function handleGetBilling() {
237
+ const billing = (await apiRequest("GET", "/v1/billing"));
238
+ return `Billing Account:
239
+ - Credits Balance: ${billing.credits_balance.toFixed(2)}
240
+ - Tier: ${billing.tier}
241
+
242
+ This Month's Usage:
243
+ - Generations: ${billing.usage_this_month.generations}
244
+ - Credits Used: ${billing.usage_this_month.credits_used.toFixed(2)}
245
+ - Minutes Generated: ${billing.usage_this_month.minutes_generated.toFixed(1)}`;
246
+ }
247
+ async function handleGetUser() {
248
+ const user = (await apiRequest("GET", "/v1/me"));
249
+ return `User Profile:
250
+ - Email: ${user.email}
251
+ - Name: ${user.name || "N/A"}
252
+ - Tier: ${user.tier}
253
+ - Verified: ${user.email_verified}
254
+ - Created: ${user.created_at}`;
255
+ }
256
+ // ============================================================================
257
+ // Server Setup
258
+ // ============================================================================
259
+ const server = new Server({
260
+ name: "cronygen-mcp",
261
+ version: "0.1.0",
262
+ }, {
263
+ capabilities: {
264
+ tools: {},
265
+ },
266
+ });
267
+ // List available tools
268
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
269
+ return { tools: TOOLS };
270
+ });
271
+ // Handle tool calls
272
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
273
+ const { name, arguments: args } = request.params;
274
+ try {
275
+ let result;
276
+ switch (name) {
277
+ case "generate_speech":
278
+ result = await handleGenerateSpeech(args);
279
+ break;
280
+ case "list_voices":
281
+ result = await handleListVoices(args);
282
+ break;
283
+ case "get_generation":
284
+ result = await handleGetGeneration(args);
285
+ break;
286
+ case "list_generations":
287
+ result = await handleListGenerations(args);
288
+ break;
289
+ case "delete_generation":
290
+ result = await handleDeleteGeneration(args);
291
+ break;
292
+ case "get_billing":
293
+ result = await handleGetBilling();
294
+ break;
295
+ case "get_user":
296
+ result = await handleGetUser();
297
+ break;
298
+ default:
299
+ throw new Error(`Unknown tool: ${name}`);
300
+ }
301
+ return {
302
+ content: [{ type: "text", text: result }],
303
+ };
304
+ }
305
+ catch (error) {
306
+ return {
307
+ content: [
308
+ {
309
+ type: "text",
310
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
311
+ },
312
+ ],
313
+ isError: true,
314
+ };
315
+ }
316
+ });
317
+ // Start server
318
+ async function main() {
319
+ if (!API_KEY) {
320
+ console.error("Error: CRONYGEN_API_KEY environment variable is required");
321
+ process.exit(1);
322
+ }
323
+ const transport = new StdioServerTransport();
324
+ await server.connect(transport);
325
+ console.error("CronyGen MCP server running on stdio");
326
+ }
327
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "cronygen-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol server for CronyGen Text-to-Speech",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "cronygen-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch",
17
+ "prepublishOnly": "npm run build",
18
+ "start": "node dist/index.js"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "tts",
24
+ "text-to-speech",
25
+ "cronygen",
26
+ "ai",
27
+ "voice"
28
+ ],
29
+ "author": "CronyGen <support@zingotron.com>",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^0.5.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.10.0",
36
+ "typescript": "^5.3.3"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ }
41
+ }