Add comprehensive rule documentation for AI-assisted development covering authentication surfaces, outbound-call safety invariants, and other project conventions to guide Claude's understanding of critical system behaviors.
3.2 KiB
description, paths
| description | paths | |
|---|---|---|
| MCP tools return formatted strings; ToolError-vs-return-string convention; lazy gateway resolution; resources are JSON |
|
MCP tools & resources
The MCP server (mcp_server/server.py) is the
AI-assistant control surface. It's built by create_mcp_server(get_gateway) and
mounted at /mcp/ before the lifespan runs, so everything is resolved lazily.
-
Auth is the ASGI
_owner_only_mcpwrapper in main.py, not FastMCP.create_mcp_serverbuildsFastMCP(auth=None); the wrapper resolves theAuthorizationbearer (Casdoor JWT or owner-minted PAT) via the sharedresolve_from_header_or_query+is_ownerand returns 401/403 before the inner app runs. Don't reintroduce a FastMCP verifier here — one resolver gates all four surfaces (see the auth-surfaces rule). MCP clients use a PAT (hs_pat_…) minted from the dashboard's Tokens modal. -
Tools return plain formatted strings; resources return JSON strings. Tools produce human-readable text an assistant reads back to a user (
"Call abc123 initiated. …"). Resources (gateway://status,gateway://call-flows,gateway://active-calls) returnjson.dumps(...). Don't blur these — a tool that returns raw JSON, or a resource that returns prose, breaks the contract. -
Error convention — match the two existing patterns:
- Raise
ToolErrorwhen the request is invalid or unsafe: emergency number, bad mode, concurrency cap hit, or "gateway still starting" (require_gateway). The assistant should treat these as errors. - Return an error string for a lookup that simply found nothing or hit a
recoverable snag:
"Call {id} not found.","No stored call flow for …","Error looking up …: {e}". The assistant reads these as content. - Rule of thumb: "you asked for something invalid/unsafe" → raise; "I looked, here's the (maybe empty/failed) answer" → return.
- Raise
-
require_gateway()gates every tool that needs the live gateway. It raisesToolError("Gateway is still starting up …")whenget_gateway()returnsNone. This is why the MCP app can mount before the lifespan builds the gateway. Call it at the top of any tool that touches the gateway; never assume the gateway exists. -
make_callis the one tool that dials. It maps the stringmodetoCallMode, defaults unknown modes toDIRECT, and letsgateway.make_call's refusals (ValueError) surface asToolError. The emergency guard and concurrency cap live in the gateway, not here — don't reimplement or skip them at the tool layer (see the call-safety rule). -
DB-backed tools use
session_scope()and read fromcall_persistence. Completed-call history, summaries, recordings, and stored flows come from the database, not fromactive_calls(those are live only). Keep theasync with session_scope() as session:pattern; don't open ad-hoc sessions. -
Keep the tool count and README table in sync. There are 15 tools + 3 resources. If you add/remove one, update the README's MCP table and the
docs/mcp-server.mdreference — a drifting tool list is a documented lie.