# הפודקאסט של מיכאל — API > Read-only HTTP + MCP surface for הפודקאסט של מיכאל. No write methods. Auth is optional (anonymous OAuth 2.1 + PKCE S256 available for clients that prefer a bearer token). ## Quickstart Three lines from zero to a real episode: ```bash curl https://podcast.lugassy.net/status # 1. health check curl 'https://podcast.lugassy.net/api/search?q=ai&limit=3' # 2. find an episode curl https://podcast.lugassy.net/1.md # 3. read the transcript (replace 1) ``` ## Authentication **Auth is optional.** Every endpoint is public, read-only, and CORS-open. Two modes: - **Zero-auth (default).** Just call the endpoints — no header, no signup. - **Public OAuth 2.1 + PKCE S256.** For clients that prefer M2M / client_credentials flows or want a bearer token to log per-request quotas, anonymous tokens are issued without consent at `https://podcast.lugassy.net/oauth/token`. Discovery: `https://podcast.lugassy.net/.well-known/oauth-authorization-server` (RFC 8414) and `https://podcast.lugassy.net/.well-known/oauth-protected-resource` (RFC 9728). ### M2M / agent auth walkthrough (optional) ```bash # 1. Generate a PKCE verifier + S256 challenge (any client lib does this) # 2. Anonymous client_credentials grant — no client secret, public client curl -X POST https://podcast.lugassy.net/oauth/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials&client_id=public&scope=read:episodes search:episodes' # Response: { access_token, token_type: "Bearer", expires_in, scope } # 3. Use the token (or skip — anonymous calls also work) curl -H 'Authorization: Bearer ' 'https://podcast.lugassy.net/api/search?q=ai' ``` Available scopes: - `read:episodes` — read episode metadata, audio URLs, transcript URLs. - `read:transcripts` — read full transcript text. - `search:episodes` — call /api/search and /ask. All scopes are granted automatically on anonymous client_credentials. ## SDK install There's no proprietary SDK. Use any HTTP client; for MCP, any MCP-compliant client works: ```bash # JavaScript / TypeScript npm install undici # or use built-in fetch # Python pip install httpx # or requests # MCP (Claude.ai, ChatGPT, Cursor) # Add this URL as a custom MCP connector — no install: # https://podcast.lugassy.net/mcp (transport: streamable-http, auth: none) ``` ## Rate limits - **60 requests/minute per IP** across all API endpoints (`/api/*`, `/mcp`, `/.well-known/mcp`, `/ask`, `/status`). - Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds). - 429 responses carry `Retry-After` (seconds). Self-throttle on those headers. ## Errors Every error is a structured JSON envelope: ```json { "error": { "code": "episode_not_found", "message": "…", "hint": "…", "docs_url": "…" } } ``` Status codes: 400 (bad query/body), 402 (donation requested at /donate), 404 (no such episode), 405 (wrong method), 429 (rate-limited), 500 (server side). ## Endpoints ### Search `GET https://podcast.lugassy.net/api/search?q=&limit=` Ranked full-text search over episode title + description + transcript. Response: `{ query, count, took_ms, results: [{ id, title, date, url, audio, transcript, score, snippet }] }`. ### Ask (NLWeb) `POST https://podcast.lugassy.net/ask` — body: `{ "query": "...", "limit": 10 }` `GET https://podcast.lugassy.net/ask?q=&limit=` — query-string variant Natural-language ask. Returns episodes ranked by transcript relevance, wrapped in NLWeb `_meta` envelope. Set `Accept: text/event-stream` (or `Prefer: streaming=true`) for SSE: events `start`, `result` (one per match), `complete`. ### Async (202 Accepted + polling) Long-running operations can opt into the async pattern. Three interchangeable entry points: **`POST https://podcast.lugassy.net/jobs`** with body `{ kind: "ask"|"search", query, limit }` (conventional path), **`POST https://podcast.lugassy.net/ask?async=1`** (or `?async=1` on `/api/search`), or set **`Prefer: respond-async`** on any of the above. All return **HTTP 202 Accepted** with `Location: /jobs/`, `Retry-After: 1`, and a JSON body `{ job_id, status: "pending", poll_url, retry_after_seconds }`. Poll `GET https://podcast.lugassy.net/jobs/` until `status` flips from `"pending"` to `"completed"`; the final response carries the result under `.result`. ```bash # 1. Kick off the job — 202 Accepted curl -i -X POST 'https://podcast.lugassy.net/ask?async=1' \ -H 'Content-Type: application/json' \ -d '{"query":"ai agents"}' # HTTP/1.1 202 Accepted # Location: https://podcast.lugassy.net/jobs/ # Retry-After: 1 # { "job_id": "", "status": "pending", "poll_url": "https://podcast.lugassy.net/jobs/", "retry_after_seconds": 1 } # 2. Poll the job — 200 OK curl https://podcast.lugassy.net/jobs/ # { "job_id": "", "status": "completed", "result": { "query": "...", "results": [...] } } ``` Job ids are stateless: the id encodes the spec, so any client that holds an id can resume polling later from a different IP / device. ### Status `GET https://podcast.lugassy.net/status` — health snapshot for circuit-breaker logic. Always 200 when reachable. Response includes show name, episode count, latest episode summary. ### MCP server (Streamable HTTP, JSON-RPC 2.0) `POST https://podcast.lugassy.net/mcp` — tool calls `GET https://podcast.lugassy.net/mcp` — manifest summary Methods: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`. Tools: `search_episodes`, `get_episode`, `get_latest_episode`. Batch / bulk: the request body may be a JSON-RPC 2.0 batch — an array of up to 50 request objects run in one round-trip, answered by an array of responses in order. MCP discovery URLs (all return the same manifest): - https://podcast.lugassy.net/.well-known/mcp - https://podcast.lugassy.net/.well-known/mcp.json - https://podcast.lugassy.net/.well-known/mcp-configuration - https://podcast.lugassy.net/.well-known/mcp/server.json ### OpenAPI `GET https://podcast.lugassy.net/.well-known/openapi.json` — OpenAPI 3.1 spec for the entire read surface. ## Agent mode Append `?mode=agent` to `/` or to any `/` to get a compact JSON envelope with endpoint inventory and either the latest episode (homepage) or the specific episode (episode page). ## Markdown view - `https://podcast.lugassy.net/index.md` — homepage as markdown - `https://podcast.lugassy.net/.md` — episode page as markdown - Or send `Accept: text/markdown` on any HTML page.