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
This commit is contained in:
2026-10-08 08:49:39 -04:00
parent ccb1f708ca
commit 96895996d9
37 changed files with 2498 additions and 55 deletions
+24 -1
View File
@@ -1,6 +1,9 @@
"""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
@@ -8,8 +11,28 @@ 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
app = FastAPI(title="s2s-demo")
@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)