@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.
Files changed (41) hide show
  1. package/README.md +30 -7
  2. package/dist/bridge.d.ts +1 -0
  3. package/dist/client.d.ts +34 -3
  4. package/dist/client.js +68 -14
  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 +85 -25
  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 +57 -24
  29. package/dist/tools/transcription.js +27 -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 +91 -0
  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
@@ -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.3.0](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-elevenlabs)
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:** [8](./src/tools/) (voices, speech, music, transcription)
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 (8)
117
+ ## Tools (24)
119
118
 
120
119
  ### Configuration
121
120
  - `configure_elevenlabs_api_key` — Save your ElevenLabs API key
122
121
 
123
- ### Voices
124
- - `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
125
125
 
126
- ### 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
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
@@ -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
  }
@@ -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: ${detail}`
67
+ ? `API key missing required permission: ${envelopedDetail}`
65
68
  : detail
66
- ? `Authentication failed: ${detail}`
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(`Access forbidden: ${detail || 'insufficient permissions or quota'}`, 'AUTH_FAILED', getErrorResolution(403, detail));
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
- throw new ElevenLabsError(`ElevenLabs API error (HTTP ${response.status}): ${detail || response.statusText}`, `HTTP_${response.status}`, getErrorResolution(response.status, detail));
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 = 'mp3') {
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()}.${fileExtension}`;
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,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
@@ -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,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerAudioIsolationTools(server: McpServer): void;
3
+ //# sourceMappingURL=audio-isolation.d.ts.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