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 +88 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +327 -0
- package/package.json +41 -0
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)
|
package/dist/index.d.ts
ADDED
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
|
+
}
|