--- description: FastMCP tool rules — sync def, string-table returns, premium gating, _log, dual dict shapes paths: - "nike/server.py" --- # MCP tools - **Tools return plain formatted `str`, not JSON and not Pydantic models.** They build human-readable ASCII blocks/tables for the LLM (`"=== {name} ==="`, aligned standings columns, grouped rosters). This is Nike's convention — don't convert a tool to return JSON or a model to match other estate servers. Match the existing formatting. - **Tools are synchronous `def`.** `sportsdb` (`requests`) and `db` (`psycopg2`) are blocking; FastMCP runs sync tools in a threadpool. Don't make them `async`. Keep them plain module-level callables — `/api/run` invokes `tool.fn(**args)` directly. - **Standard tool shape:** `@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))` (every tool here is read-only), a docstring written for Claude, resolve the team via `_resolve_team` if needed, call the `sportsdb` client, format text, and `_log(tool, args, duration_ms)` before returning. Not-found returns a friendly string (`"Team '…' not found."`), never an exception. - **Premium gating is two things kept in sync:** the runtime check `config.SPORTSDB_KEY in ('3', '')` (→ return a clear "requires a premium key" message, or degrade to cached data like `get_roster` does) **and** the `tags={"premium"}` on the decorator (so `/api/tools` and the dashboard can flag it). Add both when a new tool needs premium data. - **Rows come in two shapes — handle both.** A team/player dict is either a raw TheSportsDB dict (`strTeam`, `idTeam`, `strLeague`) or a cached DB row (`name`, `id`, `league_name`). Read with `x.get("strTeam") or x.get("name")`. A cache hit and a cache miss must render identically — don't assume one shape. - **Per-external-call `try/except … pass` is intentional** so a partial API failure yields partial results. Keep each API call individually guarded; don't merge them into one try or turn them into hard failures. - **When you add/rename a tool, update the hand-written strings too:** the `mcp` `instructions=` and the `football_analyst()` prompt enumerate tools manually and do **not** auto-update (unlike `/api/tools`, which derives from `mcp.list_tools()`).