Read text aloud with VoiceLabs
Generate speech from the user's text in one of their voice profiles, wait for it to finish, and give them the audio link.
Connecting and calling the tools
Every tool in this skill comes from the voicelabs MCP server, the VoiceLabs connector, and is written here as voicelabs:<tool>. The server lists the named tools and the Code Mode pair side by side.
- Tools listed by name: call them directly, for example
voicelabs:list_voice_profiles. - Several operations in one round trip, or a named tool is missing: call
voicelabs:search_toolsfirst, then call each tool insidevoicelabs:execute_typescriptasexternal_<tool>(for exampleexternal_list_voice_profiles); the program mustreturnits result. Scopes, confirmations and gates are identical on both surfaces. - "Tool not found": switch to the Code Mode route instead of retrying the direct call. If
voicelabs:search_toolsdoes not return the tool either, its OAuth scope was not granted; the user reconnects VoiceLabs and approves that scope. - No
voicelabstools at all: VoiceLabs is not connected yet. Install the VoiceLabs power, which adds the server; it signs in with OAuth on first use.
Tools you will use
voicelabs:list_voice_profiles: the account's voices with id, name, language,voice_type(preset or cloned) and engine.voicelabs:ensure_voice_profile: make sure a named voice exists, creating it from a built-in preset if it does not. Safe to repeat.voicelabs:speak: start a generation fromtextwithprofileIdorprofileNameand optionallanguage. ReturnsgenerationIdwith statusgenerating.voicelabs:get_generation: poll a generation; returnsstatus(generating,completed,failed) and, once completed,audioUrl.voicelabs:search_toolsandvoicelabs:execute_typescript: start several generations in one round trip for multi-part scripts.
Workflow
- Pick the voice. If the user named one, pass it as
profileName. Otherwise callvoicelabs:list_voice_profilesand ask which voice, or suggest one that matches the text's language. - If the account has no voices, or
voicelabs:speakreturnsNOT_FOUND, offer a built-in preset: callvoicelabs:ensure_voice_profilewith a name such as "Narrator" and use the id it returns. - Call
voicelabs:speakwith the text and the voice. Keep thegenerationId. - Call
voicelabs:get_generationwith that id. While the status isgenerating, call it again. When it iscompleted, give the user theaudioUrl. If it isfailed, report theerrorfield. - For a long script, split it at paragraph or section breaks and generate each part (see below). Return the links in order, labelled by section.
Several parts in one round trip
Call voicelabs:search_tools first to get the exact declarations, then start every part at once. Do not wait for audio inside the sandbox; return the ids and poll voicelabs:get_generation afterwards.
const parts = ["Section one text", "Section two text"];
const started = await Promise.all(
parts.map((text) =>
external_speak({ text, profileName: "Narrator" }).catch((e) => ({ error: String(e) })),
),
);
return started; // generationId per part, or "<CODE>: <details>"
Rules
- Speak only the text the user gave or approved. Do not add intros, disclaimers, or extra lines.
- Audio links expire after about 15 minutes. If the user comes back later, call
voicelabs:get_generationagain for a fresh link. QUOTA_EXCEEDED: the account's allowance for the period is used. Relay the message as written and stop; do not push a plan or retry.RATE_LIMITED: wait theretryAfterSecondsit names, then continue.SCOPE_MISSING: the connection lacksvoice:generate; the user can reconnect and grant it.- Arguments are strict and camelCase (
profileId, notprofile_id).
