@theproductivepixel/aittsm
v1.9.0
Published
MCP stdio server for AI TTS Microservice — connects AI agents, IDEs, and MCP-compatible clients to the unified TTS API via tool calls
Maintainers
Readme
@theproductivepixel/aittsm
MCP (Model Context Protocol) stdio server for AI TTS Microservice. Connects AI agents, IDEs, and any MCP-compatible client to the unified TTS API via tool calls.
Install
npm install -g @theproductivepixel/aittsmSetup
export AITTSM_API_KEY=tts_your_api_key_hereGet your API key from aitts.theproductivepixel.com/dashboard/api.
Hosted OAuth 2.1
This npm package runs as a local MCP stdio server and still uses AITTSM_API_KEY for authentication. It does not perform browser-based OAuth.
OAuth 2.1 is available on the hosted remote MCP endpoint at https://aitts.theproductivepixel.com/api/v1/mcp. Clients can discover the protected resource at /.well-known/oauth-protected-resource and the authorization server at /.well-known/oauth-authorization-server. The hosted flow uses S256 PKCE, requires the exact canonical resource where client policy says so, and supports predefined, HTTPS metadata-document (CIMD), and controlled dynamic-registration clients. The registered client record selects public, secret, or private_key_jwt authentication. See the MCP documentation.
Usage
Claude Desktop
{
"mcpServers": {
"aitts": {
"command": "npx",
"args": ["@theproductivepixel/aittsm"],
"env": { "AITTSM_API_KEY": "tts_your_api_key_here" }
}
}
}Cursor / Windsurf
{
"aitts": {
"command": "npx",
"args": ["@theproductivepixel/aittsm"],
"env": { "AITTSM_API_KEY": "tts_your_api_key_here" }
}
}Direct CLI
aittsmTools (56)
Voice & Generation
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| search_voices | voices:list | read | Search available TTS voices (filters + free-text q) |
| get_voice_details | voices:list | read | Get full capability details for a voice |
| get_voice_sample_url | tts:generate | generate | Get a sample-audio URL (synthesizes on miss) |
| generate_speech | tts:generate | generate | Generate TTS audio |
| get_job_status | tts:status | read | Check job status and metadata |
| get_audio_link | tts:status | read | Get fresh signed audio URL + metadata |
Jobs
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| list_jobs | jobs:read | read | List jobs with pagination |
| get_job_text | jobs:read | read | Get input text of a job |
| update_job_metadata | jobs:write | generate | Update tags/collection |
| delete_job_audio | jobs:write | generate | Delete stored audio |
Shares
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| create_share | shares:write | generate | Create share link |
| create_voice_share | shares:write | generate | Create a shareable voice collection |
| list_shares | shares:read | read | List active shares |
| get_share | shares:read | read | Get full share details |
| update_share | shares:write | generate | Update share settings |
| revoke_share | shares:write | generate | Revoke a share |
| bulk_revoke_shares | shares:write | generate | Revoke multiple shares |
| toggle_share_permanent | shares:write | generate | Toggle permanent status |
| update_track_order | shares:write | generate | Reorder tracks |
Access Codes & QR
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| create_access_codes | shares:write | generate | Create access codes |
| list_access_codes | shares:read | read | List codes (no raw values) |
| update_access_code | shares:write | generate | Update a code |
| delete_access_code | shares:write | generate | Delete a code |
| export_access_codes | shares:read | read | Export as CSV format |
| get_qr_code | shares:write | generate | Generate QR image |
Library & Storage
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| list_collections | library:read | read | List audio collections |
| manage_collection | library:write | generate | Create/rename/delete collection |
| list_tags | library:read | read | List tags with counts |
| create_bookmark | library:write | generate | Bookmark a share |
| list_bookmarks | library:read | read | List bookmarks |
| delete_bookmark | library:write | generate | Delete a bookmark |
| manage_bookmark_collection | library:write | generate | Manage bookmark collections |
| get_storage | storage:read | read | Storage usage summary |
| list_storage_items | storage:read | read | List stored items |
| bulk_delete_storage | storage:write | generate | Bulk delete storage |
| get_pricing | pricing:estimate | read | Get plan prices and rate summaries |
| estimate_cost | pricing:estimate | read | Estimate credit cost |
| get_usage | usage:read | read | Get balance and account type |
Personalization: Preferences, Modes & Projects
| Tool | Permission | Bucket | Description |
|------|-----------|--------|-------------|
| get_preferences | preferences:read | read | Get stored preference overrides |
| set_preferences | preferences:write | generate | Patch preference overrides (also sets activeModeId) |
| list_modes | modes:read | read | List system + user modes; optional explicit pagination |
| get_mode | modes:read | read | Get a single mode by id |
| preview_mode | preferences:read | read | Preview effective preferences under a given mode |
| create_mode | modes:write | generate | Create a user mode |
| update_mode | modes:write | generate | Update a user mode |
| delete_mode | modes:write | generate | Delete a user mode |
| list_projects | projects:read | read | List projects; optional explicit pagination |
| get_project | projects:read | read | Get a project |
| create_project | projects:write | generate | Create a project |
| update_project | projects:write | generate | Update a project |
| delete_project | projects:write | generate | Delete a project |
| list_project_items | projects:read | read | List project items; optional explicit pagination |
| get_project_item | projects:read | read | Get a project item |
| add_project_item | projects:write | generate | Add an item to a project |
| update_project_item | projects:write | generate | Update a project item |
| remove_project_item | projects:write | generate | Remove a project item |
Note: apply_mode (an in-app-only read-modify-write composite that validates the mode exists before activating it) is intentionally not proxied here — it isn't a single HTTP request, so it has no REST route to map to. To activate a mode from a proxy client, call set_preferences({ updates: { activeModeId: '<mode_id>' }, ... }) instead. Caveat: set_preferences does not validate that activeModeId refers to an existing mode (unlike apply_mode), so call preview_mode or get_mode first to confirm the mode exists before activating it.
Personalization list compatibility: omitting page_size and page_token returns the complete legacy envelope and omits next_page_token when the raw list has at most 100 records; larger lists return PAGINATION_REQUIRED. Supplying either field enables pagination (page_size defaults to 20, maximum 100) and returns next_page_token: string | null. Tokens are owner-bound, and project-item tokens are also project-bound.
Examples
Async Generation Workflow
// 1. Generate audio
{
"name": "generate_speech",
"arguments": {
"text": "Welcome to our podcast.",
"voice_id": "google:en-US-Chirp3HD-Charon",
"tags": ["podcast", "intro"]
}
}
// Response:
// {
// "job_id": "abc123",
// "status": "pending",
// ...
// }
// 2. Poll until completed
{
"name": "get_job_status",
"arguments": {
"job_id": "abc123"
}
}
// Response:
// {
// "status": "completed",
// "audio_bytes": 48000,
// ...
// }
// 3. Get download URL
{
"name": "get_audio_link",
"arguments": {
"job_id": "abc123"
}
}
// Response (use this JSON body — it carries a fresh signed URL — do not follow the /audio 307 redirect):
// {
// "signed_url": "https://...", // deprecated alias of audio_url
// "audio_url": "https://...",
// "expires_at": "2026-07-05T21:00:00Z",
// "expires_in": 900,
// "content_type": "audio/mpeg",
// "audio_bytes": 48000,
// "audio_endpoint": "/api/v1/tts/abc123/audio"
// }Streaming Generation
{
"name": "generate_speech",
"arguments": {
"text": "Stream this audio.",
"voice_id": "google:en-US-Chirp3HD-Charon",
"delivery_mode": "stream",
"output_format": "ogg_opus",
"idempotency_key": "unique-key-abc"
}
}Stream supports all 7 formats. Async supports wav, mp3, ogg_opus only.
Voice Search
{
"name": "search_voices",
"arguments": {
"q": "Leda",
"language": "en-US",
"provider": "google",
"model_type": "ultra"
}
}q runs a fuzzy, typo-tolerant search across voice_id, family, name, language,
and provider, so a partial term like Leda matches even without the full id.
Voice Sample URL
{
"name": "get_voice_sample_url",
"arguments": {
"voice_id": "google:en-US-Gemini-Kore"
}
}
// Ready: { voice_id, sample_url, voices_url, expires_at, expires_in, content_type }
// Pending (sample is being synthesized): { status: "pending", voices_url, retry_after: 3 }Generation-class: a voice with no existing sample triggers synthesis, so poll
again after retry_after seconds until the URL is ready.
Voice Share
{
"name": "create_voice_share",
"arguments": {
"voiceRefs": [
{ "id": "google:en-US-Gemini-Kore", "language": "en-US", "ultraModel": "gemini-2.5-flash-tts" },
{ "id": "kokoro:en-US-Kokoro-Bella" }
]
}
}
// Response: { code, url }. url is built from the configured app origin.Share Workflow
// 1. Create a password-protected share from completed jobs
{
"name": "create_share",
"arguments": {
"job_ids": ["job1", "job2"],
"title": "Client Review",
"auth_mode": "password",
"password": "secret123",
"allow_download": true
}
}
// Response:
// {
// "code": "xyz789",
// "url": "https://aitts.theproductivepixel.com/share/audio/xyz789",
// "item_count": 2
// }
// 2. Generate a QR code for the share
{
"name": "get_qr_code",
"arguments": {
"code": "xyz789",
"format": "png",
"preset": "branded"
}
}
// 3. Later, revoke access
{
"name": "revoke_share",
"arguments": {
"code": "xyz789"
}
}Collection Management
// 1. Create a collection
{
"name": "manage_collection",
"arguments": {
"action": "create",
"name": "Podcast Episodes"
}
}
// Response:
// {
// "id": "col-abc",
// "name": "Podcast Episodes"
// }
// 2. Assign jobs to it
{
"name": "update_job_metadata",
"arguments": {
"job_id": "job1",
"collection_id": "col-abc",
"tags": ["episode-1"]
}
}
// 3. Create a live share from the collection
{
"name": "create_share",
"arguments": {
"source_type": "collection",
"source_id": "col-abc",
"share_mode": "live",
"title": "All Episodes"
}
}Tool Reference
The summary table above lists all 56 proxied tools. The detailed examples below cover the highest-traffic generation, lookup, sharing, and library workflows.
search_voices
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| q | string | — | Free-text fuzzy search across voice_id, family, name, language, provider. Partial and typo-tolerant (e.g. Leda) |
| language | string | — | Filter by language code (e.g. en-US) |
| provider | string | — | Filter by provider (google, polly, kokoro) |
| model_type | premium | ultra | — | Filter by model type |
| gender | male | female | neutral | unknown | — | Filter by gender |
| voice_id | string | — | Exact voice_id filter (case-insensitive) |
Response:
{
"voices": [
{
"voice_id": "google:en-US-Gemini-Charon",
"name": "Charon",
"family": "Gemini",
"language": "en-US",
"provider": "google",
"model_type": "ultra",
"gender": "male",
"characteristics": { "styles": ["informative"], "roles": [], "age": null, "accent": null, "pitch": "lower", "use_case": null },
"sample_url": null,
"supports_ssml": true,
"supports_markup": false,
"supports_multispeaker": true
}
],
"count": 1
}get_voice_details
| Param | Type | Required | Description | |-------|------|----------|-------------| | voice_id | string | ✓ | Voice ID (provider:language-Family-Name) | | model | string | — | Specific model ID for ultra voices with model selection |
Response:
{
"voice_id": "kokoro:en-US-Kokoro-Bella",
"provider": "kokoro",
"language": "en-US",
"family": "Kokoro",
"name": "Bella",
"model_type": "premium",
"gender": "female",
"characteristics": { "styles": ["friendly"], "roles": [], "age": null, "accent": null, "pitch": null, "use_case": null },
"sample_url": null,
"capabilities": {
"max_text_bytes_async": 5000,
"max_text_bytes_stream": 5000,
"supports_streaming": true,
"stream_formats": { "ogg_opus": [24000], "mp3": [24000] },
"async_formats": ["wav", "mp3", "ogg_opus"],
"speed": { "supported_modes": ["text", "markup"], "min": 0.25, "max": 4.0 },
"prompt": null,
"supports_ssml": false,
"supports_markup": false,
"supports_multispeaker": false
}
}Ultra voice with model selection (voice_id=google:en-US-Gemini-Kore):
{
"voice_id": "google:en-US-Gemini-Kore",
"provider": "google",
"language": "en-US",
"family": "Gemini",
"name": "Kore",
"model_type": "ultra",
"gender": "female",
"available_models": ["gemini-2.5-flash-tts", "gemini-2.5-pro-tts", "gemini-2.5-flash-lite-preview-tts", "gemini-3.1-flash-tts-preview"],
"default_model": "gemini-2.5-flash-tts",
"capabilities": { "..." : "resolved for default model" },
"model_overrides": {
"gemini-2.5-flash-lite-preview-tts": {
"max_text_bytes_async": 300,
"max_text_bytes_stream": 300,
"supports_multispeaker": false
}
}
}generate_speech
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| text | string (1–500000) | ✓ | Text to synthesize |
| voice_id | string | ✓ (single) | Voice ID (provider:lang-Family-Name). Required for single-speaker, forbidden for multi-speaker. |
| delivery_mode | async | stream | — | Default: async |
| model_type | premium | ultra | — | Model type |
| model | string | — | Specific model ID |
| speed | number (0.25–4) | — | Speaking rate multiplier |
| format | text | ssml | markup | — | Input format |
| speaker_type | single | multi | — | Speaker type |
| voice_id_speaker_1 | string | ✓ (multi) | Speaker 1 voice ID. Required for multi-speaker only. |
| voice_id_speaker_2 | string | ✓ (multi) | Speaker 2 voice ID. Required for multi-speaker only. |
| output_format | wav | mp3 | ogg_opus | pcm | mulaw | alaw | ogg_vorbis | — | Audio format |
| preview_transport_format | pcm | — | Stream only: the LIVE stream carries raw s16le mono PCM (Web Audio / DSP friendly) while the SAVED artifact keeps output_format or the provider default; with it set, output_format must be wav/mp3/ogg_opus |
| prompt | string | — | Ultra model guidance prompt |
| webhook_url | string (URL) | — | Completion webhook (enterprise) |
| metadata | object | — | Custom metadata |
| sample_rate_hertz | integer | — | Output sample rate in Hz |
| output_bitrate_kbps | integer | — | Bitrate (async only) |
| language | string | — | Language code override |
| tags | string[] | — | Library tags |
| collection_id | string | — | Collection ID |
| idempotency_key | string (max 256) | — | Safe retry key |
Response (async):
{
"job_id": "uuid",
"status": "pending",
"poll_url": "/api/v1/tts/{job_id}",
"audio_endpoint": "/api/v1/tts/{job_id}/audio",
"chars_charged": 16
}Response (stream):
{
"job_id": "uuid",
"status": "pending",
"stream_url": "https://...",
"stream_url_expires_at": "...",
"transport_format": "ogg_opus",
"transport_mime_type": "audio/ogg; codecs=opus",
"transport_sample_rate_hertz": 48000,
"chars_charged": 30
}With preview_transport_format: "pcm" the transport fields describe the raw PCM live stream
(transport_format: "pcm", transport_mime_type: "audio/pcm") while the SAVED artifact keeps
the requested or default format — fetch stream_url for raw s16le mono samples at
transport_sample_rate_hertz, and the completed job's audio stays a normal playable file.
get_job_status
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_id | string | ✓ | Job ID to check |
Response:
{
"job_id": "uuid",
"status": "completed",
"created_at": "2026-05-20T10:00:00.000Z",
"progress_message": null,
"elapsed_seconds": 3,
"provider": "google",
"model_requested": "gemini-2.5-pro-preview-tts",
"model_effective": "gemini-2.5-flash-tts",
"output_format": "ogg_opus",
"sample_rate_hertz": 48000,
"output_bitrate_kbps": 64,
"duration_seconds": 3.2,
"estimated_duration_seconds": null,
"prompt_bytes": 0,
"chars_charged": 16,
"audio_endpoint": "/api/v1/tts/uuid/audio",
"audio_available": true,
"retention_tier": "permanent",
"retained_until": null,
"voice_id": "en-US-Chirp3HD-Kore",
"model_type": "ultra",
"audio_bytes": 48000,
"tags": [],
"collection_id": null,
"source": "api",
"is_expired": false,
"audio_url": "https://storage.example.com/...",
"audio_url_expires_at": "2026-05-20T11:00:00.000Z"
}get_audio_link
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_id | string | ✓ | Job ID |
Response:
{
"job_id": "uuid",
"audio_url": "https://...",
"signed_url": "https://...",
"expires_at": "2026-05-08T12:00:00Z",
"expires_in": 900,
"content_type": "audio/mpeg",
"audio_bytes": 48000,
"audio_endpoint": "/api/v1/tts/{job_id}/audio"
}
signed_urlis a deprecated alias ofaudio_url. Calling this tool on a job that is not completed, or whose audio has expired, returns an error (409/410) rather than a payload.
get_voice_sample_url
Generation-class. When a sample for the voice does not exist yet, this triggers
sample synthesis and returns a pending status until the audio is ready. The
language is derived from the voice_id.
| Param | Type | Required | Description | |-------|------|----------|-------------| | voice_id | string | ✓ | Voice ID (provider:language-Family-Name) | | model | string | — | Optional Gemini ultra sub-model id (honored only for Google ultra voices) |
Response (ready):
{
"voice_id": "google:en-US-Gemini-Kore",
"sample_url": "https://...",
"voices_url": "https://aitts.theproductivepixel.com/voices?provider=google&voice=google:kore&lang=en-US&model=gemini-2.5-flash-tts",
"expires_at": "2026-07-13T10:00:00Z",
"expires_in": 604800,
"content_type": "audio/mpeg"
}
voices_urlis a/voicesgallery deep-link pinned to the exact voice, returned besidesample_url(the raw audio URL) so you can hand the user both.Format (v1.8.0+):
voices_urluses the plain-name form:provider=X&family=Y&voice=<Name>&lang=<xx-YY>&model=<sub>?Alternate: pass a full public voice_id as
voice=<full>for a single-param deep-link (e.g.?voice=google:en-US-Gemini-Kore).Backward compat: legacy internal-canonical URLs still resolve — three-form cascade: full-public-id > legacy-canonical > plain-name.
Response (pending):
{
"status": "pending",
"voices_url": "https://aitts.theproductivepixel.com/voices?provider=google&voice=google:kore&lang=en-US&model=gemini-2.5-flash-tts",
"retry_after": 3
}The signed
sample_urlexpiry is capped at 7 days (GCS V4 limit). The URL is returned withCache-Control: no-storeand is never logged.
get_job_text
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_id | string | ✓ | Job ID |
Response:
{
"text": "Hello world"
}list_jobs
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| page_size | integer (1–100) | — | Items per page (default 20) |
| page_token | string | — | Pagination cursor |
| source | api | ui | — | Filter by source |
| status | completed | failed | pending | processing | — | Filter by status |
Response:
{
"jobs": [
{
"job_id": "...",
"status": "completed",
"...": "..."
}
],
"next_page_token": "eyJ..."
}delete_job_audio
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_id | string | ✓ | Job ID |
Response:
{
"shares_revoked": 0,
"storage": null
}update_job_metadata
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_id | string | ✓ | Job ID | | tags | string[] | — | Replace tags array | | collection_id | string | null | — | Set or clear collection |
Response:
{
"job_id": "uuid",
"tags": ["podcast", "episode-1"],
"collection_id": "col-abc"
}create_share
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| job_ids | string[] | — | Job IDs for snapshot share |
| source_type | collection | tag | — | Source type |
| source_id | string | — | Source ID |
| share_mode | snapshot | live | — | Share mode |
| auth_mode | none | password | access_code | — | Auth mode |
| password | string | — | Password (if auth_mode=password) |
| title | string | — | Share title |
| allow_download | boolean | — | Allow download |
| include_text | boolean | — | Include text excerpts |
| show_voice | boolean | — | Show voice metadata |
| show_model | boolean | — | Show model metadata |
| show_provider | boolean | — | Show provider metadata |
| show_language | boolean | — | Show language metadata |
| show_expiry | boolean | — | Show expiry metadata |
| show_track_meta | boolean | — | Show track metadata |
| track_titles | object | — | Custom track titles |
| track_order | string[] | — | Custom track order |
Response:
{
"code": "abc123",
"url": "https://aitts.theproductivepixel.com/share/audio/abc123",
"item_count": 3
}create_voice_share
Creates a shareable voice collection from caller-supplied voice references. The
returned URL is built from the configured app origin, never the caller Origin
header. Mirrors POST /api/v1/voice-shares.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| voiceRefs | VoiceRef[] (1–50) | ✓ | Voice references. Each is { id, language?, ultraModel? } |
Each VoiceRef field: id is a public voice_id (provider:language-Family-Name,
as returned by search_voices / GET /api/v1/voices) — the share is canonicalized
internally; language is an optional BCP-47 code the voice supports; ultraModel
is an optional Gemini sub-model id.
Response:
{
"code": "v1a2b3c4",
"url": "https://aitts.theproductivepixel.com/voices/share/v1a2b3c4"
}Errors: VALIDATION_ERROR, INVALID_VOICE_REF, INVALID_VOICE_LANGUAGE, INVALID_ULTRA_MODEL.
list_shares
| Param | Type | Required | Description | |-------|------|----------|-------------| | page_size | integer (1–100) | — | Items per page | | page_token | string | — | Pagination cursor |
Response:
{
"shares": [
{
"code": "...",
"title": "...",
"item_count": 3,
"auth_mode": "none",
"share_mode": "snapshot",
"is_permanent": false,
"created_at": "..."
}
],
"next_page_token": null
}get_share
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code |
Response:
{
"code": "abc123",
"title": "Demo",
"auth_mode": "none",
"share_mode": "snapshot",
"permanent": false,
"allow_download": true,
"item_count": 2,
"created_at": "2026-05-20T10:00:00.000Z",
"jobs": [
{
"job_id": "uuid",
"voice_id": "en-US-Wavenet-D",
"model_type": "premium",
"created_at": "2026-05-20T09:00:00.000Z",
"audio_bytes": 48000,
"output_format": "mp3",
"duration_seconds": 3.2,
"estimated_duration_seconds": null,
"duration_source": "probe",
"duration_confidence": "exact",
"audio_sample_rate_hertz": 44100,
"sample_rate_hertz": 44100,
"output_bitrate_kbps": 128
}
]
}update_share
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| code | string | ✓ | Share code |
| title | string | — | New title |
| auth_mode | none | password | access_code | — | Auth mode |
| password | string | — | Password |
| allow_download | boolean | — | Allow download |
| share_mode | snapshot | live | — | Share mode |
| include_text | boolean | — | Include text |
| show_voice | boolean | — | Show voice |
| show_model | boolean | — | Show model |
| show_provider | boolean | — | Show provider |
| show_language | boolean | — | Show language |
| show_expiry | boolean | — | Show expiry |
| show_track_meta | boolean | — | Show track meta |
| track_titles | object | — | Track titles |
| track_order | string[] | null | — | Track order or null |
| source_type | collection | tag | — | Source type |
| source_id | string | — | Source ID |
Response:
{
"code": "abc123",
"title": "Updated",
"auth_mode": "password",
"updated_at": "..."
}revoke_share
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code |
Response:
{
"revoked": true
}bulk_revoke_shares
| Param | Type | Required | Description | |-------|------|----------|-------------| | codes | string[] (1–100) | ✓ | Share codes to revoke |
Response:
{
"revoked_count": 3,
"skipped": ["code-not-found"]
}toggle_share_permanent
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code |
Response:
{
"code": "abc123",
"permanent": true,
"expires_at": null
}update_track_order
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code | | track_order | string[] | null | ✓ | Ordered job IDs or null to reset |
Response:
{
"code": "abc123",
"track_order": ["job-2", "job-1", "job-3"]
}create_access_codes
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code | | count | integer (1–100) | — | Number of codes (default 1) | | label | string | — | Label or label prefix | | expires_at | string (ISO date) | — | Expiration date | | max_uses | integer (≥1) | — | Max uses per code |
Response (count=1):
{
"id": "doc-id",
"code": "ABCD1234EFGH",
"code_prefix": "ABCD",
"label": "VIP",
"active": true,
"created_at": "...",
"expires_at": null,
"max_uses": 10
}Response (count>1):
[
{
"id": "doc-1",
"code": "ABCD1111AAAA"
},
{
"id": "doc-2",
"code": "ABCD2222BBBB"
}
]list_access_codes
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code |
Response:
[
{
"id": "...",
"code_prefix": "ABCD",
"label": "VIP",
"active": true,
"uses": 3,
"max_uses": 10,
"created_at": "...",
"expires_at": null,
"last_used_at": null
}
]update_access_code
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code | | access_code_id | string | ✓ | Access code document ID | | label | string | — | New label | | active | boolean | — | Active status | | expires_at | string | null | — | New expiry or null | | max_uses | integer | null | — | New max uses or null |
Response:
{
"id": "doc-id",
"code_prefix": "ABCD",
"label": "Updated",
"active": true,
"uses": 3,
"max_uses": 20,
"created_at": "2026-05-01T00:00:00Z",
"expires_at": null,
"last_used_at": null
}delete_access_code
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code | | access_code_id | string | ✓ | Access code document ID |
Response:
{
"deleted": true
}export_access_codes
| Param | Type | Required | Description | |-------|------|----------|-------------| | code | string | ✓ | Share code |
Response:
[
{
"id": "ac-1",
"code_prefix": "AB12",
"label": "Reviewer",
"active": true,
"created_at": "...",
"expires_at": null,
"max_uses": 10,
"uses": 3
}
]get_qr_code
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| code | string | ✓ | Share code |
| format | svg | png | ✓ | Image format |
| preset | clean | branded | — | Visual preset |
| include_access_code | boolean | — | Embed access code in URL |
| access_code | string | — | Access code to embed |
Response:
{
"data_uri": "data:image/png;base64,...",
"format": "png"
}list_collections
No parameters.
Response:
{
"collections": [
{
"id": "col-abc",
"name": "Podcast Episodes",
"created_at": "...",
"updated_at": "..."
}
]
}manage_collection
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| action | create | rename | delete | ✓ | Operation |
| name | string | — | Name (required for create/rename) |
| collection_id | string | — | ID (required for rename/delete) |
Response (create/rename):
{
"id": "col-id",
"name": "New Name"
}Response (delete):
{
"deleted": true
}list_tags
No parameters.
Response:
{
"tags": [
{
"tag": "podcast",
"count": 12
},
{
"tag": "demo",
"count": 3
}
]
}create_bookmark
| Param | Type | Required | Description | |-------|------|----------|-------------| | share_code | string | ✓ | Share code to bookmark |
Response:
{
"created": true
}list_bookmarks
| Param | Type | Required | Description | |-------|------|----------|-------------| | page_size | integer (1–100) | — | Items per page | | page_token | string | — | Pagination cursor |
Response:
{
"bookmarks": [
{
"id": "bm-1",
"share_code": "xyz789",
"title": "Shared Audio",
"personal_title": null,
"collection_id": null,
"created_at": "...",
"last_opened_at": null,
"effective_status": "active"
}
],
"next_page_token": null
}delete_bookmark
| Param | Type | Required | Description | |-------|------|----------|-------------| | bookmark_id | string | ✓ | Bookmark ID (share code) |
Response:
{
"deleted": true
}manage_bookmark_collection
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| action | create | rename | delete | ✓ | Operation |
| name | string | — | Name (required for create/rename) |
| collection_id | string | — | ID (required for rename/delete) |
Response (create/rename):
{
"id": "col-id",
"name": "Favorites"
}Response (delete):
{
"deleted": true
}get_storage
No parameters.
Response:
{
"used_bytes": 15728640,
"cap_bytes": 104857600,
"remaining_bytes": 89128960,
"pending_reclaim_bytes": 0,
"sync_status": "healthy"
}list_storage_items
| Param | Type | Required | Description | |-------|------|----------|-------------| | page_size | integer (1–100) | — | Items per page | | page_token | string | — | Pagination cursor |
Response:
{
"items": [
{
"job_id": "uuid",
"status": "completed",
"audio_bytes": 48000,
"output_format": "ogg_opus",
"created_at": "...",
"storage_tier": "permanent"
}
],
"next_page_token": null
}bulk_delete_storage
| Param | Type | Required | Description | |-------|------|----------|-------------| | job_ids | string[] (1–100) | ✓ | Job IDs to delete |
Response:
{
"deleted_count": 3,
"skipped_count": 1,
"skipped": [
{
"job_id": "uuid",
"reason": "job_in_progress"
}
]
}get_pricing
No parameters.
Response:
{
"pricing_available": true,
"currency": "USD",
"plans": [
{
"id": "pro",
"title": "Pro",
"price": "$20/month",
"highlights": ["Monthly credits"]
}
],
"usage_rates": [
{
"provider": "google",
"voice_class": "premium",
"rate_per_1k_bytes": 0.01
}
],
"pricing_url": "https://aitts.theproductivepixel.com/pricing",
"guidance": "Use estimate_cost for an exact quote on a specific generation."
}OAuth accounts that still need account pricing setup return pricing_available: false and requires_account_pricing_setup: true.
estimate_cost
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| text | string (1–500000) | ✓ | Text to estimate |
| model_type | premium | ultra | — | Model type |
| voice_id | string | — | Voice ID |
| output_format | wav | mp3 | ogg_opus | — | Output format |
Response:
{
"estimated_cost": 2.5,
"currency": "USD",
"chars_charged": 500,
"model_type": "ultra",
"provider": "google",
"output_format": "ogg_opus"
}get_usage
No parameters.
Response:
{
"account_type": "standard",
"credits_balance": 4250,
"balance": {
"amount": 4250,
"currency": "USD",
"formatted": "$4,250.00"
}
}Pagination
Paginated tools accept page_size (1–100, default 20) and page_token (opaque cursor). Results include next_page_token (null when exhausted). The three personalization lists retain the omission compatibility contract described above.
Idempotency
Pass idempotency_key to generate_speech for safe retries:
- Same key + same body → cached result (no duplicate generation)
- Same key + different body →
IDEMPOTENCY_KEY_REUSE(409) - Key still processing →
REQUEST_IN_PROGRESS(409)
Rate Limiting
Two buckets: read (higher limits — queries, lists) and generate (lower limits — mutations, generation). Per-account enforcement. Exceeding returns an error message at tool level; HTTP 429 at transport level.
Permissions
| Permission | Tools | |-----------|-------| | voices:list | search_voices | | tts:generate | generate_speech, get_voice_sample_url | | tts:status | get_job_status, get_audio_link | | jobs:read | list_jobs, get_job_text | | jobs:write | delete_job_audio, update_job_metadata | | shares:read | list_shares, get_share, list_access_codes, export_access_codes | | shares:write | create_share, create_voice_share, update_share, revoke_share, bulk_revoke_shares, toggle_share_permanent, update_track_order, create_access_codes, update_access_code, delete_access_code, get_qr_code | | library:read | list_collections, list_tags, list_bookmarks | | library:write | manage_collection, create_bookmark, delete_bookmark, manage_bookmark_collection | | storage:read | get_storage, list_storage_items | | storage:write | bulk_delete_storage | | pricing:estimate | get_pricing, estimate_cost | | usage:read | get_usage |
Error Reference
| Code | Status | Description | |------|--------|-------------| | VALIDATION_ERROR | 400 | Invalid input parameters | | PAGINATION_REQUIRED | 400 | No-param personalization list exceeds 100 raw records; no partial list is returned | | INVALID_VOICE | 400 | Voice ID not found or invalid | | PROVIDER_DISABLED | 400 | Provider unknown or disabled | | INSUFFICIENT_CREDITS | 402 | Not enough credits | | STORAGE_CAP_EXCEEDED | 403 | Storage quota full | | FORBIDDEN | 403 | Enterprise-only or ownership failed | | INVALID_WEBHOOK_URL | 400 | Webhook URL validation failed | | ENTERPRISE_TIER_REQUIRED | 400 | Enterprise pricing without tier | | STREAM_NOT_SUPPORTED | 400 | Voice/provider doesn't support streaming | | SPEED_OUT_OF_RANGE | 400 | Speaking rate exceeds stream limit | | STREAM_BITRATE_NOT_SUPPORTED | 400 | Bitrate in stream mode | | STREAM_FORMAT_MISMATCH | 400 | Format not supported for stream | | MAINTENANCE | 503 | API in maintenance mode | | IDEMPOTENCY_KEY_REUSE | 409 | Key reused with different body | | REQUEST_IN_PROGRESS | 409 | Key still processing | | JOB_NOT_FOUND | 404 | Job doesn't exist or not owned | | JOB_IN_PROGRESS | 409 | Cannot delete pending/processing job | | TEXT_UNAVAILABLE | 410 | Job expired or text not stored | | SHARE_NOT_FOUND | 404 | Share doesn't exist or not owned | | AMBIGUOUS_SHARE_INPUT | 400 | Both job_ids and source provided | | INVALID_SHARE_SOURCE | 400 | Invalid source configuration | | PASSWORD_REQUIRED | 400 | auth_mode=password without password | | INVALID_AUTH_MODE_PASSWORD_COMBO | 400 | auth_mode and password combination invalid | | SOURCE_IMMUTABLE | 400 | Cannot change source on source-backed share | | ACCESS_CODE_NOT_FOUND | 404 | Access code doesn't exist | | COLLECTION_NOT_FOUND | 404 | Collection doesn't exist or not owned | | BOOKMARK_NOT_FOUND | 404 | Bookmark or collection doesn't exist | | ALREADY_EXISTS | 409 | Bookmark already exists | | FREE_TIER_LIMIT_EXCEEDED | 403 | Free tier limit reached (>1000 chars per request) |
Error envelope: { error: "message", code?: "ERROR_CODE", status?: 404 }
Configuration
| Variable | Required | Description |
|----------|----------|-------------|
| AITTSM_API_KEY | Yes | API key (starts with tts_) |
| AITTSM_BASE_URL | No | Custom API base URL |
Requirements
- Node.js 18+
- An API key (get one here)
Links
Changelog
1.8.0
- Added
get_pricing(pricing:estimate, read bucket) to the local MCP stdio proxy. It mirrorsGET /api/v1/pricingand returns account-correct plan prices plus rate summaries. - Added 18 REST-backed personalization proxy tools for preferences, modes, projects, and project items. The hosted remote MCP endpoint exposes one additional native-only
apply_modetool. - Tool count 37 -> 56 for the local stdio proxy; hosted remote MCP exposes 57 tools.
get_voice_sample_urlandcreate_voice_sharenow emitvoices_urlin the NEW plain-name form:provider=X&family=Y&voice=<Name>&lang=<xx-YY>[&model=<sub>]. Human-readable, stable across internal id changes.- The
/voicesdeep-link now accepts three URL forms (cascade: full-public-id > legacy-canonical > plain-name). All existing URLs continue to resolve (backward compatible). - Full public voice_id as a single-param deep-link:
?voice=google:en-US-Gemini-Kore(self-qualifying, no other params needed).
1.7.0
- Added
create_voice_share(shares:write) — create a shareable voice collection fromvoiceRefs({ id, language?, ultraModel? }); the returned URL is built from the configured app origin, never the callerOrigin. MirrorsPOST /api/v1/voice-shares. - Added
get_voice_sample_url(tts:generate, generation-class) — fetch a sample-audio URL for a voice; synthesizes on a cache miss and returns apendingstatus until ready. MirrorsGET /api/v1/voices/{voice_id}/sample-url. search_voicesnow accepts a free-textqparam — fuzzy, typo-tolerant search across voice_id, family, name, language, and provider, so a partial term likeLedamatches without the full id.- Tool count 35 → 37.
1.6.0
get_audio_linknow returnsaudio_url(with a deprecatedsigned_urlalias) plusexpires_in,content_type, andaudio_bytesalongsideexpires_at,job_id, andaudio_endpoint.get_audio_linknow enforces the audio-expiry gate: calling it on a job that is not completed or whose audio has expired returns an error (409AUDIO_NOT_READY/ 410AUDIO_EXPIRED) instead of a bare{ audio_endpoint, job_id }payload.
License
MIT
