@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
@@ -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
@@ -3,11 +3,23 @@ import { setApiKey } from '../auth.js';
3
3
  import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
4
4
  import { ElevenLabsError } from '../types.js';
5
5
  import { withErrorHandling } from '../utils.js';
6
+ const CONFIGURED_MESSAGE = 'ElevenLabs API key configured successfully. Feature-specific permissions are checked on first use; call check_subscription to verify the account and credits now.';
6
7
  export function registerConfigureTools(server) {
7
8
  server.registerTool('configure_elevenlabs_api_key', {
8
- description: 'Save your ElevenLabs API key. Call this when the user provides their key. ' +
9
- 'WHERE TO GET A KEY: Go to https://elevenlabs.io/app/settings/api-keys → Create new API key → Copy the key (starts with "sk_"). ' +
10
- 'FREE TIER: 10,000 characters/month for TTS. Music generation requires a paid plan.',
9
+ description: `Save the user's ElevenLabs API key for this session.
10
+
11
+ WHEN TO USE:
12
+ - When the user provides their API key in chat
13
+ - After AUTH_REQUIRED errors from any other tool
14
+
15
+ EXAMPLE: {"api_key": "sk_..."}
16
+
17
+ RELATED TOOLS:
18
+ - check_subscription: verify the key and see credits after configuring
19
+
20
+ RETURNS: ok, message.
21
+
22
+ COST: FREE.`,
11
23
  inputSchema: z.object({
12
24
  api_key: z.string().min(1).describe('ElevenLabs API key (starts with "sk_").'),
13
25
  }),
@@ -21,8 +33,8 @@ export function registerConfigureTools(server) {
21
33
  if (result.success) {
22
34
  setApiKey(key);
23
35
  const message = result.warning
24
- ? `ElevenLabs API key configured successfully. Note: ${result.warning}`
25
- : 'ElevenLabs API key configured successfully! You can now generate music, speech, sound effects, and more.';
36
+ ? `${CONFIGURED_MESSAGE} Note: ${result.warning}`
37
+ : (result.message ?? CONFIGURED_MESSAGE);
26
38
  return JSON.stringify({ ok: true, message });
27
39
  }
28
40
  // Bridge returned failure — surface as error, do NOT fall through
@@ -39,7 +51,7 @@ export function registerConfigureTools(server) {
39
51
  setApiKey(key);
40
52
  return JSON.stringify({
41
53
  ok: true,
42
- message: 'ElevenLabs API key configured successfully! You can now generate music, speech, sound effects, and more.',
54
+ message: CONFIGURED_MESSAGE,
43
55
  });
44
56
  }));
45
57
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDialogueTools(server: McpServer): void;
3
+ //# sourceMappingURL=dialogue.d.ts.map
@@ -0,0 +1,70 @@
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, LONG_REQUEST_TIMEOUT_MS } from '../types.js';
6
+ import { withErrorHandling } from '../utils.js';
7
+ const dialogueInputSchema = z.object({
8
+ text: z.string().min(1).describe('Line of dialogue to speak.'),
9
+ voice_id: z.string().min(1).describe('Voice ID for this line (from list_voices).'),
10
+ });
11
+ export function registerDialogueTools(server) {
12
+ server.registerTool('text_to_dialogue', {
13
+ description: `Generate multi-voice dialogue audio from a script with one voice per line.
14
+
15
+ WHEN TO USE:
16
+ - Produce a conversation or script with different speakers
17
+ - Podcast-style back-and-forth with distinct voices per line
18
+
19
+ EXAMPLE: {"inputs": [{"text": "Hello there.", "voice_id": "21m00Tcm4TlvDq8ikWAM"}, {"text": "Hi!", "voice_id": "pNInz6obpgDQGcFmaJgB"}], "model_id": "eleven_v3"}
20
+
21
+ RELATED TOOLS:
22
+ - list_voices: pick voice_id values for each speaker
23
+ - generate_speech: single-voice TTS when you only need one narrator
24
+ - check_subscription: confirm credits before long scripts
25
+
26
+ RETURNS: file_path and size_bytes for the combined dialogue audio (tmp file).
27
+
28
+ COST: ~1 credit per 100 characters across all lines.`,
29
+ inputSchema: z.object({
30
+ inputs: z.array(dialogueInputSchema).min(1).describe('Ordered dialogue lines, each with text and voice_id.'),
31
+ model_id: z.enum([
32
+ 'eleven_v3',
33
+ 'eleven_multilingual_v2',
34
+ 'eleven_flash_v2_5',
35
+ 'eleven_turbo_v2_5',
36
+ ]).optional().describe('TTS model. Default: eleven_v3.'),
37
+ language_code: z.string().optional().describe('ISO 639-1 language code (e.g. en, es). Optional hint for pronunciation.'),
38
+ seed: z.number().int().optional().describe('Optional seed for reproducible generation.'),
39
+ }),
40
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
41
+ }, withErrorHandling(async (args) => {
42
+ const apiKey = getApiKey();
43
+ if (!apiKey) {
44
+ throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
45
+ }
46
+ const modelId = args.model_id ?? 'eleven_v3';
47
+ const body = {
48
+ inputs: args.inputs,
49
+ model_id: modelId,
50
+ };
51
+ if (args.language_code)
52
+ body.language_code = args.language_code;
53
+ if (args.seed != null)
54
+ body.seed = args.seed;
55
+ const audio = await elevenLabsAudio(apiKey, ENDPOINTS.TEXT_TO_DIALOGUE, {
56
+ method: 'POST',
57
+ body: JSON.stringify(body),
58
+ timeoutMs: LONG_REQUEST_TIMEOUT_MS,
59
+ });
60
+ return JSON.stringify({
61
+ ok: true,
62
+ file_path: audio.filePath,
63
+ size_bytes: audio.sizeBytes,
64
+ line_count: args.inputs.length,
65
+ model_id: modelId,
66
+ message: `Dialogue audio saved to ${audio.filePath} (${audio.sizeBytes} bytes, ${args.inputs.length} lines).`,
67
+ });
68
+ }));
69
+ }
70
+ //# sourceMappingURL=dialogue.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDubbingTools(server: McpServer): void;
3
+ //# sourceMappingURL=dubbing.d.ts.map
@@ -0,0 +1,234 @@
1
+ import { z } from 'zod';
2
+ import { getApiKey } from '../auth.js';
3
+ import { elevenLabsBinaryDownload, elevenLabsFetch, elevenLabsJson } from '../client.js';
4
+ import { ENDPOINTS } from '../endpoints.js';
5
+ import { ElevenLabsError, LONG_REQUEST_TIMEOUT_MS, } 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
+ const TERMINAL_DUBBING_STATUSES = new Set(['dubbed', 'failed', 'cancelled']);
10
+ const TERMINAL_STATUS_PHRASE = 'dubbed, failed, or cancelled';
11
+ function dubbingNextStep(status) {
12
+ if (status === 'dubbed') {
13
+ return 'Call download_dubbed_audio with dubbing_id and language_code.';
14
+ }
15
+ if (status === 'failed') {
16
+ return 'Inspect error_detail; fix source media or retry create_dubbing.';
17
+ }
18
+ if (status === 'cancelled') {
19
+ return 'Job was cancelled — do not poll further; submit a new create_dubbing if needed.';
20
+ }
21
+ return 'Poll get_dubbing again in ~10 seconds.';
22
+ }
23
+ function envelopDubbingStatusField(value, field) {
24
+ if (!value)
25
+ return undefined;
26
+ return wrapUntrusted(value, `elevenlabs:get_dubbing:${field}`);
27
+ }
28
+ export function registerDubbingTools(server) {
29
+ server.registerTool('create_dubbing', {
30
+ description: `Submit an async dubbing job (v1 API). You MUST poll get_dubbing until status is ${TERMINAL_STATUS_PHRASE}.
31
+
32
+ WHEN TO USE:
33
+ - Translate/dub existing audio or video into another language
34
+ - Localize a short clip the user already has on disk
35
+
36
+ EXAMPLE: {"file_path": "/path/in/workspace/clip.mp3", "target_lang": "es", "name": "rebel-live-test-dub"}
37
+
38
+ RELATED TOOLS:
39
+ - get_dubbing: poll job status (every ~10s; respect expected_duration_sec from this response)
40
+ - download_dubbed_audio: fetch audio once status is dubbed
41
+ - delete_dubbing: cleanup test jobs
42
+
43
+ RETURNS: dubbing_id and expected_duration_sec. The job runs server-side — poll get_dubbing; do not assume instant completion.
44
+
45
+ COST: Dubbing credits per minute of source media.`,
46
+ inputSchema: z.object({
47
+ target_lang: z.string().min(2).describe('Target language code (e.g. es, fr, de). Required.'),
48
+ file_path: z.string().optional().describe('Local audio/video file inside MCP_WORKSPACE_PATH (multipart field file).'),
49
+ source_url: z.string().url().optional().describe('Alternatively, a URL ElevenLabs fetches server-side (no local sandbox).'),
50
+ name: z.string().optional().describe('Optional job label (echoed by get_dubbing).'),
51
+ source_lang: z.string().optional().describe('Source language code when known (auto-detect when omitted).'),
52
+ num_speakers: z.number().int().min(1).optional().describe('Number of speakers when known.'),
53
+ watermark: z.boolean().optional().describe('Apply watermark when supported. Default: false.'),
54
+ }),
55
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
56
+ }, withErrorHandling(async (args) => {
57
+ const apiKey = getApiKey();
58
+ if (!apiKey) {
59
+ throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
60
+ }
61
+ if (!args.file_path && !args.source_url) {
62
+ throw new ElevenLabsError('Provide either file_path (local sandboxed file) or source_url (ElevenLabs-side fetch).', 'INVALID_INPUT', 'Pass file_path for a clip in MCP_WORKSPACE_PATH, or source_url for a remote asset.');
63
+ }
64
+ if (args.file_path && args.source_url) {
65
+ throw new ElevenLabsError('Provide only one of file_path or source_url, not both.', 'INVALID_INPUT', 'Choose a local file_path OR a remote source_url.');
66
+ }
67
+ const formData = new FormData();
68
+ formData.append('target_lang', args.target_lang);
69
+ if (args.name)
70
+ formData.append('name', args.name);
71
+ if (args.source_lang)
72
+ formData.append('source_lang', args.source_lang);
73
+ if (args.num_speakers != null)
74
+ formData.append('num_speakers', String(args.num_speakers));
75
+ formData.append('watermark', String(args.watermark ?? false));
76
+ if (args.file_path) {
77
+ const fileInput = readSandboxedFile(args.file_path);
78
+ formData.append('file', sandboxedFileToBlob(fileInput), fileInput.fileName);
79
+ }
80
+ else if (args.source_url) {
81
+ formData.append('source_url', args.source_url);
82
+ }
83
+ let data;
84
+ try {
85
+ data = await elevenLabsJson(apiKey, ENDPOINTS.DUBBING, { method: 'POST', body: formData, timeoutMs: LONG_REQUEST_TIMEOUT_MS });
86
+ }
87
+ catch (error) {
88
+ if (error instanceof ElevenLabsError && error.code === 'TIMEOUT') {
89
+ throw new ElevenLabsError(error.message, 'TIMEOUT', 'The submit may have timed out after uploading, but the job might still be processing. Call get_dubbing with the dubbing_id if you have one, or check recent jobs in the ElevenLabs dashboard before resubmitting to avoid duplicate jobs.');
90
+ }
91
+ throw error;
92
+ }
93
+ const expectedSec = data.expected_duration_sec;
94
+ return JSON.stringify({
95
+ ok: true,
96
+ dubbing_id: data.dubbing_id,
97
+ expected_duration_sec: expectedSec,
98
+ target_lang: args.target_lang,
99
+ message: `Dubbing job ${data.dubbing_id} submitted. You MUST poll get_dubbing every ~10 seconds until status is ${TERMINAL_STATUS_PHRASE}${expectedSec != null ? ` (expected ~${expectedSec}s)` : ''}.`,
100
+ poll_hint: `Call get_dubbing with this dubbing_id until status is ${TERMINAL_STATUS_PHRASE}; when dubbed, call download_dubbed_audio.`,
101
+ });
102
+ }));
103
+ server.registerTool('get_dubbing', {
104
+ description: `Poll dubbing job status. Call repeatedly after create_dubbing until status is ${TERMINAL_STATUS_PHRASE}.
105
+
106
+ WHEN TO USE:
107
+ - After create_dubbing — poll every ~10s (respect expected_duration_sec)
108
+ - Check whether a dub failed before retrying
109
+
110
+ EXAMPLE: {"dubbing_id": "dub_abc123"}
111
+
112
+ RELATED TOOLS:
113
+ - create_dubbing: submit the job
114
+ - download_dubbed_audio: fetch audio when status is dubbed
115
+ - delete_dubbing: remove test jobs
116
+
117
+ RETURNS: status (verbatim, incl. failed), enveloped name and error detail when present.
118
+
119
+ COST: FREE — status read only.`,
120
+ inputSchema: z.object({
121
+ dubbing_id: z.string().min(1).describe('Dubbing job ID from create_dubbing.'),
122
+ }),
123
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
124
+ }, withErrorHandling(async (args) => {
125
+ const apiKey = getApiKey();
126
+ if (!apiKey) {
127
+ throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
128
+ }
129
+ let data;
130
+ try {
131
+ data = await elevenLabsJson(apiKey, ENDPOINTS.dubbing(args.dubbing_id));
132
+ }
133
+ catch (error) {
134
+ if (error instanceof ElevenLabsError && error.code === 'HTTP_404') {
135
+ throw new ElevenLabsError(`Dubbing job not found: ${args.dubbing_id}`, 'DUBBING_NOT_FOUND', 'Verify the dubbing_id from create_dubbing or list recent jobs in the ElevenLabs dashboard.');
136
+ }
137
+ throw error;
138
+ }
139
+ const status = data.status;
140
+ const errorDetail = data.error_message ?? data.error;
141
+ const isTerminal = TERMINAL_DUBBING_STATUSES.has(status);
142
+ return JSON.stringify({
143
+ ok: true,
144
+ dubbing_id: data.dubbing_id ?? args.dubbing_id,
145
+ status,
146
+ name: envelopDubbingStatusField(data.name, 'name'),
147
+ target_languages: data.target_languages,
148
+ error_detail: envelopDubbingStatusField(errorDetail, 'error_detail'),
149
+ is_terminal: isTerminal,
150
+ message: isTerminal
151
+ ? `Dubbing ${args.dubbing_id} reached terminal status: ${status}.`
152
+ : `Dubbing ${args.dubbing_id} status: ${status}. Keep polling get_dubbing until ${TERMINAL_STATUS_PHRASE}.`,
153
+ next_step: dubbingNextStep(status),
154
+ });
155
+ }));
156
+ server.registerTool('download_dubbed_audio', {
157
+ description: `Download dubbed audio for a completed dubbing job.
158
+
159
+ WHEN TO USE:
160
+ - After get_dubbing reports status dubbed
161
+ - Fetch the localized track for a target language
162
+
163
+ EXAMPLE: {"dubbing_id": "dub_abc123", "language_code": "es"}
164
+
165
+ RELATED TOOLS:
166
+ - get_dubbing: confirm status is dubbed before downloading
167
+ - delete_dubbing: cleanup after testing
168
+
169
+ RETURNS: file_path and size_bytes (extension sniffed from Content-Type).
170
+
171
+ COST: FREE — download only (generation credits charged at submit).`,
172
+ inputSchema: z.object({
173
+ dubbing_id: z.string().min(1).describe('Dubbing job ID.'),
174
+ language_code: z.string().min(2).describe('Target language code used when creating the dub (e.g. es).'),
175
+ }),
176
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
177
+ }, withErrorHandling(async (args) => {
178
+ const apiKey = getApiKey();
179
+ if (!apiKey) {
180
+ throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
181
+ }
182
+ const audio = await elevenLabsBinaryDownload(apiKey, ENDPOINTS.dubbingAudio(args.dubbing_id, args.language_code));
183
+ return JSON.stringify({
184
+ ok: true,
185
+ file_path: audio.filePath,
186
+ size_bytes: audio.sizeBytes,
187
+ dubbing_id: args.dubbing_id,
188
+ language_code: args.language_code,
189
+ message: `Dubbed audio saved to ${audio.filePath} (${audio.sizeBytes} bytes).`,
190
+ });
191
+ }));
192
+ server.registerTool('delete_dubbing', {
193
+ description: `Permanently delete a dubbing job and its outputs.
194
+
195
+ WHEN TO USE:
196
+ - Cleanup rebel-live-test-* dubbing jobs after live tests
197
+ - Remove a failed or unwanted dub from the account
198
+
199
+ EXAMPLE: {"dubbing_id": "dub_abc123"}
200
+
201
+ RELATED TOOLS:
202
+ - create_dubbing / get_dubbing / download_dubbed_audio: the dubbing lifecycle
203
+
204
+ RETURNS: ok confirmation. Irreversible.
205
+
206
+ COST: FREE — no generation; permanently removes the dubbing job.`,
207
+ inputSchema: z.object({
208
+ dubbing_id: z.string().min(1).describe('Dubbing job ID to delete.'),
209
+ }),
210
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
211
+ }, withErrorHandling(async (args) => {
212
+ const apiKey = getApiKey();
213
+ if (!apiKey) {
214
+ throw new ElevenLabsError('ElevenLabs API key not configured', 'AUTH_REQUIRED', 'Ask the user for their API key, then call configure_elevenlabs_api_key.');
215
+ }
216
+ try {
217
+ await elevenLabsFetch(apiKey, ENDPOINTS.dubbing(args.dubbing_id), {
218
+ method: 'DELETE',
219
+ });
220
+ }
221
+ catch (error) {
222
+ if (error instanceof ElevenLabsError && error.code === 'HTTP_404') {
223
+ throw new ElevenLabsError(`Dubbing job not found: ${args.dubbing_id}`, 'DUBBING_NOT_FOUND', 'The job may already be deleted.');
224
+ }
225
+ throw error;
226
+ }
227
+ return JSON.stringify({
228
+ ok: true,
229
+ dubbing_id: args.dubbing_id,
230
+ message: `Dubbing job ${args.dubbing_id} deleted permanently.`,
231
+ });
232
+ }));
233
+ }
234
+ //# sourceMappingURL=dubbing.js.map
@@ -0,0 +1,28 @@
1
+ export interface SandboxedFileInput {
2
+ buffer: Buffer;
3
+ fileName: string;
4
+ verifiedPath: string;
5
+ }
6
+ /**
7
+ * Read bytes from a local file path constrained to the MCP_WORKSPACE_PATH
8
+ * sandbox (or os.tmpdir() when unset).
9
+ *
10
+ * Security invariants (R1 — see planning doc Refactor Assessment):
11
+ * 1. Remote URL inputs bypass the sandbox → FILE_NOT_FOUND at existsSync.
12
+ * 2. Out-of-root lexical path → PATH_SANDBOX_VIOLATION before any disk read.
13
+ * 3. In-root-but-missing file → FILE_NOT_FOUND.
14
+ * 4. Symlink inside root pointing outside root → refused.
15
+ * 5. Last-moment realpathSync re-canonicalisation before read (TOCTOU).
16
+ *
17
+ * Callers own multipart field naming (`file` vs `audio`); this helper
18
+ * returns `{ buffer, fileName, verifiedPath }` only.
19
+ */
20
+ export declare function readSandboxedFile(rawFilePath: string): SandboxedFileInput;
21
+ /**
22
+ * Resolve a MIME type from a file name extension for multipart uploads.
23
+ * Defaults to audio/mpeg when the extension is unknown (most uploads are audio).
24
+ */
25
+ export declare function mimeTypeForFileName(fileName: string): string;
26
+ /** Build a typed Blob from sandboxed file bytes for FormData multipart parts. */
27
+ export declare function sandboxedFileToBlob({ buffer, fileName }: SandboxedFileInput): Blob;
28
+ //# sourceMappingURL=file-input.d.ts.map
@@ -0,0 +1,77 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { ElevenLabsError } from '../types.js';
4
+ import { getAudioWorkspaceRoot, isInsideAudioWorkspaceRoot, isRemoteUrl, resolveAudioPath } from './path-safety.js';
5
+ const FILE_INPUT_RESOLUTION = 'Provide a path to an existing audio file inside MCP_WORKSPACE_PATH (or os.tmpdir() when unset). Remote URLs are not supported.';
6
+ /**
7
+ * Read bytes from a local file path constrained to the MCP_WORKSPACE_PATH
8
+ * sandbox (or os.tmpdir() when unset).
9
+ *
10
+ * Security invariants (R1 — see planning doc Refactor Assessment):
11
+ * 1. Remote URL inputs bypass the sandbox → FILE_NOT_FOUND at existsSync.
12
+ * 2. Out-of-root lexical path → PATH_SANDBOX_VIOLATION before any disk read.
13
+ * 3. In-root-but-missing file → FILE_NOT_FOUND.
14
+ * 4. Symlink inside root pointing outside root → refused.
15
+ * 5. Last-moment realpathSync re-canonicalisation before read (TOCTOU).
16
+ *
17
+ * Callers own multipart field naming (`file` vs `audio`); this helper
18
+ * returns `{ buffer, fileName, verifiedPath }` only.
19
+ */
20
+ export function readSandboxedFile(rawFilePath) {
21
+ let filePath;
22
+ if (isRemoteUrl(rawFilePath)) {
23
+ // Preserve pre-existing behaviour for URL inputs: existsSync below
24
+ // returns FILE_NOT_FOUND (non-sandbox error code).
25
+ filePath = rawFilePath;
26
+ }
27
+ else {
28
+ const resolution = resolveAudioPath(rawFilePath);
29
+ if (!resolution.ok) {
30
+ const code = resolution.error.startsWith('File not found')
31
+ ? 'FILE_NOT_FOUND'
32
+ : 'PATH_SANDBOX_VIOLATION';
33
+ throw new ElevenLabsError(resolution.error, code, FILE_INPUT_RESOLUTION);
34
+ }
35
+ filePath = resolution.path;
36
+ }
37
+ if (!fs.existsSync(filePath)) {
38
+ throw new ElevenLabsError(`File not found: ${rawFilePath}`, 'FILE_NOT_FOUND', 'Provide an absolute path to an existing audio file.');
39
+ }
40
+ // Defence-in-depth: re-canonicalise via realpathSync at the very last moment
41
+ // to close the TOCTOU window between sandbox validation and readFileSync.
42
+ const verifiedPath = isRemoteUrl(rawFilePath) ? filePath : fs.realpathSync(filePath);
43
+ if (!isRemoteUrl(rawFilePath)) {
44
+ const root = getAudioWorkspaceRoot();
45
+ if (!isInsideAudioWorkspaceRoot(verifiedPath, root)) {
46
+ throw new ElevenLabsError(`file_path resolves outside the workspace sandbox root (${root}); ` +
47
+ `symlinks may not escape the workspace. Got: ${rawFilePath}`, 'PATH_SANDBOX_VIOLATION', FILE_INPUT_RESOLUTION);
48
+ }
49
+ }
50
+ const buffer = fs.readFileSync(verifiedPath);
51
+ const fileName = path.basename(verifiedPath);
52
+ return { buffer, fileName, verifiedPath };
53
+ }
54
+ /** Extension → MIME for multipart uploads (ElevenLabs rejects application/octet-stream). */
55
+ const EXTENSION_TO_MIME = {
56
+ mp3: 'audio/mpeg',
57
+ wav: 'audio/wav',
58
+ m4a: 'audio/mp4',
59
+ ogg: 'audio/ogg',
60
+ flac: 'audio/flac',
61
+ webm: 'video/webm',
62
+ mp4: 'video/mp4',
63
+ mov: 'video/quicktime',
64
+ };
65
+ /**
66
+ * Resolve a MIME type from a file name extension for multipart uploads.
67
+ * Defaults to audio/mpeg when the extension is unknown (most uploads are audio).
68
+ */
69
+ export function mimeTypeForFileName(fileName) {
70
+ const ext = path.extname(fileName).slice(1).toLowerCase();
71
+ return EXTENSION_TO_MIME[ext] ?? 'audio/mpeg';
72
+ }
73
+ /** Build a typed Blob from sandboxed file bytes for FormData multipart parts. */
74
+ export function sandboxedFileToBlob({ buffer, fileName }) {
75
+ return new Blob([new Uint8Array(buffer)], { type: mimeTypeForFileName(fileName) });
76
+ }
77
+ //# sourceMappingURL=file-input.js.map
@@ -1,6 +1,14 @@
1
1
  export { registerConfigureTools } from './configure.js';
2
+ export { registerAccountTools } from './account.js';
2
3
  export { registerMusicTools } from './music.js';
3
4
  export { registerSpeechTools } from './speech.js';
4
5
  export { registerVoiceTools } from './voices.js';
5
6
  export { registerTranscriptionTools } from './transcription.js';
7
+ export { registerVoiceChangerTools } from './voice-changer.js';
8
+ export { registerAudioIsolationTools } from './audio-isolation.js';
9
+ export { registerAlignmentTools } from './alignment.js';
10
+ export { registerVoiceCloneTools } from './voice-clone.js';
11
+ export { registerDialogueTools } from './dialogue.js';
12
+ export { registerVoiceDesignTools } from './voice-design.js';
13
+ export { registerDubbingTools } from './dubbing.js';
6
14
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,14 @@
1
1
  export { registerConfigureTools } from './configure.js';
2
+ export { registerAccountTools } from './account.js';
2
3
  export { registerMusicTools } from './music.js';
3
4
  export { registerSpeechTools } from './speech.js';
4
5
  export { registerVoiceTools } from './voices.js';
5
6
  export { registerTranscriptionTools } from './transcription.js';
7
+ export { registerVoiceChangerTools } from './voice-changer.js';
8
+ export { registerAudioIsolationTools } from './audio-isolation.js';
9
+ export { registerAlignmentTools } from './alignment.js';
10
+ export { registerVoiceCloneTools } from './voice-clone.js';
11
+ export { registerDialogueTools } from './dialogue.js';
12
+ export { registerVoiceDesignTools } from './voice-design.js';
13
+ export { registerDubbingTools } from './dubbing.js';
6
14
  //# sourceMappingURL=index.js.map