@forgemeshlabs/voice-mcp 0.1.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/CHANGELOG.md +7 -0
- package/Dockerfile +9 -0
- package/README.md +76 -0
- package/SECURITY.md +5 -0
- package/glama.json +31 -0
- package/index.js +335 -0
- package/package.json +65 -0
- package/server.json +11 -0
package/CHANGELOG.md
ADDED
package/Dockerfile
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Voice MCP
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@forgemeshlabs/voice-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/@forgemeshlabs/voice-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://x402.org)
|
|
8
|
+
[](https://base.org)
|
|
9
|
+
|
|
10
|
+
Give Your Agent A Voice: x402 pay-per-call speech with 20 voices, 10 personas, 31 languages, granular speed and quality controls, OpenAI-shaped requests, and batch audio.
|
|
11
|
+
|
|
12
|
+
This MCP wraps `https://voice.forgemesh.io`, an x402 Voice API with standard voices, persona voices, OpenAI-shaped speech requests, 31 languages, speed controls, quality controls, and batch generation. Payments are made per call in USDC on Base.
|
|
13
|
+
|
|
14
|
+
## Voice Coverage
|
|
15
|
+
|
|
16
|
+
- 10 standard voices: `M1`-`M5`, `F1`-`F5`
|
|
17
|
+
- 10 persona voices: `Storyteller`, `Narrator`, `Announcer`, `Assistant`, `Urgent`, `Sage`, `Spark`, `Anchor`, `Velvet`, `Echo`
|
|
18
|
+
- 31 languages: `en`, `ko`, `ja`, `ar`, `bg`, `cs`, `da`, `de`, `el`, `es`, `et`, `fi`, `fr`, `hi`, `hr`, `hu`, `id`, `it`, `lt`, `lv`, `nl`, `pl`, `pt`, `ro`, `ru`, `sk`, `sl`, `sv`, `tr`, `uk`, `vi`
|
|
19
|
+
- Granular control: speed `0.7x`-`2.0x`, quality steps `1`-`100`, persona selection, OpenAI-shaped audio format requests, and batch generation for up to 20 texts
|
|
20
|
+
- Voice samples are generated on demand by the paid speech tools and returned as `audio_base64` WAV output
|
|
21
|
+
|
|
22
|
+
## Voice Samples
|
|
23
|
+
|
|
24
|
+
- [Assistant sample](https://voice.forgemesh.io/samples/_expressive/combo_assistant.wav)
|
|
25
|
+
- [Urgent sample](https://voice.forgemesh.io/samples/_expressive/combo_urgent.wav)
|
|
26
|
+
- [Narrator sample](https://voice.forgemesh.io/samples/_expressive/combo_narrator.wav)
|
|
27
|
+
- [Storyteller sample](https://voice.forgemesh.io/samples/_expressive/combo_storyteller.wav)
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Tool | Price | Purpose |
|
|
32
|
+
|------|-------|---------|
|
|
33
|
+
| `list_tts_voices` | Free | Voices, personas, languages, pricing |
|
|
34
|
+
| `speak_standard` | $0.001 / $0.003 | Standard voices |
|
|
35
|
+
| `speak_pro` | $0.003 / $0.006 | Speed and quality controls |
|
|
36
|
+
| `speak_persona` | $0.005 / $0.01 | Storyteller, Velvet, Narrator, Announcer, Assistant, Urgent, and more |
|
|
37
|
+
| `openai_speech` | $0.001 / $0.003 | OpenAI-shaped `/v1/audio/speech` request |
|
|
38
|
+
| `batch_speak` | $0.002 / $0.005 | Up to 20 texts per call |
|
|
39
|
+
|
|
40
|
+
Short prices apply to 1-500 characters. Long prices apply to 501-2000 characters.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @forgemeshlabs/voice-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## MCP Config
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"voice": {
|
|
54
|
+
"command": "voice-mcp",
|
|
55
|
+
"env": {
|
|
56
|
+
"WALLET_PRIVATE_KEY": "0x..."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Optional:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"X402_VOICE_BASE_URL": "https://voice.forgemesh.io",
|
|
68
|
+
"BASE_RPC_URL": "https://mainnet.base.org"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Notes
|
|
73
|
+
|
|
74
|
+
- Paid tools require a Base wallet private key with USDC.
|
|
75
|
+
- The server returns `audio_base64` for audio tools so MCP clients can store, play, or forward the WAV bytes.
|
|
76
|
+
- No API keys or subscriptions are required for the voice service itself.
|
package/SECURITY.md
ADDED
package/glama.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
|
3
|
+
"name": "voice-mcp",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Give Your Agent A Voice: x402 pay-per-call speech with 20 voices, 10 personas, 31 languages, granular speed and quality controls, OpenAI-shaped requests, and batch audio.",
|
|
6
|
+
"homepage": "https://voice.forgemesh.io",
|
|
7
|
+
"repository": "https://github.com/forgemeshlabs/voice-mcp",
|
|
8
|
+
"maintainers": [
|
|
9
|
+
"clawdbotworker"
|
|
10
|
+
],
|
|
11
|
+
"transport": {
|
|
12
|
+
"type": "stdio",
|
|
13
|
+
"command": "voice-mcp"
|
|
14
|
+
},
|
|
15
|
+
"env": {
|
|
16
|
+
"WALLET_PRIVATE_KEY": {
|
|
17
|
+
"description": "Base wallet private key used by the MCP client for paid x402 calls.",
|
|
18
|
+
"required": true
|
|
19
|
+
},
|
|
20
|
+
"X402_VOICE_BASE_URL": {
|
|
21
|
+
"description": "Optional Voice API base URL.",
|
|
22
|
+
"required": false,
|
|
23
|
+
"default": "https://voice.forgemesh.io"
|
|
24
|
+
},
|
|
25
|
+
"BASE_RPC_URL": {
|
|
26
|
+
"description": "Optional Base mainnet RPC URL.",
|
|
27
|
+
"required": false,
|
|
28
|
+
"default": "https://mainnet.base.org"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const { z } = require("zod");
|
|
7
|
+
const { x402Client, x402HTTPClient } = require("@x402/core/client");
|
|
8
|
+
const { ExactEvmScheme } = require("@x402/evm/exact/client");
|
|
9
|
+
const { toClientEvmSigner } = require("@x402/evm");
|
|
10
|
+
const { privateKeyToAccount } = require("viem/accounts");
|
|
11
|
+
const { createPublicClient, http } = require("viem");
|
|
12
|
+
const { base } = require("viem/chains");
|
|
13
|
+
|
|
14
|
+
const BASE_URL = (
|
|
15
|
+
process.env.X402_VOICE_BASE_URL ||
|
|
16
|
+
"https://voice.forgemesh.io"
|
|
17
|
+
).replace(/\/+$/, "");
|
|
18
|
+
const BASE_RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
|
|
19
|
+
|
|
20
|
+
const TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "list_tts_voices",
|
|
23
|
+
description: "List voices, persona voices, languages, prices, and character buckets. Free.",
|
|
24
|
+
inputSchema: { type: "object", properties: {} },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "speak_standard",
|
|
28
|
+
description: "Give an agent a standard voice using 10 voices and 31 languages. Costs $0.001 for <=500 chars or $0.003 for 501-2000 chars.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
text: { type: "string", description: "Text to synthesize, max 2000 characters" },
|
|
33
|
+
voice: { type: "string", description: "Standard voice: M1-M5 or F1-F5" },
|
|
34
|
+
lang: { type: "string", description: "Language code, default en" },
|
|
35
|
+
},
|
|
36
|
+
required: ["text"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "speak_pro",
|
|
41
|
+
description: "Generate tuned agent speech with speed and quality controls. Costs $0.003 for <=500 chars or $0.006 for 501-2000 chars.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
text: { type: "string", description: "Text to synthesize, max 2000 characters" },
|
|
46
|
+
voice: { type: "string", description: "Standard voice: M1-M5 or F1-F5" },
|
|
47
|
+
lang: { type: "string", description: "Language code, default en" },
|
|
48
|
+
speed: { type: "number", description: "Speech speed, 0.7-2.0" },
|
|
49
|
+
steps: { type: "integer", description: "Quality steps, 1-100" },
|
|
50
|
+
},
|
|
51
|
+
required: ["text"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "speak_persona",
|
|
56
|
+
description: "Give an agent a persona voice such as Storyteller, Velvet, Narrator, Announcer, Assistant, or Urgent. Costs $0.005 for <=500 chars or $0.01 for 501-2000 chars.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
text: { type: "string", description: "Text to synthesize, max 2000 characters" },
|
|
61
|
+
voice: { type: "string", description: "Persona voice name, default Storyteller" },
|
|
62
|
+
lang: { type: "string", description: "Language code, default en" },
|
|
63
|
+
speed: { type: "number", description: "Speech speed, 0.7-2.0" },
|
|
64
|
+
steps: { type: "integer", description: "Quality steps, 1-100" },
|
|
65
|
+
},
|
|
66
|
+
required: ["text"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "openai_speech",
|
|
71
|
+
description: "OpenAI-shaped speech request for agents already wired to /v1/audio/speech. Costs $0.001 for <=500 chars or $0.003 for 501-2000 chars.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
input: { type: "string", description: "Text to synthesize, max 2000 characters" },
|
|
76
|
+
voice: { type: "string", description: "Standard voice: M1-M5 or F1-F5" },
|
|
77
|
+
model: { type: "string", description: "Optional model field; service uses ForgeMesh Voice" },
|
|
78
|
+
response_format: { type: "string", description: "wav, flac, or ogg" },
|
|
79
|
+
},
|
|
80
|
+
required: ["input"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "batch_speak",
|
|
85
|
+
description: "Generate audio for up to 20 standard-voice texts in one paid call. Costs $0.002 for <=500 total chars or $0.005 for 501-2000 total chars.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
items: {
|
|
90
|
+
type: "array",
|
|
91
|
+
description: "Array of text items: { text, voice?, lang? }",
|
|
92
|
+
items: { type: "object" },
|
|
93
|
+
},
|
|
94
|
+
defaults: { type: "object", description: "Default standard voice and language" },
|
|
95
|
+
},
|
|
96
|
+
required: ["items"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const TOOL_SCHEMAS = {
|
|
102
|
+
list_tts_voices: {},
|
|
103
|
+
speak_standard: {
|
|
104
|
+
text: z.string().describe("Text to synthesize, max 2000 characters"),
|
|
105
|
+
voice: z.string().optional().describe("Standard voice: M1-M5 or F1-F5"),
|
|
106
|
+
lang: z.string().optional().describe("Language code, default en"),
|
|
107
|
+
},
|
|
108
|
+
speak_pro: {
|
|
109
|
+
text: z.string().describe("Text to synthesize, max 2000 characters"),
|
|
110
|
+
voice: z.string().optional().describe("Standard voice: M1-M5 or F1-F5"),
|
|
111
|
+
lang: z.string().optional().describe("Language code, default en"),
|
|
112
|
+
speed: z.number().optional().describe("Speech speed, 0.7-2.0"),
|
|
113
|
+
steps: z.number().int().optional().describe("Quality steps, 1-100"),
|
|
114
|
+
},
|
|
115
|
+
speak_persona: {
|
|
116
|
+
text: z.string().describe("Text to synthesize, max 2000 characters"),
|
|
117
|
+
voice: z.string().optional().describe("Persona voice name, default Storyteller"),
|
|
118
|
+
lang: z.string().optional().describe("Language code, default en"),
|
|
119
|
+
speed: z.number().optional().describe("Speech speed, 0.7-2.0"),
|
|
120
|
+
steps: z.number().int().optional().describe("Quality steps, 1-100"),
|
|
121
|
+
},
|
|
122
|
+
openai_speech: {
|
|
123
|
+
input: z.string().describe("Text to synthesize, max 2000 characters"),
|
|
124
|
+
voice: z.string().optional().describe("Standard voice: M1-M5 or F1-F5"),
|
|
125
|
+
model: z.string().optional().describe("Optional model field; service uses ForgeMesh Voice"),
|
|
126
|
+
response_format: z.string().optional().describe("wav, flac, or ogg"),
|
|
127
|
+
},
|
|
128
|
+
batch_speak: {
|
|
129
|
+
items: z.array(z.object({
|
|
130
|
+
text: z.string(),
|
|
131
|
+
voice: z.string().optional(),
|
|
132
|
+
lang: z.string().optional(),
|
|
133
|
+
})).min(1).max(20).describe("Array of text items"),
|
|
134
|
+
defaults: z.object({
|
|
135
|
+
voice: z.string().optional(),
|
|
136
|
+
lang: z.string().optional(),
|
|
137
|
+
}).optional().describe("Default standard voice and language"),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function pickBucketEndpoint(shortPath, longPath, length) {
|
|
142
|
+
if (length > 2000) throw new Error("Text is over the 2000 character maximum");
|
|
143
|
+
return length > 500 ? longPath : shortPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function requireWalletClient() {
|
|
147
|
+
const key = process.env.WALLET_PRIVATE_KEY;
|
|
148
|
+
if (!key) throw new Error("WALLET_PRIVATE_KEY required for paid voice tools");
|
|
149
|
+
const pk = key.startsWith("0x") ? key : "0x" + key;
|
|
150
|
+
const account = privateKeyToAccount(pk);
|
|
151
|
+
const coreClient = new x402Client().register("eip155:*", new ExactEvmScheme(toClientEvmSigner(account)));
|
|
152
|
+
return { httpClient: new x402HTTPClient(coreClient), account };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function createChainTimedPaymentPayload(httpClient, paymentRequired) {
|
|
156
|
+
try {
|
|
157
|
+
const publicClient = createPublicClient({ chain: base, transport: http(BASE_RPC_URL) });
|
|
158
|
+
const block = await publicClient.getBlock();
|
|
159
|
+
const chainNow = Number(block.timestamp);
|
|
160
|
+
const originalNow = Date.now;
|
|
161
|
+
const localNow = Math.floor(originalNow() / 1000);
|
|
162
|
+
const timeout = Number(paymentRequired.accepts?.[0]?.maxTimeoutSeconds || 300);
|
|
163
|
+
const lowerBound = localNow + 30 - timeout;
|
|
164
|
+
const upperBound = chainNow + 600;
|
|
165
|
+
const signingNow = Math.min(Math.max(chainNow, lowerBound), upperBound);
|
|
166
|
+
Date.now = () => signingNow * 1000;
|
|
167
|
+
try {
|
|
168
|
+
return await httpClient.createPaymentPayload(paymentRequired);
|
|
169
|
+
} finally {
|
|
170
|
+
Date.now = originalNow;
|
|
171
|
+
}
|
|
172
|
+
} catch (_) {
|
|
173
|
+
return httpClient.createPaymentPayload(paymentRequired);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function paidPost(path, body) {
|
|
178
|
+
const { httpClient } = requireWalletClient();
|
|
179
|
+
const url = BASE_URL + path;
|
|
180
|
+
const init = {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "Content-Type": "application/json" },
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
};
|
|
185
|
+
const challengeRes = await fetch(url, init);
|
|
186
|
+
if (challengeRes.status !== 402) {
|
|
187
|
+
const text = await challengeRes.text().catch(() => "");
|
|
188
|
+
throw new Error(`Expected x402 challenge, got ${challengeRes.status}: ${text.slice(0, 240)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let challengeBody;
|
|
192
|
+
try {
|
|
193
|
+
challengeBody = await challengeRes.clone().json();
|
|
194
|
+
} catch (_) {}
|
|
195
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
196
|
+
(name) => challengeRes.headers.get(name),
|
|
197
|
+
challengeBody
|
|
198
|
+
);
|
|
199
|
+
const paymentPayload = await createChainTimedPaymentPayload(httpClient, paymentRequired);
|
|
200
|
+
const paidRes = await fetch(url, {
|
|
201
|
+
...init,
|
|
202
|
+
headers: {
|
|
203
|
+
...init.headers,
|
|
204
|
+
...httpClient.encodePaymentSignatureHeader(paymentPayload),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
if (!paidRes.ok) {
|
|
208
|
+
const text = await paidRes.text().catch(() => paidRes.statusText);
|
|
209
|
+
throw new Error(`Paid TTS call failed: ${paidRes.status} ${text.slice(0, 240)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const paymentReceipt = paidRes.headers.get("payment-response");
|
|
213
|
+
const contentType = paidRes.headers.get("content-type") || "";
|
|
214
|
+
if (contentType.includes("application/json")) {
|
|
215
|
+
const json = await paidRes.json();
|
|
216
|
+
return { content_type: contentType, response: json, payment_response: paymentReceipt };
|
|
217
|
+
}
|
|
218
|
+
const audio = Buffer.from(await paidRes.arrayBuffer());
|
|
219
|
+
return {
|
|
220
|
+
content_type: contentType || "audio/wav",
|
|
221
|
+
audio_base64: audio.toString("base64"),
|
|
222
|
+
bytes: audio.length,
|
|
223
|
+
payment_response: paymentReceipt,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function freeGet(path) {
|
|
228
|
+
const res = await fetch(BASE_URL + path);
|
|
229
|
+
if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
|
|
230
|
+
return res.json();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function textResult(value) {
|
|
234
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function callTool(name, args = {}) {
|
|
238
|
+
if (name === "list_tts_voices") return freeGet("/v1/voices");
|
|
239
|
+
|
|
240
|
+
if (name === "speak_standard") {
|
|
241
|
+
const text = String(args.text || "");
|
|
242
|
+
const path = pickBucketEndpoint("/v1/tts/base", "/v1/tts/base-long", text.length);
|
|
243
|
+
return paidPost(path, { text, voice: args.voice || "M1", lang: args.lang || "en" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (name === "speak_pro") {
|
|
247
|
+
const text = String(args.text || "");
|
|
248
|
+
const path = pickBucketEndpoint("/v1/tts/pro", "/v1/tts/pro-long", text.length);
|
|
249
|
+
return paidPost(path, {
|
|
250
|
+
text,
|
|
251
|
+
voice: args.voice || "M1",
|
|
252
|
+
lang: args.lang || "en",
|
|
253
|
+
speed: args.speed,
|
|
254
|
+
steps: args.steps,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (name === "speak_persona") {
|
|
259
|
+
const text = String(args.text || "");
|
|
260
|
+
const path = pickBucketEndpoint("/v1/tts/custom", "/v1/tts/custom-long", text.length);
|
|
261
|
+
return paidPost(path, {
|
|
262
|
+
text,
|
|
263
|
+
voice: args.voice || "Storyteller",
|
|
264
|
+
lang: args.lang || "en",
|
|
265
|
+
speed: args.speed,
|
|
266
|
+
steps: args.steps,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (name === "openai_speech") {
|
|
271
|
+
const input = String(args.input || "");
|
|
272
|
+
const path = pickBucketEndpoint("/v1/audio/speech", "/v1/audio/speech-long", input.length);
|
|
273
|
+
return paidPost(path, {
|
|
274
|
+
input,
|
|
275
|
+
voice: args.voice || "M1",
|
|
276
|
+
model: args.model || "forgemesh-voice",
|
|
277
|
+
response_format: args.response_format || "wav",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (name === "batch_speak") {
|
|
282
|
+
if (!Array.isArray(args.items) || args.items.length === 0) throw new Error("items must be a non-empty array");
|
|
283
|
+
const totalChars = args.items.reduce((sum, item) => sum + String(item?.text || "").length, 0);
|
|
284
|
+
const path = pickBucketEndpoint("/v1/tts/batch", "/v1/tts/batch-long", totalChars);
|
|
285
|
+
return paidPost(path, { items: args.items, defaults: args.defaults || { voice: "F2", lang: "en" } });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const server = new McpServer({ name: "voice-mcp", version: "0.1.0" });
|
|
292
|
+
server.server.onerror = (error) => {
|
|
293
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
294
|
+
};
|
|
295
|
+
for (const tool of TOOLS) {
|
|
296
|
+
server.registerTool(
|
|
297
|
+
tool.name,
|
|
298
|
+
{
|
|
299
|
+
title: tool.name,
|
|
300
|
+
description: tool.description,
|
|
301
|
+
inputSchema: TOOL_SCHEMAS[tool.name],
|
|
302
|
+
},
|
|
303
|
+
async (args) => {
|
|
304
|
+
try {
|
|
305
|
+
return textResult(await callTool(tool.name, args || {}));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return {
|
|
308
|
+
isError: true,
|
|
309
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function main() {
|
|
317
|
+
await server.connect(new StdioServerTransport());
|
|
318
|
+
process.stdin.resume();
|
|
319
|
+
const keepAlive = setInterval(() => {}, 2 ** 30);
|
|
320
|
+
process.stdin.on("end", () => clearInterval(keepAlive));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (require.main === module) {
|
|
324
|
+
main().catch((error) => {
|
|
325
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
TOOLS,
|
|
332
|
+
TOOL_SCHEMAS,
|
|
333
|
+
callTool,
|
|
334
|
+
pickBucketEndpoint,
|
|
335
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgemeshlabs/voice-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.forgemeshlabs/voice-mcp",
|
|
5
|
+
"description": "Give Your Agent A Voice: x402 pay-per-call speech with 20 voices, 10 personas, 31 languages, granular speed and quality controls, OpenAI-shaped requests, and batch audio",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"voice-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"SECURITY.md",
|
|
15
|
+
"Dockerfile",
|
|
16
|
+
"glama.json",
|
|
17
|
+
"server.json"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node index.js",
|
|
21
|
+
"check": "node --check index.js",
|
|
22
|
+
"test": "npm run check && node scripts/test-mcp.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"x402",
|
|
31
|
+
"text-to-speech",
|
|
32
|
+
"tts",
|
|
33
|
+
"voice",
|
|
34
|
+
"agent-voice",
|
|
35
|
+
"openai-compatible",
|
|
36
|
+
"forgemesh",
|
|
37
|
+
"coinbase",
|
|
38
|
+
"base",
|
|
39
|
+
"usdc",
|
|
40
|
+
"micropayments"
|
|
41
|
+
],
|
|
42
|
+
"author": "clawdbotworker <clawdbotworker@gmail.com>",
|
|
43
|
+
"maintainers": [
|
|
44
|
+
"clawdbotworker <clawdbotworker@gmail.com>"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/forgemeshlabs/voice-mcp.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://voice.forgemesh.io",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/forgemeshlabs/voice-mcp/issues"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
57
|
+
"@x402/core": "2.11.0",
|
|
58
|
+
"@x402/evm": "2.11.0",
|
|
59
|
+
"zod": "^3.25.76",
|
|
60
|
+
"viem": "^2.0.0"
|
|
61
|
+
},
|
|
62
|
+
"overrides": {
|
|
63
|
+
"ws": "8.21.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voice-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Give Your Agent A Voice: x402 pay-per-call speech with 20 voices, 10 personas, 31 languages, granular speed and quality controls, OpenAI-shaped requests, and batch audio",
|
|
5
|
+
"homepage": "https://voice.forgemesh.io",
|
|
6
|
+
"repository": "https://github.com/forgemeshlabs/voice-mcp",
|
|
7
|
+
"transport": {
|
|
8
|
+
"type": "stdio",
|
|
9
|
+
"command": "voice-mcp"
|
|
10
|
+
}
|
|
11
|
+
}
|