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
@@ -30,6 +30,8 @@ async def apipilot_list_tools(
A short in-process TTL cache is applied (default 5 min, configurable via
S2S_APIPILOT_TTL_SEC). The client may force a refresh via `?refresh=true`
(e.g. after a user just logged in and previously saw only requires_auth tools)."""
if settings.S2S_MCP_URL:
raise HTTPException(409, "Las herramientas de AquaPortal usan la sesión de conversación.")
if not settings.S2S_APIPILOT_TOOLS_URL:
return validated_json_response(
model=ToolCatalogResponse,
@@ -101,6 +103,8 @@ async def apipilot_execute(request: Request, body: models_requests.ApipilotExecu
Show only `error.message` to the end user. `error.code`, `error.status_code`
and `error.raw` are for internal auditing (guide §2.2, recommended UI errors)."""
if settings.S2S_MCP_URL:
raise HTTPException(409, "Las herramientas de AquaPortal usan la sesión de conversación.")
if not settings.S2S_APIPILOT_TOOLS_URL:
return validated_json_response(
model=ToolExecutionResponse,