Architecture¶
System diagram¶
flowchart TD
Client["MCP Client\n(Claude Desktop, Cursor, Zed)"]
CR["Cloud Run\nthe-curator"]
Google["Google OAuth\naccounts.google.com"]
Gemini["Vertex AI Gemini\nTranscript generation"]
TTS["Vertex AI TTS\ngemini-3.1-flash-tts-preview"]
GCS_EP["GCS Bucket\nthe-curator-podcast-data\n(episodes)"]
GCS_AUTH["GCS Bucket\nthe-curator-oauth-state\n(tokens)"]
SM["Secret Manager\nOAuth credentials"]
Client -- "SSE /sse" --> CR
Client -- "OAuth flow" --> Google
Google -- "callback /oauth2/callback" --> CR
CR -- "generate transcript" --> Gemini
CR -- "synthesize audio" --> TTS
CR -- "upload episode" --> GCS_EP
CR -- "persist OAuth state" --> GCS_AUTH
CR -- "read secrets at startup" --> SM
Component breakdown¶
FastMCP server (main.py)¶
The entry point. Wraps two MCP tools and wires together:
- The
GoogleOAuthProvideras the MCP auth server - A
PodcastGenerationinstance for tool execution - Custom Starlette routes for
/healthand/oauth2/callback
Runs as a Gunicorn + Uvicorn process inside Docker on port 8080.
Google OAuth provider (auth/provider.py)¶
Implements OAuthAuthorizationServerProvider from the MCP SDK. Delegates identity to Google but owns the full OAuth 2.1 state machine: client registration, authorization codes, access tokens, and refresh tokens. All state is serialized to JSON and written to GCS after every mutation.
Podcast generation (podcast_generation.py)¶
Orchestrates the two-step generation pipeline:
- Calls
VertexClient.generate_content()with the transcript prompt → parses the response into(speaker, text)turns. - Calls
VertexClient.synthesize_conversation()→ collects PCM frames → writes a WAV file.
Vertex client (utils/vertex_client.py)¶
Thin wrapper around three Google SDK clients:
genai.Client(regional,us-central1) — Vertex AI TTSgenai.Client(global) — Gemini transcript generationanthropic.AnthropicVertex— optional Claude routing (for models prefixed withclaude)
Data flows¶
First-time authentication¶
sequenceDiagram
participant C as MCP Client
participant S as The Curator (Cloud Run)
participant G as Google OAuth
C->>S: POST /register (Dynamic Client Registration)
S-->>C: client_id, client_secret
C->>S: GET /authorize
S-->>C: 302 → Google OAuth
C->>G: GET /o/oauth2/v2/auth
G-->>C: user consent page
C->>G: approve
G-->>S: GET /oauth2/callback?code=...
S->>G: POST /token (exchange code)
G-->>S: access_token
S->>G: GET /userinfo
G-->>S: { email }
S->>S: verify email == ALLOWED_EMAIL
S-->>C: 302 → redirect_uri?code=mcp_code
C->>S: POST /token (exchange mcp_code)
S-->>C: access_token, refresh_token
Podcast generation¶
sequenceDiagram
participant C as MCP Client
participant S as The Curator
participant Gemini as Vertex AI Gemini
participant TTS as Vertex AI TTS
participant GCS as Cloud Storage
C->>S: create_podcast_transcript("history of radar")
S->>Gemini: generate_content(transcript_prompt)
Gemini-->>S: raw text (Annabelle: ... Link: ...)
S-->>C: [(Annabelle, ...), (Link, ...), ...]
C->>S: create_podcast_episode(title, transcript)
loop for each turn
S->>TTS: generate_content(turn_text, voice=Kore/Puck)
TTS-->>S: PCM bytes
end
S->>S: write WAV file
S->>GCS: upload episodes/<ts>/<title>.wav
S-->>C: { episode_id, status: "created" }
Infrastructure¶
All GCP resources are managed by Terraform in terraform/:
| Resource | Purpose |
|---|---|
google_cloud_run_v2_service.podcast_service |
Hosts the FastMCP server |
google_storage_bucket.podcast_data |
Episode audio storage (public read) |
google_storage_bucket.oauth_state |
OAuth state persistence (private) |
google_secret_manager_secret.* |
OAuth credentials injected at runtime |
google_service_account.podcast_service |
Cloud Run identity with scoped IAM |