Most of my agent tooling is text in, text out. That is fine until the thing I am
building is audio: a product demo, a course module, a 40-second short. Then I end
up copy-pasting the script back into a web UI, picking a voice, downloading an
MP3, and repeating the whole loop for every language.
This post walks through the setup I use now: a remote MCP server that turns
text into audio, wired into the assistant I already have open, plus a small
Python pipeline for the batch case. All the numbers below come from the live
endpoints and docs on 2026-11-05, not from a marketing page.
Why remote MCP instead of one more API wrapper
A remote MCP server is an HTTP endpoint your agent already knows how to call.
There is no local process to keep alive, no npx shim, no stdio session that
dies when you close a terminal. You give the assistant a URL and a bearer token
and it gains tools.
That matters for TTS specifically because the useful call is boring and
frequent: "read this text as this voice". You do not want to hand-write that
request twenty times while iterating on a script.
Step 0: what the server exposes
Two tools, which is all you need:
-
list_voices— returns voice ids, names, and languages. -
text_to_speech— takestext(up to 1,200 characters) and avoiceid, returns the audio plus a short summary.
The transport is Streamable HTTP at https://voicehue.com/mcp. A bare
initialize call answers without a key, which is a cheap way to check the
endpoint is alive:
curl -s https://voicehue.com/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-06-18", "capabilities":{}, "clientInfo":{"name":"curl","version":"0"}}}' The response negotiates 2025-06-18 and reports serverInfo.name = "voicehue".
If that comes back, your network path is fine and any later failure is auth or
args, not transport.
Step 1: create a key
Keys are created in the dashboard and shown once. Put yours in the
environment; never in a config file you might commit.
export VOICEHUE_API_KEY="vh_live_..." The free plan is per key: 10,000 characters per month, 2,000 per day, 10
requests per minute, 1,200 characters per request, standard voices only. An
account can hold two free keys, so a side project and a demo can be metered
separately.
Step 2: wire it into your assistant
Codex CLI:
codex mcp add voicehue --url https://voicehue.com/mcp \ --bearer-token-env-var VOICEHUE_API_KEY Claude Code:
claude mcp add --transport http voicehue https://voicehue.com/mcp \ --header "Authorization: Bearer $VOICEHUE_API_KEY" Cursor (~/.cursor/mcp.json):
{ "mcpServers": { "voicehue": { "url": "https://voicehue.com/mcp", "headers": { "Authorization": "Bearer ${env:VOICEHUE_API_KEY}" } } } } Then ask the agent for something small: "list the available voices". If the
tool list is populated but the call 401s, the env var is not reaching the
process — a shell export in one terminal does not travel to a GUI app.
Step 3: the batch case, in Python
Chat tools are great for one take. Assets need a script that can run twice and
produce the same file names. The same account also exposes an
OpenAI-compatible endpoint at https://voicehue.com/v1, so the official SDK
works with two changed lines:
from openai import OpenAI client = OpenAI( base_url="https://voicehue.com/v1", api_key=os.environ["VOICEHUE_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="voicehue-tts", voice="vh-en-sky", input="Hello from the pipeline.", ) as response: response.stream_to_file("hello.mp3") model is accepted for client compatibility and does not select the voice.
speed accepts 0.5–2, and response_format accepts mp3 (default) or wav
when the chosen voice supports it.
The six standard voice ids, one per language:
| Language | Voice id | Name |
|---|---|---|
| en-US | vh-en-sky | Sky |
| zh-CN | vh-zh-qingyin | 清音 |
| es-ES | vh-es-alex | Alex |
| fr-FR | vh-fr-camille | Camille |
| ja-JP | vh-ja-hikari | ひかり |
| ko-KR | vh-ko-haneul | 하늘 |
You can fetch the same list without a key:
curl -s "https://voicehue.com/v1/voices?language=en" Step 4: split on sentences, not characters
The 1,200-character request cap is the one limit that actually shapes the code.
Naive text[i:i+1200] slicing cuts mid-word, and the seam is audible.
Split on sentence boundaries first, and only hard-split a single sentence that
is longer than the cap:
import re MAX = 1200 def chunk(text, limit=MAX): parts, buf = [], "" for sentence in re.split(r"(?<=[.!?。!?\n])\s*", text): if not sentence: continue if len(buf) + len(sentence) + 1 <= limit: buf = f"{buf} {sentence}".strip() continue if buf: parts.append(buf) while len(sentence) > limit: parts.append(sentence[:limit]) sentence = sentence[limit:] buf = sentence if buf: parts.append(buf) return parts Then render one file per chunk and stitch with ffmpeg's concat demuxer rather
than concatenating bytes:
ffmpeg -f concat -safe 0 -i list.txt -c copy narration.mp3 Gotchas worth knowing before you ship
- A 401 usually means the client, not the key. GUI apps started from the Dock do not inherit variables exported in a terminal. Export the key in the shell that launches the client, or paste the header into the client's own config file.
- A 429 can arrive while your own quota still shows headroom. The service also enforces a shared daily capacity, so a
429does not always mean your key is exhausted. The error body carrieslimitandresets_at(00:00 UTC); branch oncode, not on the message text. - Languages outside the seven have no cloud voice. Cloud coverage is English, Chinese, Spanish, Portuguese, Japanese, Korean, French. The web page falls back to the browser's built-in voices for anything else, and those sound very different from the cloud set.
- A large voice library is not a large free catalogue. The library lists 138 voices; six are standard and free, the rest are premium and need a paid plan. Read the
tierfield before promising a client a specific voice.
When this is the wrong tool
If you need word-level timestamps for subtitles, or cloned voices, this is not
it. MCP is the right layer for "make this paragraph audible" inside an agent
loop, and the HTTP endpoint is the right layer for batch rendering. Neither
replaces a proper audio editor for mixing.
Try it
The endpoint, the voice list, and the free key are all live; the 1,200-character
cap and the six standard voices are documented, not discovered by trial.
VoiceHue — free text to speech, no sign-up
Disclosure: I work on VoiceHue, the service used in the examples. The setup
above is the same one I use for the project's own demo assets; the limits and
error codes are quoted from the live docs and API on 2026-11-05.
