Files
voicebox/backend/routes/mcp_bindings.py
T
James PineandClaude Opus 4.7 0cef2c9fe1 feat(mcp): local MCP server exposes voicebox.* tools to AI agents
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]>
2026-04-22 22:05:30 -07:00

80 lines
2.1 KiB
Python

"""REST endpoints for per-MCP-client voice binding settings.
The Settings UI uses these to let users configure distinct voices per
agent (Claude Code in Morgan, Cursor in Scarlett, ...). The ``client_id``
column is the same value the MCP client sends in ``X-Voicebox-Client-Id``
(or the stdio shim pulls from ``VOICEBOX_CLIENT_ID``).
"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from .. import models
from ..database import get_db
from ..database.models import MCPClientBinding
router = APIRouter()
@router.get(
"/mcp/bindings",
response_model=models.MCPClientBindingListResponse,
)
async def list_mcp_bindings(db: Session = Depends(get_db)):
rows = (
db.query(MCPClientBinding)
.order_by(MCPClientBinding.client_id)
.all()
)
return models.MCPClientBindingListResponse(
items=[models.MCPClientBindingResponse.model_validate(r) for r in rows]
)
@router.put(
"/mcp/bindings",
response_model=models.MCPClientBindingResponse,
)
async def upsert_mcp_binding(
data: models.MCPClientBindingUpsert,
db: Session = Depends(get_db),
):
"""Create-or-update a binding. Matches by client_id."""
row = (
db.query(MCPClientBinding)
.filter(MCPClientBinding.client_id == data.client_id)
.first()
)
if row is None:
row = MCPClientBinding(client_id=data.client_id)
db.add(row)
row.label = data.label
row.profile_id = data.profile_id
row.default_engine = data.default_engine
row.default_intent = data.default_intent
row.updated_at = datetime.utcnow()
db.commit()
db.refresh(row)
return models.MCPClientBindingResponse.model_validate(row)
@router.delete("/mcp/bindings/{client_id}")
async def delete_mcp_binding(
client_id: str,
db: Session = Depends(get_db),
):
row = (
db.query(MCPClientBinding)
.filter(MCPClientBinding.client_id == client_id)
.first()
)
if row is None:
raise HTTPException(status_code=404, detail="Binding not found")
db.delete(row)
db.commit()
return {"deleted": client_id}