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:
@@ -204,3 +204,20 @@ if SPEECH_TO_SPEECH_URL or S2S_PROXY_TARGET:
|
||||
SERPER_URL = "https://google.serper.dev/search"
|
||||
# Cap results so the tool output stays small enough to feed back to the model.
|
||||
MAX_RESULTS = 5
|
||||
|
||||
# Optional authenticated AquaPortal MCP integration. Legacy deployments remain unchanged.
|
||||
S2S_MCP_URL = os.environ.get("S2S_MCP_URL", "").strip()
|
||||
|
||||
|
||||
def _bounded_mcp_int(name: str, default: int, minimum: int, maximum: int) -> int:
|
||||
try:
|
||||
value = int(os.environ.get(name, str(default)))
|
||||
except ValueError:
|
||||
value = default
|
||||
return max(minimum, min(maximum, value))
|
||||
|
||||
|
||||
S2S_MCP_TIMEOUT_SEC = _bounded_mcp_int("S2S_MCP_TIMEOUT_SEC", 20, 1, 120)
|
||||
S2S_MCP_MAX_CALLS_PER_TURN = _bounded_mcp_int("S2S_MCP_MAX_CALLS_PER_TURN", 6, 1, 20)
|
||||
S2S_CONVERSATION_TTL_SEC = _bounded_mcp_int("S2S_CONVERSATION_TTL_SEC", 3600, 60, 86400)
|
||||
S2S_MAX_CONVERSATIONS = _bounded_mcp_int("S2S_MAX_CONVERSATIONS", 1000, 1, 10000)
|
||||
|
||||
@@ -59,3 +59,13 @@ app_logging.logger.info(
|
||||
else "-",
|
||||
)
|
||||
del _STARTUP_APIPILOT_EXEC_URL
|
||||
|
||||
if settings.S2S_MCP_URL:
|
||||
app_logging.logger.info(
|
||||
"[STARTUP] AquaPortal MCP enabled: per-caller sessions, legacy content preloads disabled; "
|
||||
"operationTimeout=%ds maxCallsPerTurn=%d conversationTtl=%ds capacity=%d",
|
||||
settings.S2S_MCP_TIMEOUT_SEC,
|
||||
settings.S2S_MCP_MAX_CALLS_PER_TURN,
|
||||
settings.S2S_CONVERSATION_TTL_SEC,
|
||||
settings.S2S_MAX_CONVERSATIONS,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,9 @@ if not _logging_configured:
|
||||
logging.getLogger("uvicorn").setLevel(logging.DEBUG if _is_debug else logging.INFO)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.DEBUG if _is_debug else logging.INFO)
|
||||
logging.getLogger("httpcore").setLevel(logging.DEBUG if _is_debug else logging.WARNING)
|
||||
# The MCP SDK logs raw protocol messages and error bodies. Our integration
|
||||
# reports safe operation metadata and public errors at its own boundary.
|
||||
logging.getLogger("mcp").setLevel(logging.CRITICAL)
|
||||
logger.setLevel(logging.DEBUG if _is_debug else logging.DEBUG) # s2s always uses at least DEBUG in development
|
||||
_logging_configured = True
|
||||
logger.debug(
|
||||
|
||||
@@ -42,6 +42,8 @@ def _proxy_upstream_url(ws: WebSocket) -> str:
|
||||
target = urlsplit(settings.S2S_PROXY_TARGET)
|
||||
merged_query = dict(parse_qsl(target.query, keep_blank_values=True))
|
||||
merged_query.update(parse_qsl(ws.url.query, keep_blank_values=True))
|
||||
if settings.S2S_MCP_URL:
|
||||
merged_query.pop("conversation_id", None)
|
||||
return urlunsplit(
|
||||
(
|
||||
target.scheme,
|
||||
|
||||
Reference in New Issue
Block a user