# 09 — External APIs, Protocols, and Responses ## HTTP Client Conventions **Rule:** Host-authored ordinary outbound HTTP must go through `RequestUtils` from `app/adapters/network/http.py`. This transport rule does not authorize Application or Chain to import the concrete Adapter; they own/use a Port and receive its implementation from startup. Plugins import the curated facade from `app.sdk.network`. Direct `requests`/`httpx` clients, SDK transports, product streaming protocols, contained vendor code, diagnostics and control-plane access are governed by the exact `direct_egress` facts and manual policy. An approved fingerprint is containment for that exact source/binding/use identity only; `owner: "$source"` names that source module and does not authorize another caller or operation. New SDK roots must be added to the registry and receive explicit policy review. Ordinary direct HTTP, RequestUtils Session bridges and Application-owned DNS I/O remain `temporary_debt` with an empty target state. Runtime wildcard imports from registered egress roots are forbidden. `RequestUtils` handles: - Proxy configuration (from `settings.PROXY_*`) - Timeouts - SSL verification settings - User-Agent headers - Retry logic ```python # Adapter implementation example; Application/Chain consume an injected Port instead. from app.adapters.network.http import RequestUtils res = RequestUtils( ua=settings.USER_AGENT, proxies=settings.PROXY, timeout=30, ).get_res(url="https://api.example.com/data") if res and res.status_code == 200: data = res.json() ``` --- ## Response Format — REST API All REST API responses use Pydantic schema models from `app/schemas/`. Do not return raw `dict` objects from endpoints. ### Standard Response Patterns ```python # Success with data from app.schemas.response import Response return Response(success=True, message="", data=result) # Success without data return Response(success=True, message="操作成功") # Error return Response(success=False, message="错误原因描述") ``` ### List Responses For paginated lists, follow the pattern of existing endpoint files. Check `app/api/endpoints/` for examples matching the resource domain. ### Error Responses (Endpoint Layer Only) In endpoints, raise `HTTPException` for request-level errors: ```python from fastapi import HTTPException raise HTTPException(status_code=404, detail="Resource not found") raise HTTPException(status_code=403, detail="Permission denied") ``` Do not raise `HTTPException` in chain or module code. Chains and modules return `None` or domain-level error objects on failure; the endpoint translates that into an HTTP response. --- ## Error Handling by Layer | Layer | On external API failure | |---|---| | Module | Log the error, return `None` or `(False, "error message")` tuple | | Chain | Log the error, return `None` or an appropriate domain object with failure indication | | Endpoint | Translate `None` or failure result into a `Response(success=False, ...)` or `HTTPException` | ```python # Module layer def test(self) -> Optional[Tuple[bool, str]]: """测试模块连通性""" try: ok = self.client.ping() return (True, "连接成功") if ok else (False, "连接失败") except Exception as err: logger.error(f"测试连通性失败:{str(err)}") return (False, str(err)) ``` --- ## MCP Protocol MoviePilot exposes an MCP (Model Context Protocol) interface for AI agent integration. - **Transport:** HTTP, JSON-RPC 2.0 - **Base path:** `/api/v1/mcp` - **Protocol versions supported:** `2025-11-25`, `2025-06-18`, `2024-11-05` ### Authentication ``` Header: X-API-KEY: Query: ?apikey= ``` ### Supported Methods | Method | Description | |---|---| | `initialize` | Initialize session, negotiate protocol version and capabilities | | `notifications/initialized` | Client confirmation of initialization | | `tools/list` | List all available tools | | `tools/call` | Invoke a specific tool | | `ping` | Connection liveness check | ### Error Codes | Code | Message | Meaning | |---|---|---| | -32700 | Parse error | Malformed JSON | | -32600 | Invalid Request | Invalid JSON-RPC request structure | | -32601 | Method not found | Unknown method | | -32602 | Invalid params | Parameter validation failure | | -32002 | Session not found | Session does not exist or has expired | | -32003 | Not initialized | Session has not completed initialization | | -32603 | Internal error | Server-side error | ### Tool Response Format MCP tools return structured content. Errors must use the JSON-RPC error object format, not HTTP status codes. --- ## Notification and Messaging Internal notifications use the `Notification` schema and the event system: ```python from app.schemas import Notification from app.schemas.types import NotificationType, MessageChannel from app.runtime.events import eventmanager from app.schemas.types import EventType eventmanager.send_event( EventType.NoticeMessage, { "channel": MessageChannel.Telegram, "type": NotificationType.Download, "title": "下载成功", "text": f"{media_name} 已添加到下载队列", "image": poster_url, } ) ``` Do not call message channel modules directly from chain code. Use the event bus to decouple senders from channels. --- ## Media Metadata API Conventions When calling TMDB, TheTVDB, Douban, or Bangumi via the module layer: - Always check the module return for `None` before using the result — modules return `None` when the backend is not configured or the request fails. - Cache responses using `FileCache` / `AsyncFileCache` where the result is stable and repeated requests would be expensive. - Return domain objects (`MediaInfo`, `TmdbEpisode`, `MediaPerson`, etc.) from modules, never raw API response dicts. --- ## Webhook Handling Webhook payloads arrive at `app/api/endpoints/webhook.py` and are dispatched via `eventmanager.send_event(EventType.WebhookMessage, ...)`. Processing logic lives in the chain layer (`app/chain/webhook.py`). Do not add webhook-specific business logic directly in the endpoint. The endpoint parses the payload and fires the event; the chain handles the response. *Last Updated: 2026-08-14*