Azure Neural TTS as an MCP tool for Claude Code
Send text → get back a downloadable MP3 URL in seconds.
graph LR
A[Claude Code] --> B[Local MCP Server<br/>Node.js stdio]
A --> M[mcp-remote CLI]
M -->|Google OAuth| O[Google Sign-In]
M -->|Bearer token| B2[Remote MCP Server<br/>Next.js on Vercel]
B2 -->|tokeninfo| O
B --> C[App Service<br/>C#, Azure]
B2 --> C
C --> D[Azure Speech SDK]
C --> E[Azure Blob Storage]
E --> F[SAS URL<br/>1hr expiry]
C --> G[Returns<br/>url, duration, voice]
A C# ASP.NET Core minimal API running on Azure App Service (F1 free tier) that:
- Accepts a POST with text + voice + format
- Synthesizes speech via Azure Cognitive Services Neural TTS
- Uploads the MP3 to Azure Blob Storage
- Returns a time-limited SAS download URL
Two MCP server options wrap the HTTP call so Claude Code can use it as a native tool (mcp__tts__synthesize_speech):
- Local (
mcp-server/) — stdio-based, runs on your machine alongside Claude Code - Remote (
mcp-remote/) — HTTP-based Next.js app deployed to Vercel, accessible from anywhere with optional Bearer token auth
- .NET 10 SDK
- Node.js 18+
- Azure CLI
- An Azure subscription (
az loginto authenticate)
$RG = "rg-xxx"
$LOCATION = "eastus2"
$SPEECH_NAME = "speech-xxx"
$STORAGE_NAME = "xxx"
$APP_NAME = "app-xxx"
$PLAN_NAME = "plan-xxx"
$PLAN_LOCATION = "centralus" # F1 free tier may not be available in all regionsaz group create --name $RG --location $LOCATIONaz --% cognitiveservices account create --name speech-claudetts --resource-group rg-claudettsserver --kind SpeechServices --sku F0 --location eastus2 --yesNote: Use
az --%(stop-parsing token) to prevent PowerShell from mangling arguments.
az storage account create --name $STORAGE_NAME --resource-group $RG --location $LOCATION --sku Standard_LRS --kind StorageV2az appservice plan create --name $PLAN_NAME --resource-group $RG --location $PLAN_LOCATION --sku F1 --is-linuxNote: F1 free tier has quota limits per region. If you get a quota error, try a different
--location(e.g.,centralus,westus2,eastus).
az --% webapp create --name app-claudetts --resource-group rg-claudettsserver --plan plan-claudetts --runtime "DOTNETCORE:10.0"$SPEECH_KEY = az cognitiveservices account keys list --name $SPEECH_NAME --resource-group $RG --query key1 -o tsv
$STORAGE_CONN = az storage account show-connection-string --name $STORAGE_NAME --resource-group $RG --query connectionString -o tsv
az webapp config appsettings set --name $APP_NAME --resource-group $RG --settings AZURE_SPEECH_KEY="$SPEECH_KEY" AZURE_SPEECH_REGION="$LOCATION" AZURE_STORAGE_CONNECTION_STRING="$STORAGE_CONN"If
az webapp config appsettings setfails with a version error (known Azure CLI bug on PowerShell), use the REST API instead:# 1. List current settings az --% rest --method POST --uri "/subscriptions/<SUB_ID>/resourceGroups/rg-claudettsserver/providers/Microsoft.Web/sites/app-claudetts/config/appsettings/list?api-version=2023-12-01" -o json > current_settings.json # 2. PUT all settings (existing + custom) via REST az --% rest --method PUT --uri "/subscriptions/<SUB_ID>/resourceGroups/rg-claudettsserver/providers/Microsoft.Web/sites/app-claudetts/config/appsettings?api-version=2023-12-01" --body "{\"properties\":{...merge existing + your 3 keys...}}"
cd C:\Dev\ClaudeChatTTSServer
dotnet publish -c Release -o ./publish
Compress-Archive -Path ./publish/* -DestinationPath ./deploy.zip -Force
az webapp deploy --name $APP_NAME --resource-group $RG --src-path ./deploy.zip --type zip# Health check
Invoke-WebRequest -Uri "https://$APP_NAME.azurewebsites.net/"
# Synthesize speech
Invoke-WebRequest -Uri "https://$APP_NAME.azurewebsites.net/api/tts" -Method POST -ContentType "application/json" -Body '{"text":"Hello, this is a test."}'cd C:\Dev\ClaudeChatTTSServer\mcp-server
npm install
npm run buildCopy .mcp.json to your home directory so the TTS tool is available in every Claude Code project:
Copy-Item C:\Dev\ClaudeChatTTSServer\.mcp.json C:\Users\<YOUR_USERNAME>\.mcp.jsonOr to ~/.claude/.mcp.json if the home root doesn't work.
Important: Edit
.mcp.jsonand setTTS_ENDPOINTto your Azure App Service URL (e.g.https://your-app.azurewebsites.net/api/ttsorhttp://localhost:7841/api/ttsfor local development). The MCP server will not start without it.
Fully quit and reopen Claude Code. You should see tts in the MCP server list.
Just ask Claude:
"Generate audio for: Welcome to the show, folks!"
Claude will call mcp__tts__synthesize_speech and return a downloadable MP3 URL.
A cloud-hosted alternative that requires no local setup — deploy once and connect from any machine. Uses Google OAuth 2.0 to restrict access to a single authorized email.
cd C:\Dev\ClaudeChatTTSServer\mcp-remote
npm install
vercel deploy --prodIn the Vercel dashboard (or via CLI), set:
| Variable | Required | Description |
|---|---|---|
TTS_ENDPOINT |
Yes | URL of your C# TTS API (e.g. https://your-app.azurewebsites.net/api/tts) |
ALLOWED_EMAIL |
No | Google email authorized to use the server. If unset, the server is open (dev only) |
vercel env add TTS_ENDPOINT production
vercel env add ALLOWED_EMAIL productionAdd the remote MCP server to your .mcp.json with your Google OAuth client credentials:
{
"mcpServers": {
"tts": {
"command": "npx",
"args": [
"mcp-remote",
"https://your-project.vercel.app/api/mcp",
"--oauth-client-id", "<YOUR_GOOGLE_CLIENT_ID>",
"--oauth-client-secret", "<YOUR_GOOGLE_CLIENT_SECRET>"
]
}
}
}The mcp-remote CLI handles the OAuth browser flow automatically — it opens a Google sign-in page, exchanges the authorization code for an access token, and passes it as a Bearer token on every MCP request.
sequenceDiagram
participant CC as Claude Code
participant MR as mcp-remote CLI
participant G as Google OAuth
participant V as Vercel MCP Server
CC->>MR: Start MCP session
MR->>V: GET /.well-known/oauth-protected-resource
V-->>MR: Auth server = accounts.google.com
MR->>G: Authorization request (browser)
G-->>MR: Access token
MR->>V: MCP request + Bearer token
V->>G: GET tokeninfo?access_token=...
G-->>V: {email, email_verified, scope}
V->>V: Verify email matches ALLOWED_EMAIL
V-->>MR: MCP response
MR-->>CC: Tool result
How it works:
- Discovery —
mcp-remotefetches/.well-known/oauth-protected-resourcefrom the Vercel server, which advertiseshttps://accounts.google.comas the authorization server - Browser login —
mcp-remoteopens a Google sign-in page using the configured OAuth client ID/secret. The user signs in and grants access - Token relay —
mcp-remoteattaches the Google access token as aBearertoken on every MCP request - Server-side verification — the Vercel server validates the token against Google's
tokeninfoendpoint and checks that the email is verified and matchesALLOWED_EMAIL - Access granted — if the email matches, the request proceeds to the TTS tool; otherwise it returns
401 Unauthorized
| Mode | Condition | Behavior |
|---|---|---|
| Open (dev) | ALLOWED_EMAIL not set |
All requests accepted, no token required |
| Google OAuth | ALLOWED_EMAIL set |
Bearer token required, email must match |
To create your own OAuth client credentials:
- Go to Google Cloud Console → Credentials
- Create an OAuth 2.0 Client ID (type: Desktop app or Web application)
- Note the Client ID and Client Secret
- Add them to your
.mcp.jsonas--oauth-client-idand--oauth-client-secret - Set
ALLOWED_EMAILin Vercel to the Google account you'll sign in with
| Field | Type | Default | Description |
|---|---|---|---|
text |
string | (required) | Text to synthesize (max 100K chars) |
voice |
string | en-US-AriaNeural |
Azure Neural TTS voice name |
format |
string | audio-16khz-128kbitrate-mono-mp3 |
Output audio format |
Response:
{
"url": "https://stclaudetts.blob.core.windows.net/tts-audio/abc123.mp3?sv=...",
"durationSeconds": 3.2,
"voice": "en-US-AriaNeural",
"characterCount": 42
}Health check. Returns {"status":"healthy","service":"ClaudeChatTTSServer"}.
| Voice | Gender | Style |
|---|---|---|
en-US-AriaNeural |
Female | Conversational (default) |
en-US-GuyNeural |
Male | Conversational |
en-US-JennyNeural |
Female | Warm |
en-US-DavisNeural |
Male | Calm |
en-US-SaraNeural |
Female | Cheerful |
en-US-TonyNeural |
Male | Friendly |
en-US-NancyNeural |
Female | Empathetic |
en-GB-SoniaNeural |
Female | British |
en-GB-RyanNeural |
Male | British |
en-US-AvaMultilingualNeural |
Female | Multilingual |
en-US-AndrewMultilingualNeural |
Male | Multilingual |
| Format | Quality | Use Case |
|---|---|---|
audio-16khz-128kbitrate-mono-mp3 |
Good | Default, small file size |
audio-24khz-160kbitrate-mono-mp3 |
Better | Higher quality |
audio-48khz-192kbitrate-mono-mp3 |
Best | Studio quality |
dotnet testRuns 21 tests covering API endpoint validation, service error handling, format parsing, and model defaults. Uses WebApplicationFactory with mocked dependencies — no Azure credentials needed.
cd mcp-server
npm testRuns 8 tests covering the synthesizeSpeech function: request formatting, success/error responses, default parameters, and network error handling.
ClaudeChatTTSServer/
Program.cs # Minimal API entry point
Models/
TtsRequest.cs # Input model
TtsResponse.cs # Output model
Services/
ITtsService.cs # Service interface
TtsService.cs # Speech synthesis + blob upload
ClaudeChatTTSServer.Tests/
ApiIntegrationTests.cs # API endpoint integration tests (xUnit)
TtsServiceTests.cs # Format parsing unit tests
ModelTests.cs # Model default/property tests
mcp-server/ # Local MCP server (stdio)
src/index.ts # Stdio transport entry point
src/server.ts # MCP server + tool registration
src/synthesize.ts # Extracted synthesis logic
src/synthesize.test.ts # Vitest tests
dist/ # Built output (run npm run build)
mcp-remote/ # Remote MCP server (Vercel)
app/api/[transport]/
route.ts # Next.js API route — MCP over HTTP + OAuth verification
app/.well-known/
oauth-protected-resource/
route.ts # OAuth metadata — advertises Google as auth server
app/page.tsx # Landing page
vercel.json # Vercel deployment config
package.json
.mcp.json # MCP config for Claude Code
appsettings.json # Local config (keys go in Azure app settings)
| Resource | Tier | Cost |
|---|---|---|
| App Service | F1 (free) | $0/month |
| Speech Services | F0 (free) | 500K chars/month free |
| Blob Storage | Standard LRS | ~$0.02/GB/month |
| Vercel | Hobby (free) | $0/month |
For occasional Claude-driven TTS, total cost is effectively $0/month.
If you exposed keys during setup (e.g., pasted them in chat), rotate them:
# Regenerate Speech key
az cognitiveservices account keys regenerate --name speech-claudetts --resource-group rg-claudettsserver --key-name key1
# Regenerate Storage key
az storage account keys renew --account-name stclaudetts --resource-group rg-claudettsserver --key primary
# Get new values and update app settings
$SPEECH_KEY = az cognitiveservices account keys list --name speech-claudetts --resource-group rg-claudettsserver --query key1 -o tsv
$STORAGE_CONN = az storage account show-connection-string --name stclaudetts --resource-group rg-claudettsserver --query connectionString -o tsv
az webapp config appsettings set --name app-claudetts --resource-group rg-claudettsserver --settings AZURE_SPEECH_KEY="$SPEECH_KEY" AZURE_STORAGE_CONNECTION_STRING="$STORAGE_CONN"