Files
alvaro 96895996d9 feat(mcp): connect authenticated AquaPortal tools to Vocero (#1)
Vocero needs to query AquaPortal on behalf of the caller selected by the existing phone link. This adds server-owned conversations and MCP tool execution for voice and HTTP reasoning while leaving the speech pipeline unchanged.

Release notes:

- Added browser-bound conversations that keep backend credentials server-side and isolate callers.
- Connected the eight-tool AquaPortal catalog through authenticated Streamable HTTP with full input schemas and structured results.
- Added bounded multi-step tool execution with cancellation, expiry, and stale-result protection.
- Updated browser bootstrap, tool progress, reasoning headers, and conversation cleanup.
- Disabled legacy unscoped content preloads in MCP mode and documented configuration and deployment constraints.
- Preserved disabled-mode contracts and added local SDK, WebSocket, and browser regression coverage.

Reviewed-on: AquaPortal/s2s#1
2026-10-08 08:49:39 -04:00

49 lines
1.6 KiB
Python

"""Assemble the client API and retain its existing documentation URLs."""
import asyncio
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager, suppress
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from vocero_api.api.router import router
from vocero_api.core import config, diagnostics # noqa: F401 - retain the startup banner
from vocero_api.services.conversations import store
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async def expire_conversations() -> None:
while True:
await asyncio.sleep(30)
store.prune()
cleanup = asyncio.create_task(expire_conversations()) if config.S2S_MCP_URL else None
try:
yield
finally:
if cleanup is not None:
cleanup.cancel()
with suppress(asyncio.CancelledError):
await cleanup
store.clear()
app = FastAPI(title="s2s-demo", lifespan=lifespan)
app.include_router(router)
@app.get("/project", include_in_schema=False)
@app.get("/project/", include_in_schema=False)
@app.get("/project/index.html", include_in_schema=False)
@app.get("/public/docs.html", include_in_schema=False)
async def project_documentation() -> RedirectResponse:
return RedirectResponse("/docs/proposals/vocero-architecture.html", status_code=307)
# The existing FastAPI /docs page remains registered ahead of this mount.
app.mount("/docs", StaticFiles(directory=os.path.join(config.ROOT_DIR, "docs"), html=True), name="documentation")