Files
hold-slayer/.claude/rules/mcp-tools.md
Robert Helewka 4a3c14d4af
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: add Claude AI assistant rules and configuration
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.
2026-07-28 19:01:38 -04:00

57 lines
3.2 KiB
Markdown

---
description: MCP tools return formatted strings; ToolError-vs-return-string convention; lazy gateway resolution; resources are JSON
paths:
- "mcp_server/server.py"
---
# MCP tools & resources
The MCP server ([mcp_server/server.py](../../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_mcp` wrapper in [main.py](../../main.py), not
FastMCP.** `create_mcp_server` builds `FastMCP(auth=None)`; the wrapper resolves
the `Authorization` bearer (Casdoor JWT or owner-minted PAT) via the shared
`resolve_from_header_or_query` + `is_owner` and 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](auth-surfaces.md)). 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`) return `json.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 `ToolError`** when 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.*
- **`require_gateway()` gates every tool that needs the live gateway.** It raises
`ToolError("Gateway is still starting up …")` when `get_gateway()` returns
`None`. 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_call` is the one tool that dials.** It maps the string `mode` to
`CallMode`, defaults unknown modes to `DIRECT`, and lets `gateway.make_call`'s
refusals (`ValueError`) surface as `ToolError`. 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 from `call_persistence`.
Completed-call history, summaries, recordings, and stored flows come from the
database, not from `active_calls` (those are live only). Keep the
`async 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.md` reference — a drifting tool list is a documented lie.