How VoiceStudio Manages Per-Agent Voice Bindings via `X-VoiceStudio-Client-Id` and the MCP REST API

VoiceStudio binds unique voices to external agents using the X-VoiceStudio-Client-Id HTTP header, which maps to persistent SQLite records via a RESTful MCP server.

The VoiceStudio MCP (Multi-Client Protocol) server enables multiple external agents—such as Claude Code, ElevenLabs clients, or custom LLM integrations—to each have their own dedicated voice configuration. This article explains the complete binding mechanism, from header ingestion through REST API management.

The X-VoiceStudio-Client-Id Header

VoiceStudio identifies each calling agent through a mandatory HTTP header. Per the source code in backend/mcp_server.py, the middleware extracts this value at line 410, where the code references: "The X-OmniVoice-Client-Id of the calling MCP client, if any" (the header was later renamed to X-VoiceStudio-Client-Id).

When present, this header value becomes the lookup key for agent-specific voice bindings. Requests without the header are treated as anonymous and receive no binding-based voice customization.

Database Schema for Persistent Bindings

Agent-to-voice mappings persist in SQLite via the mcp_client_bindings table. The schema is defined in backend/core/db.py at lines 152–159:

CREATE TABLE mcp_client_bindings (
    client_id TEXT PRIMARY KEY,
    label TEXT,
    profile_id TEXT,
    default_engine TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • client_id: The exact string passed in X-VoiceStudio-Client-Id
  • label: Human-readable description (e.g., "Claude Bot")
  • profile_id: References the voice profile to use
  • default_engine: Optional TTS engine override (e.g., "fast", "high-quality")

The table was introduced in migration backend/migrations/versions/0004_mcp_client_bindings.py, which runs automatically on server startup.

CRUD Service Layer in mcp_bindings.py

The service layer at backend/services/mcp_bindings.py provides three core operations:

  • get_binding(client_id) — Returns the voice profile and engine for a given client ID, or None if unbound
  • set_binding(client_id, label, profile_id, default_engine) — Creates new or updates existing binding
  • delete_binding(client_id) — Permanently removes a binding

These functions enforce data validation and handle concurrent access through SQLite's connection pooling.

MCP REST API Endpoints

The REST API under the /mcp prefix exposes full CRUD for managing bindings programmatically. Routes are implemented in the MCP router (referenced from backend/mcp_server.py).

Retrieve a Binding

curl -X GET http://localhost:3900/mcp/bindings/{client_id}

Returns JSON with label, profile_id, and default_engine for the specified client.

Create or Update a Binding

curl -X POST http://localhost:3900/mcp/bindings \
     -H "Content-Type: application/json" \
     -d '{
       "client_id": "claude-code",
       "label": "Claude Bot",
       "profile_id": "warm-professional",
       "default_engine": "fast"
     }'

Idempotent operation—creates if new, updates if existing.

Delete a Binding

curl -X DELETE http://localhost:3900/mcp/bindings/{client_id}

Permanently removes the association. Subsequent requests from that client ID will be anonymous.

Request Flow: Header to Voice Resolution

The complete per-agent voice resolution works as follows:

  1. Request arrives with X-VoiceStudio-Client-Id: claude-code
  2. Middleware extracts the header value in backend/mcp_server.py
  3. Database lookup calls get_binding("claude-code") from mcp_bindings.py
  4. Voice injection merges the bound profile_id and default_engine into the request context
  5. TTS generation uses the resolved voice parameters instead of defaults

This ensures consistent voice identity across sessions without requiring the client to specify voice parameters in every request.

Client Integration Example

Python clients can bind and use voices in two API calls:

import requests

MCP_BASE = "http://localhost:3900"

def register_binding(client_id: str, voice: str, engine: str = "fast"):
    """Create or update this agent's voice binding."""
    payload = {
        "client_id": client_id,
        "label": f"Agent {client_id}",
        "profile_id": voice,
        "default_engine": engine
    }
    resp = requests.post(f"{MCP_BASE}/mcp/bindings", json=payload)
    resp.raise_for_status()
    return resp.json()

def speak(text: str, client_id: str) -> bytes:
    """Synthesize speech using the bound voice for this client."""
    headers = {"X-VoiceStudio-Client-Id": client_id}
    resp = requests.post(
        f"{MCP_BASE}/v1/audio/speech",
        headers=headers,
        json={"text": text}
    )
    resp.raise_for_status()
    return resp.content

# Usage

register_binding("claude-code", voice="warm-professional")
audio = speak("Welcome to VoiceStudio", client_id="claude-code")

Frontend Management Interface

Users can visually manage bindings through frontend/src/components/settings/MCPBindingsPanel.jsx. The panel:

  • Lists all existing bindings with labels and profile names
  • Provides forms for creating new bindings
  • Calls the same /mcp/bindings REST endpoints
  • Updates in real-time as bindings change

This bridges the API functionality with non-technical users who prefer GUI management.

Summary

  • The X-VoiceStudio-Client-Id header uniquely identifies each external agent
  • mcp_client_bindings table in SQLite persists agent-to-voice mappings
  • backend/services/mcp_bindings.py provides get_binding, set_binding, and delete_binding operations
  • REST endpoints at /mcp/bindings/* enable programmatic CRUD management
  • Middleware in mcp_server.py automatically resolves and injects bound voices into the TTS pipeline
  • Frontend panel offers visual binding management for end users

Frequently Asked Questions

What happens if X-VoiceStudio-Client-Id is missing from a request?

The request proceeds as anonymous with no voice binding applied. The TTS pipeline falls back to default voice settings configured at the server level.

Can multiple agents share the same voice profile?

Yes. The profile_id field is not unique—multiple client_id entries can reference the same voice profile. Each agent maintains independent default_engine settings even when sharing a profile.

Is the client ID validated or restricted to specific formats?

Per the source code, client IDs are treated as opaque strings. No format validation enforces patterns, though the SQLite schema uses TEXT PRIMARY KEY which implies reasonable length limits and prohibits null values.

How do I migrate or back up agent bindings?

Since bindings store in SQLite, standard database tools apply. Export mcp_client_bindings via sqlite3 .dump, or copy the database file when the server is offline. The Alembic migration system also ensures schema compatibility across VoiceStudio versions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →