Files
TalkBox/backend/mcp_server/server.py
T
Labyricorn b8815e94ea
CI / frontend-quality (push) Canceled after 0s
rebrand: rename VoiceBox to TalkBox throughout codebase
- All 'voicebox'/'Voicebox'/'VOICEBOX' strings replaced with 'talkbox'/'TalkBox'/'TALKBOX'
- Port changed from 17493 to 17494 (avoids conflict with upstream VoiceBox)
- MCP tool namespace: voicebox.* -> talkbox.*
- App bundle ID: sh.voicebox.app -> com.talkbox.app
- Binary names: voicebox-server -> talkbox-server, voicebox-mcp -> talkbox-mcp
- Docker user/group: voicebox -> talkbox
- Database: voicebox.db -> talkbox.db
- Env vars: VOICEBOX_* -> TALKBOX_*
- Asset files renamed: voicebox-logo.* -> talkbox-logo.*, etc.
- External binaries in tauri.conf.json updated to talkbox-server/talkbox-mcp
2026-08-24 19:45:56 -07:00

80 lines
2.6 KiB
Python

"""Construct the FastMCP server and mount it on the FastAPI app.
The MCP endpoint lives at ``/mcp`` (Streamable HTTP transport). Modern MCP
clients (Claude Code, Cursor, Windsurf, VS Code MCP extensions) connect
directly via URL; older stdio-only clients use the ``talkbox-mcp`` shim
binary bundled with the desktop app.
"""
from __future__ import annotations
import logging
from contextlib import AsyncExitStack, asynccontextmanager
from collections.abc import Callable
from fastapi import FastAPI
from fastmcp import FastMCP
from .context import ClientIdMiddleware
from .tools import register_tools
logger = logging.getLogger(__name__)
def build_mcp_server() -> FastMCP:
"""Create the FastMCP instance with TalkBox tools registered."""
mcp = FastMCP(
name="talkbox",
instructions=(
"TalkBox is a local voice I/O layer. Use `talkbox.speak` to "
"play text in a voice profile, `talkbox.transcribe` for "
"audio→text, and the `list_*` tools to discover profiles and "
"captures."
),
)
register_tools(mcp)
return mcp
def mount_into(
app: FastAPI,
*,
extra_startup: Callable[[], None] | None = None,
) -> None:
"""Attach the MCP app to ``app`` at ``/mcp`` and install the client-id middleware.
``extra_startup`` — if provided, runs during the FastAPI lifespan. This
is the hook that lets ``app.py`` keep its existing startup/shutdown
bodies while also driving FastMCP's session manager.
"""
mcp = build_mcp_server()
mcp_app = mcp.http_app(path="/", transport="http")
# ClientIdMiddleware must run before FastMCP so the ContextVar is set
# by the time tool handlers execute. Starlette composes middlewares
# outermost-first, so adding here on the parent app is correct.
app.add_middleware(ClientIdMiddleware)
app.mount("/mcp", mcp_app)
app.state.mcp_lifespan = mcp_app.router.lifespan_context
logger.info("MCP: mounted at /mcp (FastMCP %s)", getattr(mcp, "version", ""))
def compose_lifespan(*lifespans):
"""Combine multiple async context managers into a single FastAPI lifespan.
Used by ``create_app`` to run the existing TalkBox startup/shutdown
together with FastMCP's session manager (which MUST run in the
ASGI lifespan for Streamable HTTP to work).
"""
@asynccontextmanager
async def _combined(app):
async with AsyncExitStack() as stack:
for cm_factory in lifespans:
cm = cm_factory(app) if callable(cm_factory) else cm_factory
await stack.enter_async_context(cm)
yield
return _combined