@mindstone/mcp-server-elevenlabs 0.3.0 → 0.4.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 +30 -7
- package/dist/bridge.d.ts +1 -0
- package/dist/client.d.ts +34 -3
- package/dist/client.js +68 -14
- package/dist/endpoints.d.ts +34 -0
- package/dist/endpoints.js +37 -0
- package/dist/error-detail.d.ts +5 -0
- package/dist/error-detail.js +20 -0
- package/dist/server.js +9 -1
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +105 -0
- package/dist/tools/alignment.d.ts +3 -0
- package/dist/tools/alignment.js +55 -0
- package/dist/tools/audio-isolation.d.ts +3 -0
- package/dist/tools/audio-isolation.js +51 -0
- package/dist/tools/configure.js +18 -6
- package/dist/tools/dialogue.d.ts +3 -0
- package/dist/tools/dialogue.js +70 -0
- package/dist/tools/dubbing.d.ts +3 -0
- package/dist/tools/dubbing.js +234 -0
- package/dist/tools/file-input.d.ts +28 -0
- package/dist/tools/file-input.js +77 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/music.js +85 -25
- package/dist/tools/path-safety.d.ts +1 -0
- package/dist/tools/path-safety.js +4 -1
- package/dist/tools/speech.js +57 -24
- package/dist/tools/transcription.js +27 -61
- package/dist/tools/voice-changer.d.ts +3 -0
- package/dist/tools/voice-changer.js +62 -0
- package/dist/tools/voice-clone.d.ts +3 -0
- package/dist/tools/voice-clone.js +104 -0
- package/dist/tools/voice-design.d.ts +3 -0
- package/dist/tools/voice-design.js +153 -0
- package/dist/tools/voices.js +167 -13
- package/dist/types.d.ts +91 -0
- package/dist/types.js +20 -4
- package/dist/untrusted-content.d.ts +45 -0
- package/dist/untrusted-content.js +104 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,11 +7,10 @@ ElevenLabs MCP server for Model Context Protocol hosts. Generate speech, music,
|
|
|
7
7
|
|
|
8
8
|
## Status
|
|
9
9
|
|
|
10
|
-
- **Version:** [0.
|
|
10
|
+
- **Version:** [0.4.0](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-elevenlabs)
|
|
11
11
|
- **Auth:** API key ([`ELEVENLABS_API_KEY`](./server.json))
|
|
12
|
-
- **Tools:** [
|
|
12
|
+
- **Tools:** [24](./src/tools/) (account, voices, speech, music, transcription, voice conversion, isolation, alignment, cloning, dialogue, voice design, dubbing)
|
|
13
13
|
- **Surface:** cloud-api
|
|
14
|
-
- **Hosts tested:** Claude Desktop, Cursor, Mindstone Rebel
|
|
15
14
|
- **Machine-readable:** [`STATUS.json`](./STATUS.json)
|
|
16
15
|
|
|
17
16
|
## Requirements
|
|
@@ -115,17 +114,39 @@ node dist/index.js
|
|
|
115
114
|
}
|
|
116
115
|
```
|
|
117
116
|
|
|
118
|
-
## Tools (
|
|
117
|
+
## Tools (24)
|
|
119
118
|
|
|
120
119
|
### Configuration
|
|
121
120
|
- `configure_elevenlabs_api_key` — Save your ElevenLabs API key
|
|
122
121
|
|
|
123
|
-
###
|
|
124
|
-
- `
|
|
122
|
+
### Account & discovery (FREE)
|
|
123
|
+
- `check_subscription` — Check subscription tier and character credit usage
|
|
124
|
+
- `list_models` — List TTS models with languages and capabilities
|
|
125
125
|
|
|
126
|
-
###
|
|
126
|
+
### Voices
|
|
127
|
+
- `list_voices` — Search and browse voices on your account
|
|
128
|
+
- `get_voice` — Get full details for one voice by voice_id
|
|
129
|
+
- `search_shared_voices` — Search the public shared voice library (filters include accent)
|
|
130
|
+
- `clone_voice` — Create an instant voice clone from local audio samples (`destructiveHint`)
|
|
131
|
+
- `delete_voice` — Permanently delete a voice (`destructiveHint`)
|
|
132
|
+
- `design_voice` — Generate voice-design previews from a text description (slow; previews saved to tmp files)
|
|
133
|
+
- `create_voice_from_preview` — Save a design preview as a permanent voice (`destructiveHint`)
|
|
134
|
+
|
|
135
|
+
### Speech & conversion
|
|
127
136
|
- `generate_speech` — Generate spoken audio from text using text-to-speech
|
|
128
137
|
- `generate_sound_effect` — Generate sound effects from a text description
|
|
138
|
+
- `speech_to_speech` — Convert source audio to a different voice
|
|
139
|
+
- `text_to_dialogue` — Multi-voice dialogue from a script (one voice per line)
|
|
140
|
+
|
|
141
|
+
### Audio processing
|
|
142
|
+
- `isolate_audio` — Remove background noise from an audio file (source must be ≥ ~4.6s; shorter clips fail upstream)
|
|
143
|
+
- `forced_alignment` — Align transcript text to audio with per-word timestamps
|
|
144
|
+
|
|
145
|
+
### Dubbing (v1 API — async submit → poll → download)
|
|
146
|
+
- `create_dubbing` — Submit a dubbing job (local `file` via sandbox or `source_url` for ElevenLabs-side fetch)
|
|
147
|
+
- `get_dubbing` — Poll job status until `dubbed`, `failed`, or `cancelled`
|
|
148
|
+
- `download_dubbed_audio` — Download dubbed audio (Content-Type sniffed)
|
|
149
|
+
- `delete_dubbing` — Permanently delete a dubbing job (`destructiveHint`)
|
|
129
150
|
|
|
130
151
|
### Music
|
|
131
152
|
- `generate_music` — Generate music from a text prompt
|
|
@@ -135,6 +156,8 @@ node dist/index.js
|
|
|
135
156
|
### Transcription
|
|
136
157
|
- `transcribe_audio` — Transcribe speech from an audio file to text
|
|
137
158
|
|
|
159
|
+
Local file paths for upload tools must be inside `MCP_WORKSPACE_PATH` (or `os.tmpdir()` when unset). See `src/tools/file-input.ts`.
|
|
160
|
+
|
|
138
161
|
## Licence
|
|
139
162
|
|
|
140
163
|
[FSL-1.1-MIT](./LICENSE) — Functional Source License, Version 1.1, with MIT future licence. The software converts to MIT licence on 2030-04-08.
|
package/dist/bridge.d.ts
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -9,17 +9,48 @@
|
|
|
9
9
|
* Voices URL: https://api.elevenlabs.io/v2/voices
|
|
10
10
|
*/
|
|
11
11
|
import { type AudioResult } from './types.js';
|
|
12
|
+
export interface ElevenLabsFetchOptions extends RequestInit {
|
|
13
|
+
/** Per-call timeout override (default REQUEST_TIMEOUT_MS). Explicit `signal` wins. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Make an authenticated request to the ElevenLabs API.
|
|
14
18
|
* Returns a raw Response object.
|
|
15
19
|
*/
|
|
16
|
-
export declare function elevenLabsFetch(apiKey: string, urlPath: string, options?:
|
|
20
|
+
export declare function elevenLabsFetch(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<Response>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract a human-readable detail message from an ElevenLabs error response.
|
|
23
|
+
*
|
|
24
|
+
* The API uses several detail shapes:
|
|
25
|
+
* - `detail: "string message"` (legacy)
|
|
26
|
+
* - `detail: { message: "...", status?: "..." }` (current auth/permission errors)
|
|
27
|
+
* - `detail: [{ type, loc: ["body", "field", ...], msg, input }, ...]` (FastAPI 422)
|
|
28
|
+
*
|
|
29
|
+
* The 422 array form is the one that bit us in
|
|
30
|
+
* `generate_music_from_plan`: previously we threw away the field-level info
|
|
31
|
+
* and left the user with `HTTP 422: unknown`. We now flatten the validation
|
|
32
|
+
* errors into something like
|
|
33
|
+
* `body.composition_plan.sections.0.section_name: Field required;
|
|
34
|
+
* body.composition_plan.sections.0.lines: Field required`
|
|
35
|
+
* which is what an LLM agent actually needs to self-correct.
|
|
36
|
+
*
|
|
37
|
+
* Raw detail is third-party-authored — callers must envelope before exposing
|
|
38
|
+
* to model-visible output (see `envelopeApiErrorDetail`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractErrorDetail(response: Response): Promise<string>;
|
|
17
41
|
/**
|
|
18
42
|
* Make a JSON API call and parse the response.
|
|
19
43
|
*/
|
|
20
|
-
export declare function elevenLabsJson<T>(apiKey: string, urlPath: string, options?:
|
|
44
|
+
export declare function elevenLabsJson<T>(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<T>;
|
|
45
|
+
/** Map response Content-Type to a file extension (OpenAPI drift — don't trust empty schemas). */
|
|
46
|
+
export declare function extensionFromContentType(contentType: string | null | undefined): string;
|
|
21
47
|
/**
|
|
22
48
|
* Make an API call that returns raw audio binary. Save to file and return path.
|
|
23
49
|
*/
|
|
24
|
-
export declare function elevenLabsAudio(apiKey: string, urlPath: string, options?:
|
|
50
|
+
export declare function elevenLabsAudio(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions, fileExtension?: string): Promise<AudioResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Download binary audio with content-type sniffing.
|
|
53
|
+
* When the API returns JSON (error body despite 200 drift), surfaces the flattened error.
|
|
54
|
+
*/
|
|
55
|
+
export declare function elevenLabsBinaryDownload(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<AudioResult>;
|
|
25
56
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
CHANGED
|
@@ -13,7 +13,8 @@ import * as path from 'path';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { ElevenLabsError, REQUEST_TIMEOUT_MS, getErrorResolution, } from './types.js';
|
|
16
|
-
|
|
16
|
+
import { ELEVENLABS_API_V1_BASE } from './endpoints.js';
|
|
17
|
+
import { envelopeApiErrorDetail, formatApiErrorMessage } from './error-detail.js';
|
|
17
18
|
/**
|
|
18
19
|
* Make an authenticated request to the ElevenLabs API.
|
|
19
20
|
* Returns a raw Response object.
|
|
@@ -24,21 +25,22 @@ export async function elevenLabsFetch(apiKey, urlPath, options = {}) {
|
|
|
24
25
|
}
|
|
25
26
|
const url = urlPath.startsWith('https://')
|
|
26
27
|
? urlPath
|
|
27
|
-
: `${
|
|
28
|
+
: `${ELEVENLABS_API_V1_BASE}${urlPath}`;
|
|
29
|
+
const { timeoutMs, ...fetchOptions } = options;
|
|
28
30
|
const headers = {
|
|
29
31
|
'xi-api-key': apiKey,
|
|
30
|
-
...(
|
|
32
|
+
...(fetchOptions.headers || {}),
|
|
31
33
|
};
|
|
32
34
|
// Only set Content-Type for JSON bodies (not FormData)
|
|
33
|
-
if (!(
|
|
35
|
+
if (!(fetchOptions.body instanceof FormData) && !headers['Content-Type']) {
|
|
34
36
|
headers['Content-Type'] = 'application/json';
|
|
35
37
|
}
|
|
36
|
-
console.error(`[ElevenLabs API] ${
|
|
38
|
+
console.error(`[ElevenLabs API] ${fetchOptions.method || 'GET'} ${url}`);
|
|
37
39
|
let response;
|
|
38
40
|
try {
|
|
39
41
|
response = await fetch(url, {
|
|
40
|
-
...
|
|
41
|
-
signal:
|
|
42
|
+
...fetchOptions,
|
|
43
|
+
signal: fetchOptions.signal ?? AbortSignal.timeout(timeoutMs ?? REQUEST_TIMEOUT_MS),
|
|
42
44
|
headers,
|
|
43
45
|
});
|
|
44
46
|
}
|
|
@@ -60,22 +62,30 @@ export async function elevenLabsFetch(apiKey, urlPath, options = {}) {
|
|
|
60
62
|
if (response.status === 401) {
|
|
61
63
|
const detail = await extractErrorDetail(response);
|
|
62
64
|
const isMissingPermission = detail.includes('missing_permissions');
|
|
65
|
+
const envelopedDetail = detail ? envelopeApiErrorDetail(detail) : '';
|
|
63
66
|
throw new ElevenLabsError(isMissingPermission
|
|
64
|
-
? `API key missing required permission: ${
|
|
67
|
+
? `API key missing required permission: ${envelopedDetail}`
|
|
65
68
|
: detail
|
|
66
|
-
?
|
|
69
|
+
? formatApiErrorMessage('Authentication failed', detail)
|
|
67
70
|
: 'Authentication failed', isMissingPermission ? 'MISSING_PERMISSION' : 'AUTH_FAILED', isMissingPermission
|
|
68
71
|
? 'Regenerate your API key at https://elevenlabs.io/app/settings/api-keys with the missing permission enabled, then call configure_elevenlabs_api_key again.'
|
|
69
72
|
: getErrorResolution(401));
|
|
70
73
|
}
|
|
71
74
|
if (response.status === 403) {
|
|
72
75
|
const detail = await extractErrorDetail(response);
|
|
73
|
-
throw new ElevenLabsError(
|
|
76
|
+
throw new ElevenLabsError(detail
|
|
77
|
+
? formatApiErrorMessage('Access forbidden', detail)
|
|
78
|
+
: 'Access forbidden: insufficient permissions or quota', 'AUTH_FAILED', getErrorResolution(403, detail));
|
|
74
79
|
}
|
|
75
80
|
// Handle other errors
|
|
76
81
|
if (!response.ok) {
|
|
77
82
|
const detail = await extractErrorDetail(response);
|
|
78
|
-
|
|
83
|
+
const statusText = response.statusText
|
|
84
|
+
? envelopeApiErrorDetail(response.statusText)
|
|
85
|
+
: '';
|
|
86
|
+
throw new ElevenLabsError(detail
|
|
87
|
+
? `ElevenLabs API error (HTTP ${response.status}): ${envelopeApiErrorDetail(detail)}`
|
|
88
|
+
: `ElevenLabs API error (HTTP ${response.status})${statusText ? `: ${statusText}` : ''}`, `HTTP_${response.status}`, getErrorResolution(response.status, detail));
|
|
79
89
|
}
|
|
80
90
|
return response;
|
|
81
91
|
}
|
|
@@ -94,8 +104,11 @@ export async function elevenLabsFetch(apiKey, urlPath, options = {}) {
|
|
|
94
104
|
* `body.composition_plan.sections.0.section_name: Field required;
|
|
95
105
|
* body.composition_plan.sections.0.lines: Field required`
|
|
96
106
|
* which is what an LLM agent actually needs to self-correct.
|
|
107
|
+
*
|
|
108
|
+
* Raw detail is third-party-authored — callers must envelope before exposing
|
|
109
|
+
* to model-visible output (see `envelopeApiErrorDetail`).
|
|
97
110
|
*/
|
|
98
|
-
async function extractErrorDetail(response) {
|
|
111
|
+
export async function extractErrorDetail(response) {
|
|
99
112
|
try {
|
|
100
113
|
const errBody = (await response.clone().json());
|
|
101
114
|
const d = errBody?.detail;
|
|
@@ -146,13 +159,54 @@ export async function elevenLabsJson(apiKey, urlPath, options = {}) {
|
|
|
146
159
|
const response = await elevenLabsFetch(apiKey, urlPath, options);
|
|
147
160
|
return (await response.json());
|
|
148
161
|
}
|
|
162
|
+
/** Map response Content-Type to a file extension (OpenAPI drift — don't trust empty schemas). */
|
|
163
|
+
export function extensionFromContentType(contentType) {
|
|
164
|
+
if (!contentType)
|
|
165
|
+
return 'mp3';
|
|
166
|
+
const ct = contentType.split(';')[0]?.trim().toLowerCase() ?? '';
|
|
167
|
+
const map = {
|
|
168
|
+
'audio/mpeg': 'mp3',
|
|
169
|
+
'audio/mp3': 'mp3',
|
|
170
|
+
'audio/wav': 'wav',
|
|
171
|
+
'audio/x-wav': 'wav',
|
|
172
|
+
'audio/wave': 'wav',
|
|
173
|
+
'audio/ogg': 'ogg',
|
|
174
|
+
'audio/flac': 'flac',
|
|
175
|
+
'audio/mp4': 'm4a',
|
|
176
|
+
'audio/aac': 'aac',
|
|
177
|
+
'audio/webm': 'webm',
|
|
178
|
+
};
|
|
179
|
+
return map[ct] ?? 'mp3';
|
|
180
|
+
}
|
|
149
181
|
/**
|
|
150
182
|
* Make an API call that returns raw audio binary. Save to file and return path.
|
|
151
183
|
*/
|
|
152
|
-
export async function elevenLabsAudio(apiKey, urlPath, options = {}, fileExtension
|
|
184
|
+
export async function elevenLabsAudio(apiKey, urlPath, options = {}, fileExtension) {
|
|
153
185
|
const response = await elevenLabsFetch(apiKey, urlPath, options);
|
|
186
|
+
const contentType = response.headers.get('content-type');
|
|
187
|
+
const ext = fileExtension ?? extensionFromContentType(contentType);
|
|
188
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
189
|
+
const fileName = `elevenlabs_${crypto.randomUUID()}.${ext}`;
|
|
190
|
+
const filePath = path.join(os.tmpdir(), fileName);
|
|
191
|
+
fs.writeFileSync(filePath, buffer);
|
|
192
|
+
return { filePath, sizeBytes: buffer.length };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Download binary audio with content-type sniffing.
|
|
196
|
+
* When the API returns JSON (error body despite 200 drift), surfaces the flattened error.
|
|
197
|
+
*/
|
|
198
|
+
export async function elevenLabsBinaryDownload(apiKey, urlPath, options = {}) {
|
|
199
|
+
const response = await elevenLabsFetch(apiKey, urlPath, options);
|
|
200
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
201
|
+
if (contentType.includes('application/json')) {
|
|
202
|
+
const detail = await extractErrorDetail(response);
|
|
203
|
+
throw new ElevenLabsError(detail
|
|
204
|
+
? formatApiErrorMessage('Download failed', detail)
|
|
205
|
+
: 'Download failed: API returned JSON instead of audio', 'DOWNLOAD_FAILED', detail ? getErrorResolution(422, detail) : 'Verify dubbing status with get_dubbing before downloading.');
|
|
206
|
+
}
|
|
207
|
+
const ext = extensionFromContentType(contentType);
|
|
154
208
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
155
|
-
const fileName = `elevenlabs_${crypto.randomUUID()}.${
|
|
209
|
+
const fileName = `elevenlabs_${crypto.randomUUID()}.${ext}`;
|
|
156
210
|
const filePath = path.join(os.tmpdir(), fileName);
|
|
157
211
|
fs.writeFileSync(filePath, buffer);
|
|
158
212
|
return { filePath, sizeBytes: buffer.length };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical ElevenLabs API endpoint paths.
|
|
3
|
+
*
|
|
4
|
+
* Every path used by this connector lives here so a wrong path is a
|
|
5
|
+
* one-line fix. v1 paths are relative to `ELEVENLABS_API_V1_BASE`; v2 voice
|
|
6
|
+
* listing uses the separate v2 base URL.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ELEVENLABS_API_V1_BASE = "https://api.elevenlabs.io/v1";
|
|
9
|
+
export declare const ELEVENLABS_API_V2_BASE = "https://api.elevenlabs.io/v2";
|
|
10
|
+
/** v1 relative paths — pass to `elevenLabsFetch` / `elevenLabsJson`. */
|
|
11
|
+
export declare const ENDPOINTS: {
|
|
12
|
+
readonly USER_SUBSCRIPTION: "/user/subscription";
|
|
13
|
+
readonly MODELS: "/models";
|
|
14
|
+
readonly SHARED_VOICES: "/shared-voices";
|
|
15
|
+
readonly SOUND_GENERATION: "/sound-generation";
|
|
16
|
+
readonly MUSIC: "/music";
|
|
17
|
+
readonly MUSIC_PLAN: "/music/plan";
|
|
18
|
+
readonly SPEECH_TO_TEXT: "/speech-to-text";
|
|
19
|
+
readonly AUDIO_ISOLATION: "/audio-isolation";
|
|
20
|
+
readonly FORCED_ALIGNMENT: "/forced-alignment";
|
|
21
|
+
readonly VOICES_ADD: "/voices/add";
|
|
22
|
+
readonly TEXT_TO_DIALOGUE: "/text-to-dialogue";
|
|
23
|
+
readonly TEXT_TO_VOICE_DESIGN: "/text-to-voice/design";
|
|
24
|
+
readonly TEXT_TO_VOICE: "/text-to-voice";
|
|
25
|
+
readonly DUBBING: "/dubbing";
|
|
26
|
+
readonly voice: (voiceId: string) => string;
|
|
27
|
+
readonly dubbing: (dubbingId: string) => string;
|
|
28
|
+
readonly dubbingAudio: (dubbingId: string, languageCode: string) => string;
|
|
29
|
+
readonly speechToSpeech: (voiceId: string) => string;
|
|
30
|
+
readonly textToSpeech: (voiceId: string, outputFormat: string) => string;
|
|
31
|
+
};
|
|
32
|
+
/** Build a full v2 voices list/search URL with optional query string. */
|
|
33
|
+
export declare function voicesV2Url(params?: URLSearchParams): string;
|
|
34
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical ElevenLabs API endpoint paths.
|
|
3
|
+
*
|
|
4
|
+
* Every path used by this connector lives here so a wrong path is a
|
|
5
|
+
* one-line fix. v1 paths are relative to `ELEVENLABS_API_V1_BASE`; v2 voice
|
|
6
|
+
* listing uses the separate v2 base URL.
|
|
7
|
+
*/
|
|
8
|
+
export const ELEVENLABS_API_V1_BASE = 'https://api.elevenlabs.io/v1';
|
|
9
|
+
export const ELEVENLABS_API_V2_BASE = 'https://api.elevenlabs.io/v2';
|
|
10
|
+
/** v1 relative paths — pass to `elevenLabsFetch` / `elevenLabsJson`. */
|
|
11
|
+
export const ENDPOINTS = {
|
|
12
|
+
USER_SUBSCRIPTION: '/user/subscription',
|
|
13
|
+
MODELS: '/models',
|
|
14
|
+
SHARED_VOICES: '/shared-voices',
|
|
15
|
+
SOUND_GENERATION: '/sound-generation',
|
|
16
|
+
MUSIC: '/music',
|
|
17
|
+
MUSIC_PLAN: '/music/plan',
|
|
18
|
+
SPEECH_TO_TEXT: '/speech-to-text',
|
|
19
|
+
AUDIO_ISOLATION: '/audio-isolation',
|
|
20
|
+
FORCED_ALIGNMENT: '/forced-alignment',
|
|
21
|
+
VOICES_ADD: '/voices/add',
|
|
22
|
+
TEXT_TO_DIALOGUE: '/text-to-dialogue',
|
|
23
|
+
TEXT_TO_VOICE_DESIGN: '/text-to-voice/design',
|
|
24
|
+
TEXT_TO_VOICE: '/text-to-voice',
|
|
25
|
+
DUBBING: '/dubbing',
|
|
26
|
+
voice: (voiceId) => `/voices/${encodeURIComponent(voiceId)}`,
|
|
27
|
+
dubbing: (dubbingId) => `/dubbing/${encodeURIComponent(dubbingId)}`,
|
|
28
|
+
dubbingAudio: (dubbingId, languageCode) => `/dubbing/${encodeURIComponent(dubbingId)}/audio/${encodeURIComponent(languageCode)}`,
|
|
29
|
+
speechToSpeech: (voiceId) => `/speech-to-speech/${encodeURIComponent(voiceId)}`,
|
|
30
|
+
textToSpeech: (voiceId, outputFormat) => `/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${encodeURIComponent(outputFormat)}`,
|
|
31
|
+
};
|
|
32
|
+
/** Build a full v2 voices list/search URL with optional query string. */
|
|
33
|
+
export function voicesV2Url(params) {
|
|
34
|
+
const qs = params?.toString();
|
|
35
|
+
return `${ELEVENLABS_API_V2_BASE}/voices${qs ? `?${qs}` : ''}`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=endpoints.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Wrap API-authored error detail for inclusion in ElevenLabsError message/resolution. */
|
|
2
|
+
export declare function envelopeApiErrorDetail(detail: string): string;
|
|
3
|
+
/** Prefix + enveloped detail, or the prefix alone when detail is empty. */
|
|
4
|
+
export declare function formatApiErrorMessage(prefix: string, detail: string): string;
|
|
5
|
+
//# sourceMappingURL=error-detail.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope third-party API error `detail` strings before they reach model-visible
|
|
3
|
+
* tool error output (message + resolution). FastAPI-422 flattening stays intact —
|
|
4
|
+
* we wrap the flattened string, we do not discard field-path usefulness.
|
|
5
|
+
*/
|
|
6
|
+
import { wrapUntrusted } from './untrusted-content.js';
|
|
7
|
+
const ERROR_DETAIL_SOURCE = 'elevenlabs:api:error_detail';
|
|
8
|
+
/** Wrap API-authored error detail for inclusion in ElevenLabsError message/resolution. */
|
|
9
|
+
export function envelopeApiErrorDetail(detail) {
|
|
10
|
+
if (!detail)
|
|
11
|
+
return '';
|
|
12
|
+
return wrapUntrusted(detail, ERROR_DETAIL_SOURCE);
|
|
13
|
+
}
|
|
14
|
+
/** Prefix + enveloped detail, or the prefix alone when detail is empty. */
|
|
15
|
+
export function formatApiErrorMessage(prefix, detail) {
|
|
16
|
+
if (!detail)
|
|
17
|
+
return prefix;
|
|
18
|
+
return `${prefix}: ${envelopeApiErrorDetail(detail)}`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=error-detail.js.map
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import { registerConfigureTools, registerMusicTools, registerSpeechTools, registerVoiceTools, registerTranscriptionTools, } from './tools/index.js';
|
|
3
|
+
import { registerConfigureTools, registerAccountTools, registerMusicTools, registerSpeechTools, registerVoiceTools, registerTranscriptionTools, registerVoiceChangerTools, registerAudioIsolationTools, registerAlignmentTools, registerVoiceCloneTools, registerDialogueTools, registerVoiceDesignTools, registerDubbingTools, } from './tools/index.js';
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const pkg = require('../package.json');
|
|
6
6
|
export function createServer() {
|
|
@@ -9,10 +9,18 @@ export function createServer() {
|
|
|
9
9
|
version: pkg.version,
|
|
10
10
|
});
|
|
11
11
|
registerConfigureTools(server);
|
|
12
|
+
registerAccountTools(server);
|
|
12
13
|
registerMusicTools(server);
|
|
13
14
|
registerSpeechTools(server);
|
|
14
15
|
registerVoiceTools(server);
|
|
15
16
|
registerTranscriptionTools(server);
|
|
17
|
+
registerVoiceChangerTools(server);
|
|
18
|
+
registerAudioIsolationTools(server);
|
|
19
|
+
registerAlignmentTools(server);
|
|
20
|
+
registerVoiceCloneTools(server);
|
|
21
|
+
registerDialogueTools(server);
|
|
22
|
+
registerVoiceDesignTools(server);
|
|
23
|
+
registerDubbingTools(server);
|
|
16
24
|
return server;
|
|
17
25
|
}
|
|
18
26
|
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getApiKey } from '../auth.js';
|
|
3
|
+
import { elevenLabsJson } from '../client.js';
|
|
4
|
+
import { ENDPOINTS } from '../endpoints.js';
|
|
5
|
+
import { ElevenLabsError, } from '../types.js';
|
|
6
|
+
import { wrapUntrusted } from '../untrusted-content.js';
|
|
7
|
+
import { withErrorHandling } from '../utils.js';
|
|
8
|
+
export function registerAccountTools(server) {
|
|
9
|
+
server.registerTool('check_subscription', {
|
|
10
|
+
description: `Check ElevenLabs subscription tier and character credit usage.
|
|
11
|
+
|
|
12
|
+
WHEN TO USE:
|
|
13
|
+
- Before expensive generation calls (speech, music, sound effects) to confirm credits remain
|
|
14
|
+
- When a tool returns quota or 403 errors — read remaining characters and next reset
|
|
15
|
+
- To answer "how much ElevenLabs credit do I have left?"
|
|
16
|
+
|
|
17
|
+
EXAMPLE: {} (no arguments)
|
|
18
|
+
|
|
19
|
+
RELATED TOOLS:
|
|
20
|
+
- generate_speech, generate_music, generate_sound_effect: credit-consuming generation
|
|
21
|
+
- list_models: discover which models your tier can use
|
|
22
|
+
|
|
23
|
+
RETURNS: tier, character_count, character_limit, characters_remaining, next_character_count_reset_unix (and ISO), status when present.
|
|
24
|
+
|
|
25
|
+
COST: FREE — no credits consumed.`,
|
|
26
|
+
inputSchema: z.object({}),
|
|
27
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
28
|
+
}, withErrorHandling(async () => {
|
|
29
|
+
const apiKey = getApiKey();
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
|
|
32
|
+
}
|
|
33
|
+
const data = await elevenLabsJson(apiKey, ENDPOINTS.USER_SUBSCRIPTION);
|
|
34
|
+
const characterCount = data.character_count ?? 0;
|
|
35
|
+
const characterLimit = data.character_limit ?? 0;
|
|
36
|
+
const remaining = Math.max(0, characterLimit - characterCount);
|
|
37
|
+
const resetUnix = data.next_character_count_reset_unix;
|
|
38
|
+
const resetIso = resetUnix
|
|
39
|
+
? new Date(resetUnix * 1000).toISOString()
|
|
40
|
+
: undefined;
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
ok: true,
|
|
43
|
+
tier: wrapUntrusted(data.tier ?? undefined, 'elevenlabs:check_subscription:tier'),
|
|
44
|
+
status: wrapUntrusted(data.status ?? undefined, 'elevenlabs:check_subscription:status'),
|
|
45
|
+
character_count: characterCount,
|
|
46
|
+
character_limit: characterLimit,
|
|
47
|
+
characters_remaining: remaining,
|
|
48
|
+
next_character_count_reset_unix: resetUnix,
|
|
49
|
+
next_character_count_reset_iso: resetIso,
|
|
50
|
+
voice_slots_used: data.voice_slots_used,
|
|
51
|
+
voice_limit: data.voice_limit,
|
|
52
|
+
cost: 'FREE — no credits consumed',
|
|
53
|
+
message: `${remaining.toLocaleString()} of ${characterLimit.toLocaleString()} characters remaining` +
|
|
54
|
+
(resetIso ? `; next reset ${resetIso}` : '') +
|
|
55
|
+
'. See tier field for plan name.',
|
|
56
|
+
hint: 'Call this before large batch generations. Quota errors elsewhere should be resolved by waiting for reset or upgrading the plan.',
|
|
57
|
+
});
|
|
58
|
+
}));
|
|
59
|
+
server.registerTool('list_models', {
|
|
60
|
+
description: `List ElevenLabs models with languages and capability flags.
|
|
61
|
+
|
|
62
|
+
WHEN TO USE:
|
|
63
|
+
- Pick a TTS model_id for generate_speech (e.g. eleven_v3, eleven_multilingual_v2)
|
|
64
|
+
- Verify a model supports the language or capability you need before calling generation tools
|
|
65
|
+
- Discover model IDs after an invalid model_id error
|
|
66
|
+
|
|
67
|
+
EXAMPLE: {} (no arguments)
|
|
68
|
+
|
|
69
|
+
RELATED TOOLS:
|
|
70
|
+
- generate_speech: consumes a model_id from this list
|
|
71
|
+
- check_subscription: confirm credits before generation
|
|
72
|
+
|
|
73
|
+
RETURNS: models[] with model_id, name, languages[], and capability booleans (TTS, voice conversion, finetuning).
|
|
74
|
+
|
|
75
|
+
COST: FREE — no credits consumed.`,
|
|
76
|
+
inputSchema: z.object({}),
|
|
77
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
78
|
+
}, withErrorHandling(async () => {
|
|
79
|
+
const apiKey = getApiKey();
|
|
80
|
+
if (!apiKey) {
|
|
81
|
+
throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
|
|
82
|
+
}
|
|
83
|
+
const raw = await elevenLabsJson(apiKey, ENDPOINTS.MODELS);
|
|
84
|
+
const models = (Array.isArray(raw) ? raw : []).map((m) => ({
|
|
85
|
+
model_id: m.model_id,
|
|
86
|
+
name: wrapUntrusted(m.name, 'elevenlabs:list_models:name'),
|
|
87
|
+
can_do_text_to_speech: m.can_do_text_to_speech,
|
|
88
|
+
can_do_voice_conversion: m.can_do_voice_conversion,
|
|
89
|
+
can_be_finetuned: m.can_be_finetuned,
|
|
90
|
+
token_cost_factor: m.token_cost_factor,
|
|
91
|
+
languages: (m.languages ?? []).map((lang) => ({
|
|
92
|
+
language_id: lang.language_id,
|
|
93
|
+
name: wrapUntrusted(lang.name, 'elevenlabs:list_models:language_name'),
|
|
94
|
+
})),
|
|
95
|
+
}));
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
ok: true,
|
|
98
|
+
models,
|
|
99
|
+
count: models.length,
|
|
100
|
+
cost: 'FREE — no credits consumed',
|
|
101
|
+
message: `Found ${models.length} model${models.length === 1 ? '' : 's'}. Use model_id with generate_speech.`,
|
|
102
|
+
});
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getApiKey } from '../auth.js';
|
|
3
|
+
import { elevenLabsJson } from '../client.js';
|
|
4
|
+
import { ENDPOINTS } from '../endpoints.js';
|
|
5
|
+
import { ElevenLabsError } from '../types.js';
|
|
6
|
+
import { wrapUntrusted } from '../untrusted-content.js';
|
|
7
|
+
import { withErrorHandling } from '../utils.js';
|
|
8
|
+
import { readSandboxedFile, sandboxedFileToBlob } from './file-input.js';
|
|
9
|
+
export function registerAlignmentTools(server) {
|
|
10
|
+
server.registerTool('forced_alignment', {
|
|
11
|
+
description: `Align a transcript to audio and return per-word timestamps.
|
|
12
|
+
|
|
13
|
+
WHEN TO USE:
|
|
14
|
+
- Build karaoke-style captions or precise edit markers from audio + transcript
|
|
15
|
+
- Verify that spoken words match a provided script
|
|
16
|
+
|
|
17
|
+
EXAMPLE: {"file_path": "/path/to/clip.mp3", "text": "Hello world."}
|
|
18
|
+
|
|
19
|
+
RELATED TOOLS:
|
|
20
|
+
- transcribe_audio: generate transcript text from audio alone
|
|
21
|
+
- generate_speech: create audio from text (inverse workflow)
|
|
22
|
+
|
|
23
|
+
RETURNS: words[] with enveloped aligned text and start/end times, plus loss score. Input transcript text is caller-supplied and not echoed raw.
|
|
24
|
+
|
|
25
|
+
COST: Credits based on audio duration.`,
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
file_path: z.string().min(1).describe('Absolute path to local audio inside MCP_WORKSPACE_PATH (or os.tmpdir()).'),
|
|
28
|
+
text: z.string().min(1).describe('Transcript text to align against the audio.'),
|
|
29
|
+
}),
|
|
30
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
31
|
+
}, withErrorHandling(async (args) => {
|
|
32
|
+
const apiKey = getApiKey();
|
|
33
|
+
if (!apiKey) {
|
|
34
|
+
throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
|
|
35
|
+
}
|
|
36
|
+
const fileInput = readSandboxedFile(args.file_path);
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
formData.append('file', sandboxedFileToBlob(fileInput), fileInput.fileName);
|
|
39
|
+
formData.append('text', args.text);
|
|
40
|
+
const data = await elevenLabsJson(apiKey, ENDPOINTS.FORCED_ALIGNMENT, { method: 'POST', body: formData });
|
|
41
|
+
const words = (data.words ?? []).map((w) => ({
|
|
42
|
+
text: wrapUntrusted(w.text, 'elevenlabs:forced_alignment:word_text'),
|
|
43
|
+
start: w.start,
|
|
44
|
+
end: w.end,
|
|
45
|
+
}));
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
ok: true,
|
|
48
|
+
words,
|
|
49
|
+
word_count: words.length,
|
|
50
|
+
loss: data.loss,
|
|
51
|
+
message: `Aligned ${words.length} word${words.length === 1 ? '' : 's'} to audio.`,
|
|
52
|
+
});
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=alignment.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getApiKey } from '../auth.js';
|
|
3
|
+
import { elevenLabsAudio } from '../client.js';
|
|
4
|
+
import { ENDPOINTS } from '../endpoints.js';
|
|
5
|
+
import { ElevenLabsError } from '../types.js';
|
|
6
|
+
import { withErrorHandling } from '../utils.js';
|
|
7
|
+
import { readSandboxedFile, sandboxedFileToBlob } from './file-input.js';
|
|
8
|
+
export function registerAudioIsolationTools(server) {
|
|
9
|
+
server.registerTool('isolate_audio', {
|
|
10
|
+
description: `Remove background noise from an audio file (audio isolation).
|
|
11
|
+
|
|
12
|
+
WHEN TO USE:
|
|
13
|
+
- Clean up meeting recordings or voice memos with background noise
|
|
14
|
+
- Prepare a cleaner clip before transcription or voice cloning
|
|
15
|
+
- Source audio is at least ~4.6 seconds long (shorter clips fail upstream)
|
|
16
|
+
|
|
17
|
+
COMMON MISTAKES:
|
|
18
|
+
- Clips under ~4.6 seconds — the API rejects them; trim/merge or pick a longer sample first
|
|
19
|
+
|
|
20
|
+
EXAMPLE: {"audio_path": "/path/to/noisy.mp3"}
|
|
21
|
+
|
|
22
|
+
RELATED TOOLS:
|
|
23
|
+
- transcribe_audio: transcribe the isolated clip
|
|
24
|
+
- speech_to_speech: apply a different voice after isolation
|
|
25
|
+
- clone_voice: clone from a cleaner sample
|
|
26
|
+
|
|
27
|
+
RETURNS: file_path and size_bytes for the isolated audio (saved under os.tmpdir()).
|
|
28
|
+
|
|
29
|
+
COST: Credits based on audio duration.`,
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
audio_path: z.string().min(1).describe('Absolute path to local audio inside MCP_WORKSPACE_PATH (or os.tmpdir()).'),
|
|
32
|
+
}),
|
|
33
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
34
|
+
}, withErrorHandling(async (args) => {
|
|
35
|
+
const apiKey = getApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
|
|
38
|
+
}
|
|
39
|
+
const fileInput = readSandboxedFile(args.audio_path);
|
|
40
|
+
const formData = new FormData();
|
|
41
|
+
formData.append('audio', sandboxedFileToBlob(fileInput), fileInput.fileName);
|
|
42
|
+
const result = await elevenLabsAudio(apiKey, ENDPOINTS.AUDIO_ISOLATION, { method: 'POST', body: formData }, 'mp3');
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
ok: true,
|
|
45
|
+
file_path: result.filePath,
|
|
46
|
+
size_bytes: result.sizeBytes,
|
|
47
|
+
message: `Isolated audio saved to ${result.filePath} (${(result.sizeBytes / 1024).toFixed(1)} KB).`,
|
|
48
|
+
});
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=audio-isolation.js.map
|