mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 21:00:42 -07:00
Mounts FastMCP at /mcp (Streamable HTTP) so Claude Code, Cursor, Windsurf, and the VS Code MCP extensions can call voicebox.speak, voicebox.transcribe, voicebox.list_captures, and voicebox.list_profiles against the running Voicebox server. Backend - new backend/mcp_server package (tools, middleware, profile resolve, pub/sub events); named mcp_server to avoid shadowing the installed mcp PyPI package FastMCP imports internally - app.py migrated from @app.on_event to lifespan= so FastMCP's session manager cohabits with Voicebox's startup/shutdown - new MCPClientBinding table + /mcp/bindings CRUD; ClientIdMiddleware reads X-Voicebox-Client-Id into a ContextVar and stamps last_seen_at - profile resolution precedence: explicit -> per-client binding -> capture_settings.default_playback_voice_id - POST /speak REST wrapper for non-MCP callers (shell, ACP, A2A) - GET /events/speak SSE broadcasts speak-start / speak-end so the pill surfaces agent-initiated speech - backend/mcp_shim proxy (plain httpx) for stdio-only MCP clients - PyInstaller spec updates + new --shim build target (~18 MB) Frontend - Settings -> MCP page with HTTP / stdio / claude-mcp-add copy snippets, default voice picker, per-client bindings table, connection status - useMCPBindings, useSpeakEvents hooks - CapturePill gains 'speaking' state; DictateWindow subscribes to SSE and emits dictate:show so the Rust side surfaces the pill window Native - tauri.conf.json externalBin now includes voicebox-mcp - show_dictate_window helper + dictate:show listener in main.rs - (also in this commit: InputMonitoringGate UX, hotkey_monitor tweaks, landing footer/navbar updates, new overview docs for captures / dictation / mcp-server / voice-personalities) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
"""Voice profile resolution for MCP tool calls.
|
|
|
|
Precedence:
|
|
1. Explicit tool arg (profile name or id)
|
|
2. Per-client MCPClientBinding.profile_id
|
|
3. CaptureSettings.default_playback_voice_id (global default)
|
|
4. None — caller raises a helpful error
|
|
"""
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..database import VoiceProfile as DBVoiceProfile, get_db
|
|
from ..database.models import CaptureSettings
|
|
from ..services.profiles import get_profile_orm_by_name_or_id as _lookup_profile
|
|
|
|
|
|
def resolve_profile(
|
|
explicit: str | None,
|
|
client_id: str | None,
|
|
db: Session,
|
|
) -> DBVoiceProfile | None:
|
|
"""Apply the full precedence chain and return the profile ORM row (or None)."""
|
|
if explicit:
|
|
profile = _lookup_profile(explicit, db)
|
|
if profile is not None:
|
|
return profile
|
|
# Explicit but not found — return None so the caller can report it.
|
|
return None
|
|
|
|
if client_id:
|
|
# Per-client binding. Imported lazily so this module stays importable
|
|
# even before the migration adds the table on first boot.
|
|
from ..database.models import MCPClientBinding # noqa: WPS433
|
|
|
|
binding = (
|
|
db.query(MCPClientBinding)
|
|
.filter(MCPClientBinding.client_id == client_id)
|
|
.first()
|
|
)
|
|
if binding and binding.profile_id:
|
|
profile = _lookup_profile(binding.profile_id, db)
|
|
if profile is not None:
|
|
return profile
|
|
|
|
# Global default from capture settings.
|
|
settings = db.query(CaptureSettings).filter(CaptureSettings.id == 1).first()
|
|
if settings and settings.default_playback_voice_id:
|
|
profile = _lookup_profile(settings.default_playback_voice_id, db)
|
|
if profile is not None:
|
|
return profile
|
|
|
|
return None
|
|
|
|
|
|
def with_db() -> Session:
|
|
"""Utility for tool handlers that aren't managed by FastAPI's Depends."""
|
|
return next(get_db())
|