#!/usr/bin/env python3 """ Chatterbox TTS – MCP-Adapter Setzt einen laufenden tts_service.py voraus (Standard: http://127.0.0.1:9999). Start (streamable-http, Port 8001 – für beliebige MCP-Clients): python mcp_adapter.py Start (stdio – für Claude Code / Claude Desktop): python mcp_adapter.py --stdio Claude Code (bereits konfiguriert via `claude mcp add --scope user`): claude mcp add --scope user chatterbox-tts \ /home/dschlueter/miniforge3/envs/chatterbox/bin/python \ /home/dschlueter/chatterbox-tts-cli/mcp_adapter.py --stdio Umgebungsvariable TTS_URL überschreibt die Service-Adresse: TTS_URL=http://192.168.1.10:9999 python mcp_adapter.py --stdio """ from __future__ import annotations import argparse import os import httpx from mcp.server.fastmcp import FastMCP TTS_URL = os.environ.get("TTS_URL", "http://127.0.0.1:9999").rstrip("/") mcp = FastMCP( "Chatterbox TTS", instructions=( "Lokaler Text-to-Speech-Service. Liest Texte auf Deutsch und 20+ weiteren " "Sprachen vor. Unterstützt Voice Cloning, Geschwindigkeitsanpassung und " "Aussprache-Wörterbücher." ), port=8001, ) # --------------------------------------------------------------------------- # Tools # --------------------------------------------------------------------------- def _raise_for_status(r: httpx.Response) -> None: """Wirft einen klaren Fehler bei HTTP-4xx/5xx statt rohem httpx-Fehler.""" try: r.raise_for_status() except httpx.HTTPStatusError as exc: raise RuntimeError( f"TTS-Service antwortet mit HTTP {exc.response.status_code}: {exc.response.text[:200]}" ) from exc @mcp.tool() async def speak( text: str, lang: str = "de", voice: str | None = None, interrupt: bool = False, speed: float = 1.0, session_id: str | None = None, ) -> dict: """Text als Sprache ausgeben. Reiht den Text in die Ausgabewarteschlange ein. Das Modell generiert satzweise und beginnt sofort mit der Wiedergabe. Args: text: Auszugebender Text (max. 4000 Zeichen). lang: Sprachcode, z. B. 'de', 'en', 'fr'. Standard: 'de'. voice: Optionaler Pfad zu einer WAV-Referenzdatei (10–30s) für Voice Cloning. interrupt: True = laufende Ausgabe sofort unterbrechen und diesen Text vorgezogen abspielen. speed: Wiedergabegeschwindigkeit (0.5–2.0). Pitch bleibt gleich. session_id: Optionale Session-ID für Job-Tracking im TTS-Service. """ async with httpx.AsyncClient(timeout=30) as client: r = await client.post(f"{TTS_URL}/speak", json={ "text": text, "lang": lang, "voice": voice, "interrupt": interrupt, "speed": speed, "session_id": session_id, }) _raise_for_status(r) return r.json() @mcp.tool() async def stop() -> dict: """Laufende Sprachausgabe sofort stoppen und Warteschlange leeren.""" async with httpx.AsyncClient(timeout=10) as client: r = await client.post(f"{TTS_URL}/stop") _raise_for_status(r) return r.json() @mcp.tool() async def pause() -> dict: """Laufende Sprachausgabe pausieren (ohne Datenverlust). Mit resume() fortsetzen.""" async with httpx.AsyncClient(timeout=10) as client: r = await client.post(f"{TTS_URL}/pause") _raise_for_status(r) return r.json() @mcp.tool() async def resume() -> dict: """Pausierte Sprachausgabe fortsetzen.""" async with httpx.AsyncClient(timeout=10) as client: r = await client.post(f"{TTS_URL}/resume") _raise_for_status(r) return r.json() @mcp.tool() async def get_status() -> dict: """Aktuellen Ausgabe-Status abfragen. Gibt zurück: laufender Job (mit Chunk-Fortschritt), Queue-Länge und die letzten abgeschlossenen Jobs. """ async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{TTS_URL}/status") _raise_for_status(r) return r.json() @mcp.tool() async def list_voices() -> dict: """Unterstützte Sprachen und Hinweise zu Voice Cloning abfragen.""" async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{TTS_URL}/voices") _raise_for_status(r) return r.json() # --------------------------------------------------------------------------- # Einstiegspunkt # --------------------------------------------------------------------------- if __name__ == "__main__": parser = argparse.ArgumentParser(description="Chatterbox TTS MCP-Adapter") parser.add_argument( "--stdio", action="store_true", help="stdio-Transport (für Claude Code / Claude Desktop)", ) parser.add_argument("--host", default="127.0.0.1", help="Host für streamable-http (Standard: 127.0.0.1)") parser.add_argument("--port", type=int, default=8001, help="Port für streamable-http (Standard: 8001)") args = parser.parse_args() if args.stdio: mcp.run() # stdio ist der Default-Transport else: mcp.run(transport="streamable-http", host=args.host, port=args.port)