@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.
- package/README.md +68 -4
- package/dist/bridge.d.ts +1 -0
- package/dist/client.d.ts +34 -3
- package/dist/client.js +141 -35
- package/dist/endpoints.d.ts +34 -0
- package/dist/endpoints.js +37 -0
- package/dist/error-detail.d.ts +5 -0
- package/dist/error-detail.js +20 -0
- package/dist/server.js +9 -1
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +105 -0
- package/dist/tools/alignment.d.ts +3 -0
- package/dist/tools/alignment.js +55 -0
- package/dist/tools/audio-isolation.d.ts +3 -0
- package/dist/tools/audio-isolation.js +51 -0
- package/dist/tools/configure.js +18 -6
- package/dist/tools/dialogue.d.ts +3 -0
- package/dist/tools/dialogue.js +70 -0
- package/dist/tools/dubbing.d.ts +3 -0
- package/dist/tools/dubbing.js +234 -0
- package/dist/tools/file-input.d.ts +28 -0
- package/dist/tools/file-input.js +77 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/music.js +152 -27
- package/dist/tools/path-safety.d.ts +1 -0
- package/dist/tools/path-safety.js +4 -1
- package/dist/tools/speech.js +90 -34
- package/dist/tools/transcription.js +34 -61
- package/dist/tools/voice-changer.d.ts +3 -0
- package/dist/tools/voice-changer.js +62 -0
- package/dist/tools/voice-clone.d.ts +3 -0
- package/dist/tools/voice-clone.js +104 -0
- package/dist/tools/voice-design.d.ts +3 -0
- package/dist/tools/voice-design.js +153 -0
- package/dist/tools/voices.js +167 -13
- package/dist/types.d.ts +110 -13
- package/dist/types.js +20 -4
- package/dist/untrusted-content.d.ts +45 -0
- package/dist/untrusted-content.js +104 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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
|
+
[](cursor://anysphere.cursor-deeplink/mcp/install?name=ElevenLabs&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtaW5kc3RvbmUvbWNwLXNlcnZlci1lbGV2ZW5sYWJzIl0sImVudiI6eyJFTEVWRU5MQUJTX0FQSV9LRVkiOiIifX0)
|
|
25
|
+
[](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
|
+
[](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 (
|
|
117
|
+
## Tools (24)
|
|
78
118
|
|
|
79
119
|
### Configuration
|
|
80
120
|
- `configure_elevenlabs_api_key` — Save your ElevenLabs API key
|
|
81
121
|
|
|
82
|
-
###
|
|
83
|
-
- `
|
|
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
|
-
###
|
|
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
package/dist/client.d.ts
CHANGED
|
@@ -9,17 +9,48 @@
|
|
|
9
9
|
* Voices URL: https://api.elevenlabs.io/v2/voices
|
|
10
10
|
*/
|
|
11
11
|
import { type AudioResult } from './types.js';
|
|
12
|
+
export interface ElevenLabsFetchOptions extends RequestInit {
|
|
13
|
+
/** Per-call timeout override (default REQUEST_TIMEOUT_MS). Explicit `signal` wins. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Make an authenticated request to the ElevenLabs API.
|
|
14
18
|
* Returns a raw Response object.
|
|
15
19
|
*/
|
|
16
|
-
export declare function elevenLabsFetch(apiKey: string, urlPath: string, options?:
|
|
20
|
+
export declare function elevenLabsFetch(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<Response>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract a human-readable detail message from an ElevenLabs error response.
|
|
23
|
+
*
|
|
24
|
+
* The API uses several detail shapes:
|
|
25
|
+
* - `detail: "string message"` (legacy)
|
|
26
|
+
* - `detail: { message: "...", status?: "..." }` (current auth/permission errors)
|
|
27
|
+
* - `detail: [{ type, loc: ["body", "field", ...], msg, input }, ...]` (FastAPI 422)
|
|
28
|
+
*
|
|
29
|
+
* The 422 array form is the one that bit us in
|
|
30
|
+
* `generate_music_from_plan`: previously we threw away the field-level info
|
|
31
|
+
* and left the user with `HTTP 422: unknown`. We now flatten the validation
|
|
32
|
+
* errors into something like
|
|
33
|
+
* `body.composition_plan.sections.0.section_name: Field required;
|
|
34
|
+
* body.composition_plan.sections.0.lines: Field required`
|
|
35
|
+
* which is what an LLM agent actually needs to self-correct.
|
|
36
|
+
*
|
|
37
|
+
* Raw detail is third-party-authored — callers must envelope before exposing
|
|
38
|
+
* to model-visible output (see `envelopeApiErrorDetail`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractErrorDetail(response: Response): Promise<string>;
|
|
17
41
|
/**
|
|
18
42
|
* Make a JSON API call and parse the response.
|
|
19
43
|
*/
|
|
20
|
-
export declare function elevenLabsJson<T>(apiKey: string, urlPath: string, options?:
|
|
44
|
+
export declare function elevenLabsJson<T>(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<T>;
|
|
45
|
+
/** Map response Content-Type to a file extension (OpenAPI drift — don't trust empty schemas). */
|
|
46
|
+
export declare function extensionFromContentType(contentType: string | null | undefined): string;
|
|
21
47
|
/**
|
|
22
48
|
* Make an API call that returns raw audio binary. Save to file and return path.
|
|
23
49
|
*/
|
|
24
|
-
export declare function elevenLabsAudio(apiKey: string, urlPath: string, options?:
|
|
50
|
+
export declare function elevenLabsAudio(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions, fileExtension?: string): Promise<AudioResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Download binary audio with content-type sniffing.
|
|
53
|
+
* When the API returns JSON (error body despite 200 drift), surfaces the flattened error.
|
|
54
|
+
*/
|
|
55
|
+
export declare function elevenLabsBinaryDownload(apiKey: string, urlPath: string, options?: ElevenLabsFetchOptions): Promise<AudioResult>;
|
|
25
56
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
CHANGED
|
@@ -13,7 +13,8 @@ import * as path from 'path';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { ElevenLabsError, REQUEST_TIMEOUT_MS, getErrorResolution, } from './types.js';
|
|
16
|
-
|
|
16
|
+
import { ELEVENLABS_API_V1_BASE } from './endpoints.js';
|
|
17
|
+
import { envelopeApiErrorDetail, formatApiErrorMessage } from './error-detail.js';
|
|
17
18
|
/**
|
|
18
19
|
* Make an authenticated request to the ElevenLabs API.
|
|
19
20
|
* Returns a raw Response object.
|
|
@@ -24,21 +25,22 @@ export async function elevenLabsFetch(apiKey, urlPath, options = {}) {
|
|
|
24
25
|
}
|
|
25
26
|
const url = urlPath.startsWith('https://')
|
|
26
27
|
? urlPath
|
|
27
|
-
: `${
|
|
28
|
+
: `${ELEVENLABS_API_V1_BASE}${urlPath}`;
|
|
29
|
+
const { timeoutMs, ...fetchOptions } = options;
|
|
28
30
|
const headers = {
|
|
29
31
|
'xi-api-key': apiKey,
|
|
30
|
-
...(
|
|
32
|
+
...(fetchOptions.headers || {}),
|
|
31
33
|
};
|
|
32
34
|
// Only set Content-Type for JSON bodies (not FormData)
|
|
33
|
-
if (!(
|
|
35
|
+
if (!(fetchOptions.body instanceof FormData) && !headers['Content-Type']) {
|
|
34
36
|
headers['Content-Type'] = 'application/json';
|
|
35
37
|
}
|
|
36
|
-
console.error(`[ElevenLabs API] ${
|
|
38
|
+
console.error(`[ElevenLabs API] ${fetchOptions.method || 'GET'} ${url}`);
|
|
37
39
|
let response;
|
|
38
40
|
try {
|
|
39
41
|
response = await fetch(url, {
|
|
40
|
-
...
|
|
41
|
-
signal:
|
|
42
|
+
...fetchOptions,
|
|
43
|
+
signal: fetchOptions.signal ?? AbortSignal.timeout(timeoutMs ?? REQUEST_TIMEOUT_MS),
|
|
42
44
|
headers,
|
|
43
45
|
});
|
|
44
46
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
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
|
|
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()}.${
|
|
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,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
|