@mindstone/mcp-server-elevenlabs 0.2.2 → 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.
Files changed (41) hide show
  1. package/README.md +68 -4
  2. package/dist/bridge.d.ts +1 -0
  3. package/dist/client.d.ts +34 -3
  4. package/dist/client.js +141 -35
  5. package/dist/endpoints.d.ts +34 -0
  6. package/dist/endpoints.js +37 -0
  7. package/dist/error-detail.d.ts +5 -0
  8. package/dist/error-detail.js +20 -0
  9. package/dist/server.js +9 -1
  10. package/dist/tools/account.d.ts +3 -0
  11. package/dist/tools/account.js +105 -0
  12. package/dist/tools/alignment.d.ts +3 -0
  13. package/dist/tools/alignment.js +55 -0
  14. package/dist/tools/audio-isolation.d.ts +3 -0
  15. package/dist/tools/audio-isolation.js +51 -0
  16. package/dist/tools/configure.js +18 -6
  17. package/dist/tools/dialogue.d.ts +3 -0
  18. package/dist/tools/dialogue.js +70 -0
  19. package/dist/tools/dubbing.d.ts +3 -0
  20. package/dist/tools/dubbing.js +234 -0
  21. package/dist/tools/file-input.d.ts +28 -0
  22. package/dist/tools/file-input.js +77 -0
  23. package/dist/tools/index.d.ts +8 -0
  24. package/dist/tools/index.js +8 -0
  25. package/dist/tools/music.js +152 -27
  26. package/dist/tools/path-safety.d.ts +1 -0
  27. package/dist/tools/path-safety.js +4 -1
  28. package/dist/tools/speech.js +90 -34
  29. package/dist/tools/transcription.js +34 -61
  30. package/dist/tools/voice-changer.d.ts +3 -0
  31. package/dist/tools/voice-changer.js +62 -0
  32. package/dist/tools/voice-clone.d.ts +3 -0
  33. package/dist/tools/voice-clone.js +104 -0
  34. package/dist/tools/voice-design.d.ts +3 -0
  35. package/dist/tools/voice-design.js +153 -0
  36. package/dist/tools/voices.js +167 -13
  37. package/dist/types.d.ts +110 -13
  38. package/dist/types.js +20 -4
  39. package/dist/untrusted-content.d.ts +45 -0
  40. package/dist/untrusted-content.js +104 -0
  41. package/package.json +2 -2
package/README.md CHANGED
@@ -5,11 +5,51 @@
5
5
 
6
6
  ElevenLabs MCP server for Model Context Protocol hosts. Generate speech, music, and sound effects, browse voices, and transcribe audio using the ElevenLabs API through a standardised MCP interface.
7
7
 
8
+ ## Status
9
+
10
+ - **Version:** [0.4.0](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-elevenlabs)
11
+ - **Auth:** API key ([`ELEVENLABS_API_KEY`](./server.json))
12
+ - **Tools:** [24](./src/tools/) (account, voices, speech, music, transcription, voice conversion, isolation, alignment, cloning, dialogue, voice design, dubbing)
13
+ - **Surface:** cloud-api
14
+ - **Machine-readable:** [`STATUS.json`](./STATUS.json)
15
+
8
16
  ## Requirements
9
17
 
10
18
  - Node.js 20+
11
19
  - npm
12
20
 
21
+ <!-- BEGIN INSTALL_LINKS: do not edit by hand; regenerated by scripts/gen-install-links.mjs -->
22
+ ## One-click install
23
+
24
+ [![Add to Cursor](https://img.shields.io/badge/Add_to_Cursor-black?style=for-the-badge&logo=cursor&logoColor=white)](cursor://anysphere.cursor-deeplink/mcp/install?name=ElevenLabs&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtaW5kc3RvbmUvbWNwLXNlcnZlci1lbGV2ZW5sYWJzIl0sImVudiI6eyJFTEVWRU5MQUJTX0FQSV9LRVkiOiIifX0)
25
+ [![Add to VS Code](https://img.shields.io/badge/Add_to_VS_Code-007ACC?style=for-the-badge&logo=visual-studio-code&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22ElevenLabs%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40mindstone%2Fmcp-server-elevenlabs%22%5D%2C%22env%22%3A%7B%22ELEVENLABS_API_KEY%22%3A%22%22%7D%7D)
26
+ [![Add to VS Code Insiders](https://img.shields.io/badge/Add_to_VS_Code_Insiders-24bfa5?style=for-the-badge&logo=visual-studio-code&logoColor=white)](vscode-insiders:mcp/install?%7B%22name%22%3A%22ElevenLabs%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40mindstone%2Fmcp-server-elevenlabs%22%5D%2C%22env%22%3A%7B%22ELEVENLABS_API_KEY%22%3A%22%22%7D%7D)
27
+
28
+ After clicking the button, your host will prompt you to fill: `ELEVENLABS_API_KEY`.
29
+
30
+ <details>
31
+ <summary>Manual config for Claude Desktop / Claude Code / Goose / Continue.dev (ElevenLabs)</summary>
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "ElevenLabs": {
37
+ "command": "npx",
38
+ "args": [
39
+ "-y",
40
+ "@mindstone/mcp-server-elevenlabs"
41
+ ],
42
+ "env": {
43
+ "ELEVENLABS_API_KEY": ""
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ </details>
51
+ <!-- END INSTALL_LINKS -->
52
+
13
53
  ## Quick Start
14
54
 
15
55
  ### Install & build
@@ -74,17 +114,39 @@ node dist/index.js
74
114
  }
75
115
  ```
76
116
 
77
- ## Tools (8)
117
+ ## Tools (24)
78
118
 
79
119
  ### Configuration
80
120
  - `configure_elevenlabs_api_key` — Save your ElevenLabs API key
81
121
 
82
- ### Voices
83
- - `list_voices` — Search and browse available ElevenLabs voices
122
+ ### Account & discovery (FREE)
123
+ - `check_subscription` — Check subscription tier and character credit usage
124
+ - `list_models` — List TTS models with languages and capabilities
84
125
 
85
- ### Speech
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
86
136
  - `generate_speech` — Generate spoken audio from text using text-to-speech
87
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`)
88
150
 
89
151
  ### Music
90
152
  - `generate_music` — Generate music from a text prompt
@@ -94,6 +156,8 @@ node dist/index.js
94
156
  ### Transcription
95
157
  - `transcribe_audio` — Transcribe speech from an audio file to text
96
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
+
97
161
  ## Licence
98
162
 
99
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
@@ -12,5 +12,6 @@ export declare const bridgeRequest: (urlPath: string, body: Record<string, unkno
12
12
  success: boolean;
13
13
  warning?: string;
14
14
  error?: string;
15
+ message?: string;
15
16
  }>;
16
17
  //# sourceMappingURL=bridge.d.ts.map
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?: RequestInit): Promise<Response>;
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?: RequestInit): Promise<T>;
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?: RequestInit, fileExtension?: string): Promise<AudioResult>;
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
- const ELEVENLABS_API_BASE = 'https://api.elevenlabs.io/v1';
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
- : `${ELEVENLABS_API_BASE}${urlPath}`;
28
+ : `${ELEVENLABS_API_V1_BASE}${urlPath}`;
29
+ const { timeoutMs, ...fetchOptions } = options;
28
30
  const headers = {
29
31
  'xi-api-key': apiKey,
30
- ...(options.headers || {}),
32
+ ...(fetchOptions.headers || {}),
31
33
  };
32
34
  // Only set Content-Type for JSON bodies (not FormData)
33
- if (!(options.body instanceof FormData) && !headers['Content-Type']) {
35
+ if (!(fetchOptions.body instanceof FormData) && !headers['Content-Type']) {
34
36
  headers['Content-Type'] = 'application/json';
35
37
  }
36
- console.error(`[ElevenLabs API] ${options.method || 'GET'} ${url}`);
38
+ console.error(`[ElevenLabs API] ${fetchOptions.method || 'GET'} ${url}`);
37
39
  let response;
38
40
  try {
39
41
  response = await fetch(url, {
40
- ...options,
41
- signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
42
+ ...fetchOptions,
43
+ signal: fetchOptions.signal ?? AbortSignal.timeout(timeoutMs ?? REQUEST_TIMEOUT_MS),
42
44
  headers,
43
45
  });
44
46
  }
@@ -52,41 +54,104 @@ export async function elevenLabsFetch(apiKey, urlPath, options = {}) {
52
54
  if (response.status === 429) {
53
55
  throw new ElevenLabsError('Rate limited. Please wait a moment before retrying.', 'RATE_LIMITED', getErrorResolution(429));
54
56
  }
55
- // Handle auth errors
57
+ // Handle auth errors. ElevenLabs returns 401 both for genuinely invalid
58
+ // keys AND for valid keys missing a specific scope (e.g. sound_generation).
59
+ // The latter ships a `detail.status = "missing_permissions"` payload that
60
+ // the user needs to see in order to fix the key permissions; previously we
61
+ // threw it away and emitted a bare "Authentication failed".
56
62
  if (response.status === 401) {
57
- throw new ElevenLabsError('Authentication failed', 'AUTH_FAILED', getErrorResolution(401));
63
+ const detail = await extractErrorDetail(response);
64
+ const isMissingPermission = detail.includes('missing_permissions');
65
+ const envelopedDetail = detail ? envelopeApiErrorDetail(detail) : '';
66
+ throw new ElevenLabsError(isMissingPermission
67
+ ? `API key missing required permission: ${envelopedDetail}`
68
+ : detail
69
+ ? formatApiErrorMessage('Authentication failed', detail)
70
+ : 'Authentication failed', isMissingPermission ? 'MISSING_PERMISSION' : 'AUTH_FAILED', isMissingPermission
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.'
72
+ : getErrorResolution(401));
58
73
  }
59
74
  if (response.status === 403) {
60
- let detail = '';
61
- try {
62
- const errBody = await response.clone().json();
63
- if (typeof errBody.detail === 'string') {
64
- detail = errBody.detail;
65
- }
66
- else if (errBody.detail?.message) {
67
- detail = errBody.detail.message;
68
- }
69
- }
70
- catch { /* not JSON */ }
71
- throw new ElevenLabsError(`Access forbidden: ${detail || 'insufficient permissions or quota'}`, 'AUTH_FAILED', getErrorResolution(403, detail));
75
+ const detail = await extractErrorDetail(response);
76
+ throw new ElevenLabsError(detail
77
+ ? formatApiErrorMessage('Access forbidden', detail)
78
+ : 'Access forbidden: insufficient permissions or quota', 'AUTH_FAILED', getErrorResolution(403, detail));
72
79
  }
73
80
  // Handle other errors
74
81
  if (!response.ok) {
75
- let detail = '';
76
- try {
77
- const errBody = await response.clone().json();
78
- if (typeof errBody.detail === 'string') {
79
- detail = errBody.detail;
80
- }
81
- else if (errBody.detail?.message) {
82
- detail = errBody.detail.message;
83
- }
84
- }
85
- catch { /* not JSON */ }
86
- throw new ElevenLabsError(`ElevenLabs API error (HTTP ${response.status}): ${detail || response.statusText}`, `HTTP_${response.status}`, getErrorResolution(response.status, detail));
82
+ const detail = await extractErrorDetail(response);
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));
87
89
  }
88
90
  return response;
89
91
  }
92
+ /**
93
+ * Extract a human-readable detail message from an ElevenLabs error response.
94
+ *
95
+ * The API uses several detail shapes:
96
+ * - `detail: "string message"` (legacy)
97
+ * - `detail: { message: "...", status?: "..." }` (current auth/permission errors)
98
+ * - `detail: [{ type, loc: ["body", "field", ...], msg, input }, ...]` (FastAPI 422)
99
+ *
100
+ * The 422 array form is the one that bit us in
101
+ * `generate_music_from_plan`: previously we threw away the field-level info
102
+ * and left the user with `HTTP 422: unknown`. We now flatten the validation
103
+ * errors into something like
104
+ * `body.composition_plan.sections.0.section_name: Field required;
105
+ * body.composition_plan.sections.0.lines: Field required`
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`).
110
+ */
111
+ export async function extractErrorDetail(response) {
112
+ try {
113
+ const errBody = (await response.clone().json());
114
+ const d = errBody?.detail;
115
+ if (d == null)
116
+ return '';
117
+ if (typeof d === 'string')
118
+ return d;
119
+ if (Array.isArray(d)) {
120
+ // FastAPI validation errors → "loc.path: msg" joined by "; ".
121
+ // Skip null/non-object entries defensively rather than throwing.
122
+ const parts = d
123
+ .map((e) => {
124
+ if (!e || typeof e !== 'object')
125
+ return '';
126
+ const entry = e;
127
+ const loc = Array.isArray(entry.loc) ? entry.loc.join('.') : '';
128
+ const msg = typeof entry.msg === 'string'
129
+ ? entry.msg
130
+ : typeof entry.type === 'string'
131
+ ? entry.type
132
+ : 'invalid';
133
+ return loc ? `${loc}: ${msg}` : msg;
134
+ })
135
+ .filter((s) => s.length > 0);
136
+ return parts.join('; ');
137
+ }
138
+ if (typeof d === 'object') {
139
+ const o = d;
140
+ const parts = [];
141
+ if (typeof o.status === 'string')
142
+ parts.push(o.status);
143
+ if (typeof o.message === 'string')
144
+ parts.push(o.message);
145
+ if (typeof o.cause === 'string')
146
+ parts.push(`cause: ${o.cause}`);
147
+ return parts.join(' — ');
148
+ }
149
+ return '';
150
+ }
151
+ catch {
152
+ return '';
153
+ }
154
+ }
90
155
  /**
91
156
  * Make a JSON API call and parse the response.
92
157
  */
@@ -94,13 +159,54 @@ export async function elevenLabsJson(apiKey, urlPath, options = {}) {
94
159
  const response = await elevenLabsFetch(apiKey, urlPath, options);
95
160
  return (await response.json());
96
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
+ }
97
181
  /**
98
182
  * Make an API call that returns raw audio binary. Save to file and return path.
99
183
  */
100
- export async function elevenLabsAudio(apiKey, urlPath, options = {}, fileExtension = 'mp3') {
184
+ export async function elevenLabsAudio(apiKey, urlPath, options = {}, fileExtension) {
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 = {}) {
101
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);
102
208
  const buffer = Buffer.from(await response.arrayBuffer());
103
- const fileName = `elevenlabs_${crypto.randomUUID()}.${fileExtension}`;
209
+ const fileName = `elevenlabs_${crypto.randomUUID()}.${ext}`;
104
210
  const filePath = path.join(os.tmpdir(), fileName);
105
211
  fs.writeFileSync(filePath, buffer);
106
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,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerAccountTools(server: McpServer): void;
3
+ //# sourceMappingURL=account.d.ts.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,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerAlignmentTools(server: McpServer): void;
3
+ //# sourceMappingURL=alignment.d.ts.map