Guide · AWS Polly · SSML Markup
AWS Polly SSML Markup in MCP Servers — Prosody, Phoneme, Say-As, Pauses
Speech Synthesis Markup Language (SSML) is an XML vocabulary that lets MCP server tools control exactly how AWS Polly speaks a given text — adding pauses between sections, slowing down for important numbers, correcting mispronounced domain terms, and formatting dates or phone numbers as a human would speak them. SSML input is set via TextType: 'ssml' on SynthesizeSpeechCommand and allows up to 6,000 characters (double the plain-text limit). This guide covers every Polly-supported SSML tag with practical examples for MCP tool use: <break> for natural pauses, <prosody> for rate, pitch, and volume control, <phoneme> for IPA pronunciation, <say-as> for formatting numbers, dates, and phone numbers, <sub> for acronym expansion, and <lang> for inline language switching.
TL;DR
Wrap your text in <speak>...</speak> and set TextType: 'ssml' in SynthesizeSpeechCommand. Use <break time="1s"/> for section pauses, <prosody rate="slow"> for deliberate speech, <say-as interpret-as="digits">404</say-as> to read numbers digit-by-digit, and <phoneme alphabet="ipa" ph="ˈmɒdəl">model</phoneme> for exact pronunciation. XML-special characters in the text content must be escaped: & for &, < for <, > for >, " for ". Unescaped entities are the most common source of SSML parse errors. The generative engine does not support SSML — use the neural or long-form engine.
SSML tool implementation
import {
PollyClient,
SynthesizeSpeechCommand,
OutputFormat,
Engine,
TextType,
VoiceId,
} from '@aws-sdk/client-polly';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const polly = new PollyClient({ region: 'us-east-1' });
const server = new McpServer({ name: 'polly-ssml-tools', version: '1.0.0' });
// Escape XML special characters in user-supplied strings before inserting into SSML
function escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
server.tool('synthesize_ssml', {
ssml: z.string().max(6000).describe('Full SSML string including <speak> wrapper'),
voice_id: z.string().default('Joanna'),
}, async ({ ssml, voice_id }) => {
const { AudioStream } = await polly.send(new SynthesizeSpeechCommand({
Text: ssml,
TextType: TextType.SSML,
VoiceId: voice_id as VoiceId,
OutputFormat: OutputFormat.MP3,
Engine: Engine.NEURAL,
}));
if (!AudioStream) throw new Error('No audio stream');
const chunks: Uint8Array[] = [];
for await (const chunk of AudioStream as AsyncIterable<Uint8Array>) chunks.push(chunk);
const buf = Buffer.concat(chunks);
return {
content: [{
type: 'text',
text: JSON.stringify({ audio_base64: buf.toString('base64'), bytes: buf.byteLength }),
}],
};
});
Pauses with <break>
The <break> element inserts silence. It accepts either a time attribute (in seconds or milliseconds) or a strength attribute (preset relative durations):
<speak>
First, let me summarize the key findings.
<break time="1s"/>
Revenue is up 12 percent year over year.
<break time="500ms"/>
Operating costs decreased by 8 percent.
<break strength="x-strong"/>
In conclusion, the business is growing efficiently.
</speak>
| strength value | Approximate duration | Typical use |
|---|---|---|
none | 0 ms (removes natural pause) | Eliminate automatic word pause at punctuation |
x-weak | ~50 ms | Minimal hesitation between clauses |
weak | ~100 ms | Between items in a list |
medium | ~250 ms | Default for a comma |
strong | ~700 ms | Default for a period |
x-strong | ~1,200 ms | Section break or end of a major point |
The time attribute accepts values up to 10 seconds (time="10s"). Values with units other than s or ms are rejected. Do not use <break> between words inside a clause to simulate a dramatic pause — use strength="none" to remove undesired commas or use <prosody rate="slow"> around the clause instead.
Prosody: rate, pitch, and volume
<speak>
<!-- Slow down for important information -->
Your verification code is
<prosody rate="x-slow" volume="loud">
<say-as interpret-as="digits">482910</say-as>
</prosody>.
<break time="800ms"/>
<!-- Speed up for a disclaimer -->
<prosody rate="fast" volume="soft">
Terms and conditions apply. See website for details.
</prosody>
</speak>
| Attribute | Named values | Percentage values |
|---|---|---|
rate |
x-slow, slow, medium, fast, x-fast |
50% (half speed) to 200% (double speed) |
pitch |
x-low, low, medium, high, x-high |
-20% to +20% |
volume |
silent, x-soft, soft, medium, loud, x-loud |
+6dB, -6dB (relative dB adjustments) |
Prosody tags can be nested. Inner tags take precedence over outer tags for the same attribute. Keep pitch adjustments under ±10% for natural-sounding output — extreme pitch values produce an unnatural chipmunk or low-robot effect. The standard engine supports prosody more reliably than neural; the neural engine may ignore subtle rate and pitch adjustments and apply its own prosodic model instead.
Pronunciation with <phoneme>
Domain-specific terms, product names, and proper nouns are frequently mispronounced by TTS systems. Use <phoneme> with International Phonetic Alphabet (IPA) to override the default pronunciation:
<speak>
Welcome to <phoneme alphabet="ipa" ph="ˈeɪlɪvˌɛm.siˈpiː">AliveMCP</phoneme>,
the uptime monitoring dashboard for
<phoneme alphabet="ipa" ph="ˌɛm.siˈpiː">MCP</phoneme> servers.
<break time="500ms"/>
Your server is currently
<phoneme alphabet="ipa" ph="ˈhɛlθi">healthy</phoneme>.
</speak>
Polly also supports the X-SAMPA phoneme alphabet via alphabet="x-sampa" — useful if your team already has X-SAMPA pronunciation data. IPA is preferred for English. To find the IPA transcription for a word, use a dictionary tool or the CMU Pronouncing Dictionary. When correcting a product name, test the SSML with SynthesizeSpeechCommand in the Polly console before shipping to production — IPA transcriptions are not validated by the API and a wrong IPA string silently produces garbled audio.
Formatting with <say-as>
The <say-as interpret-as> element tells Polly how to interpret and speak a text value. Without it, Polly uses heuristics that are often wrong for structured data:
<speak>
<!-- Read digits individually (not as a number: "four hundred and four") -->
Error code <say-as interpret-as="digits">404</say-as>.
<!-- Spell out the letters of an acronym -->
Your <say-as interpret-as="spell-out">API</say-as> key has been rotated.
<!-- Phone number with area code -->
Call us at <say-as interpret-as="telephone">+1-800-555-0100</say-as>.
<!-- Date: "November fourth twenty twenty-six" -->
Created on <say-as interpret-as="date" format="mdy">11/04/2026</say-as>.
<!-- Time: "ten thirty AM" -->
Scheduled at <say-as interpret-as="time" format="hms12">10:30am</say-as>.
<!-- Cardinal number -->
You have <say-as interpret-as="cardinal">1024</say-as> unread messages.
<!-- Ordinal -->
This is the <say-as interpret-as="ordinal">3</say-as> attempt.
</speak>
| interpret-as value | Input example | Spoken as |
|---|---|---|
digits | 404 | "four zero four" |
spell-out | API | "A P I" |
cardinal | 1024 | "one thousand twenty-four" |
ordinal | 3 | "third" |
fraction | 3/4 | "three quarters" |
unit | 5kg | "five kilograms" |
date | 11/04/2026 (format="mdy") | "November fourth twenty twenty-six" |
time | 10:30am (format="hms12") | "ten thirty A M" |
telephone | +1-800-555-0100 | "plus one eight hundred five five five zero one zero zero" |
address | 500 Main St | "five hundred Main Street" |
expletive | damn | Bleeps the word (content moderation) |
Acronym expansion with <sub>
Use <sub alias="..."> to replace a term with an alternative pronunciation without changing the displayed text. This is useful when the spoken form of an acronym or symbol differs from its written form:
<speak>
Your <sub alias="sequel">SQL</sub> query completed in 42 milliseconds.
The <sub alias="Structured Query Language">SQL</sub> standard was updated in 2023.
Response time: <sub alias="forty-two milliseconds">42ms</sub>.
Uptime: <sub alias="ninety-nine point nine percent">99.9%</sub>.
</speak>
Unlike <phoneme> which specifies IPA, <sub alias> takes a plain-text string that Polly synthesizes normally. This makes it easier to use than IPA for most substitutions — you write what you want spoken, not a phonetic encoding. <sub> is not supported by the generative engine.
Inline language switching with <lang>
<speak>
The French phrase for "good morning" is
<lang xml:lang="fr-FR">bonjour</lang>.
In Japanese, it's
<lang xml:lang="ja-JP">おはようございます</lang>.
</speak>
The <lang> tag switches pronunciation rules for the enclosed text. The outer voice's language rules apply to all text outside the tag. This feature requires that the selected VoiceId supports the target language as an additional language — not all voices do. Query GetVoicesCommand and inspect AdditionalLanguageCodes for the voice you're using. If the voice doesn't support the target language, Polly ignores the <lang> tag and applies the voice's primary language rules.
Common SSML pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
Unescaped & in text |
InvalidSsmlException: The SSML is not valid |
Replace & with & in all text nodes |
Unescaped < or > |
SSML parse error or truncated speech | Escape as < and > |
Missing <speak> root |
InvalidSsmlException |
Always wrap in <speak>...</speak> |
| SSML with generative engine | SSML tags stripped; plain text spoken | Use neural or long-form engine for SSML |
| IPA string with wrong symbols | Garbled or mispronounced word (no error thrown) | Validate IPA against a dictionary before shipping |
| SSML > 6,000 characters | TextLengthExceededException |
Split at SSML element boundaries and concatenate audio |
Comments in SSML (<!-- -->) |
Included in character count, may cause size errors | Strip SSML comments before submitting |