Add hierarchical key-scoped catalog API for machine consumers

GET /catalog walks agents through progressive disclosure: available
APIs with descriptions, then one API's tags, then a tag's endpoints,
then full detail for a single operation with $refs resolved inline.
Oversized tags (tag-poor upstreams) fall back to path-prefix groups,
drillable with ?prefix= and compressed through single-child chains.

Responses are filtered to the key's grants, carry ETags for cheap
revalidation, and reuse the discovery spec cache. Key auth is shared
with the docs portal via portal.resolve_api_key; 'catalog' and 'mcp'
are now reserved slugs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Amar
2026-09-02 11:39:07 +02:00
co-authored by Claude Fable 5
parent b90ecaf61c
commit 9a3feccc90
5 changed files with 352 additions and 4 deletions
+11 -2
View File
@@ -183,8 +183,9 @@ def build_key_spec(db: Session, api_key: ApiKey, gateway_url: str) -> dict:
}
@router.get("/openapi.json", include_in_schema=False)
def key_scoped_openapi(request: Request, db: Session = Depends(get_db)):
def resolve_api_key(request: Request, db: Session) -> ApiKey | JSONResponse:
"""Authenticate a consumer-facing request by X-API-Key. Returns the key,
or the error response to send back."""
plain_key = request.headers.get(config.API_KEY_HEADER)
if not plain_key:
return _error(401, "missing_api_key",
@@ -194,6 +195,14 @@ def key_scoped_openapi(request: Request, db: Session = Depends(get_db)):
.one_or_none())
if api_key is None or not api_key.is_active or not api_key.user.is_active:
return _error(403, "invalid_api_key", "API key is unknown or has been revoked.")
return api_key
@router.get("/openapi.json", include_in_schema=False)
def key_scoped_openapi(request: Request, db: Session = Depends(get_db)):
api_key = resolve_api_key(request, db)
if isinstance(api_key, JSONResponse):
return api_key
spec = build_key_spec(db, api_key, str(request.base_url).rstrip("/"))
return JSONResponse(spec, headers={"Cache-Control": "no-store"})