podcast-guest-crm
About
Manage podcast guest pipeline, outreach drafts, and analytics via 5 MCP tools.
Details
- Author
- rudrendupaul
- Categories
- Productivity
Jump to
Setup
Install podcast-guest-crm in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/rudrendupaul/podcast-guest-crm
Follow the installation instructions in the repository README, then restart your MCP client.
The operating system for podcast booking.
AI-native. Keyboard-first. Built for agencies.4.2 million podcasts. $4B+ creator economy. Zero purpose-built workflow software.
We built the tool that should have existed for the last decade.
Built byRudrendu Paul&Sourav Nandy· Engineered withClaude Code
Quick Start·The Problem·The Product·AI Layer·Architecture·Tech Stack
git clone https://github.com/RudrenduPaul/podcast-guest-crm cd podcast-guest-crm pnpm install pnpm dev
The app runs on seed data from first boot; 34 realistic guests across all six pipeline stages. No environment variables required.
web: http://localhost:3000 api: http://localhost:3001 docs: http://localhost:3001/docs ← Swagger UI, auto-generated from route schemas
[!NOTE] Zero-config only applies to this local dev mode running on seed data. A production deployment needs real Supabase and Anthropic credentials set in the environment: the Zod env schema inpackages/configcrashes the server at boot if a required secret is missing, by design.
Every tool a podcast host reaches for was built for a different job.HubSpot is a sales CRM. PodMatch is a discovery marketplace. Notion is a blank canvas that requires engineering to become anything useful. None of them model the guest lifecycle (the arc from discovery through outreach, scheduling, recording, publishing, and follow-up) as a first-class object.
This tool does. Guest fit scoring, personalized outreach drafting, interview prep, and follow-up sequences run onclaude-sonnet-4-6, which is what makes automating those specific steps viable now in a way that wasn't a couple of years ago.
A full-stack AI-native CRM with six pages, eleven features, and zero compromises on craft.
Discover → Outreach → Scheduled → Recorded → Published → Follow-up
Every guest moves through this lifecycle. Every transition is validated, logged, and acted on. The system knows where every guest is, when they last heard from you, and what needs to happen next. Without you having to remember.
The app is designed for keyboard-first workflows. Power users never touch the mouse for core tasks.
All AI lives inpackages/ai. The only place in the codebase that imports@anthropic-ai/sdk. Every feature calls a typed function. It never touches the SDK directly.
Two modes:completeJSON<T>()for structured output with generic type inference,stream()for the real-time typewriter effect. The outreach composer uses both simultaneously. Streaming for the live preview, JSON for the copy-ready result with confidence score.
// packages/ai/src/client.ts. The single seam for all AI calls export class ClaudeClient { async completeJSON<T>(system: string, user: string): Promise<T> async stream(system: string, user: string): AsyncIterable<string> } // Feature code never touches the SDK. It calls typed prompt functions: const brief = await generateInterviewBrief(guest); // → InterviewBrief const email = await draftOutreachEmail(guest, show); // → OutreachEmail const score = await scoreGuestFit(guest, workspace); // → FitScore
We're not prompting generically. Here's the actual constraint set from the outreach module. Specificity is the moat:
export const OUTREACH_EMAIL_SYSTEM_PROMPT = You are an expert podcast booking agent working on behalf of a host with a specific audience and brand. Your emails must: 1. Be authentic, specific, and not generic. Reference the guest's actual recent work 2. Clearly state the show's value proposition and the size and shape of the audience 3. Make the ask simple and low-friction. One clear question, not a pitch deck 4. Be concise: 150–250 words for the body 5. Have a subject line under 70 characters that doesn't feel like a cold email 6. NEVER use: "passionate", "synergy", "journey", "touch base", "hop on a call" 7. End with a single clear call-to-action. Not multiple options;
The fit scoring prompt evaluates guests against the show's actual topic taxonomy. Not generic relevance signals. The interview brief generates question types calibrated to the podcast format (depth, contrarian, forward-looking). This is prompt engineering as product design, not prompt engineering as a party trick.
Browser (Next.js 14 App Router) ├── TanStack Query v5 : server state, optimistic updates, stale-while-revalidate │ every query falls back to seed data on API error ├── Zustand : UI state (sidebar, modals, ⌘K palette, filters) │ persisted to localStorage via middleware ├── lib/api.ts : typed fetch wrapper; catches 503, returns seed data └── components/ : shadcn/ui primitives + Framer Motion feature components │ HTTP/REST + JWT (Bearer token) ▼ Fastify v5 API (Node.js 20, TypeScript strict mode) ├── Plugins: CORS (allowlist), @fastify/rate-limit (100/min), @fastify/jwt, swagger-ui ├── Routes: /guests, /outreach, /ai, /analytics ← all require authentication ├── Middleware: Zod schemas on every route. Body, query params, path params └── Services: guestService (in-memory store seeded from packages/db on startup) │ │ ▼ ▼ packages/db packages/ai Drizzle ORM schema + ClaudeClient + 34 seed guests 6 typed prompt modules SQLite (dev) │ Turso (prod) ▼ Anthropic API claude-sonnet-4-6
Five Architectural Decisions Worth Reading
1. Shared types inpackages/types, zero inline definitions inapps/.Every interface that crosses the API boundary (Guest,OutreachEmail,Workspace,AnalyticsOverview) lives in one package, imported by both the API and the web app. A TypeScript error on the frontend is a broken API contract caught before it ships.
2. Single AI seam inpackages/ai.ClaudeClientis the only place@anthropic-ai/sdkis imported. It handles exponential backoff on 429s and 5xx, token tracking per call, markdown stripping from JSON responses, and streaming viaAsyncIterable. Feature code calls typed functions and never knows the SDK exists. Swapping models or providers is a one-file change.
3. Graceful degradation as a design requirement, not an afterthought.Every TanStack Query hook catches API errors and returns seed data. Every mutation has a synthetic fallback. The app is fully interactive without a running backend. This is deliberate: demos should never fail because a server is down.
4. Optimistic updates with enforced rollback.Stage transitions on the kanban board are instant in the UI. The server confirms asynchronously. If the server rejects a transition (the lifecycle rules are strict, you cannot move fromdiscovertopublisheddirectly), the previous state is restored and an error toast fires. Users never wait for drag-drop feedback; errors surface clearly without corrupting state.
5. Zod at every boundary.The env schema crashes the server at boot if a required secret is missing. Silent misconfiguration is worse than a loud failure. Every API route has a Zod schema for body, query, and params; the CI pipeline rejects routes without schemas. Shared schemas live inpackages/configso frontend and backend enforce the identical contract.
This codebase was built using four specialized Claude Code sub-agents running in parallel, each scoped to a domain slice. This isn't a workflow preference. It's architectural isolation enforced at the tooling layer.
The UI agent cannot write a Drizzle query. The DB agent cannot create a React component.Constraint becomes architecture.You stop second-guessing whether a UI change silently mutated a schema.
Custom slash commands in.claude/commands/:
- /new-feature <name>: scaffolds a full feature, API route + Zod schema + service + page + components + TanStack hook + tests
- /review-pr: runs a security, type safety, and MLP checklist before merge
The "ship fast" advantage is gone. A capable developer scaffolds a CRM in a weekend; our solution compresses that to hours. The moat is now craft. The quality of what you build in that time.
We hold a Minimum Lovable Product bar on every PR. Elena Verna's framing: the threshold where a product earns genuine affection from its users, not just adequate utility.
- Confetti on booking.When a guest moves to Scheduled, confetti fires. A confirmed booking is a real win. The app should treat it that way.
- Typewriter effect on AI output.The generated email types out character by character. Streaming makes it feel like workingwitha collaborator, not waitingfora tool.
- Fit score counts up.The ring animates from 0 to the actual score over 600ms. People watch it. That wait makes the score feel earned.
- Command palette.⌘K puts every guest, page, and action one keypress away. Power users never reach for the mouse.
- Today's Focus.The dashboard tells you exactly who needs attention today (stale outreach, upcoming recordings) without requiring you to remember what to check.
- Named nudges."Sara hasn't replied in 8 days" beats "3 follow-ups pending." Named, specific, actionable.
- Personality copy in empty states."Your discovery list is empty. Your next great episode is one outreach away" tells you what to do next. "No data found" doesn't.
- Empty states have personality copy, not "No data found"
- Loading states useSkeletoncomponents, not blank screens
- Errors have actionable messages, not "Something went wrong"
- Key interactions have Framer Motion animations
- What's the wow moment?If there isn't one, find it before merging.
Two-tier SaaS. Simple pricing that grows with the customer.
Usage-based AI credits above the base tier: the first 200 AI calls/month (outreach email, fit score, brief, social post) are included, above that teams pay for what they use.
The gap isn't features. It's the mental model.
PodMatch solvesdiscovery: finding guests. We solveworkflow: the months-long process of pitching, following up, scheduling, prepping, recording, publishing, and staying in relationship. These are not the same problem. The companies that built discovery tools left the workflow problem untouched. That's the gap.
Every integration point sits behind an interface. MCP servers slot in without refactoring.
With Gmail MCP active, outreach goes from drafted to sent in one click. With Calendar MCP, a guest moving to Scheduled creates the recording event automatically. With Exa, fit scoring pulls the guest's latest work from the web. Not just what's in their bio.
Every choice is defended. No resume-driven development.
podcast-guest-crm/ ├── apps/ │ ├── web/ # Next.js 14 App Router │ │ ├── app/ │ │ │ ├── (auth)/login/ # Demo login (route group) │ │ │ └── dashboard/ # Protected routes. No route group by design │ │ │ ├── page.tsx # Overview · Today's Focus · Recent Activity │ │ │ ├── layout.tsx # Sidebar + Navbar + GlobalModals │ │ │ ├── guests/ # Table · filters · Add Guest modal │ │ │ ├── pipeline/ # Kanban board │ │ │ │ └── [id]/ # Guest detail · AI action sidebar │ │ │ ├── outreach/ # AI email composer (streaming) │ │ │ ├── analytics/ # Charts · conversion metrics │ │ │ └── settings/ # Workspace · AI model config │ │ ├── components/ │ │ │ ├── ui/ # shadcn/ui primitives │ │ │ ├── shared/ # Sidebar · Navbar · CommandPalette │ │ │ │ # NotificationDropdown · GlobalModals · EmptyState │ │ │ ├── guests/ # GuestCard · GuestTable · AddGuestModal │ │ │ │ # InterviewBriefPanel · SocialPostsPanel │ │ │ ├── pipeline/ # KanbanBoard · KanbanColumn │ │ │ └── outreach/ # AIAssistPanel (streaming typewriter) │ │ ├── hooks/ # TanStack Query hooks. Graceful fallback on every query │ │ ├── lib/ # api.ts · mock-data.ts · utils.ts │ │ └── stores/ # Zustand. Sidebar · modals · palette · filters │ │ │ └── api/ # Fastify v5 backend │ └── src/ │ ├── plugins/ # cors · rate-limit · jwt · swagger-ui │ ├── routes/ # /guests · /outreach · /ai · /analytics │ ├── services/ # guestService. In-memory store, seeded on startup │ └── tests/ # Vitest. Coverage gate >70% │ ├── packages/ │ ├── types/ # Shared TypeScript interfaces. Single source of truth │ ├── config/ # Zod env validation · shared constants │ ├── db/ # Drizzle schema · 34 seed guests · migrations │ ├── ai/ # ClaudeClient · 6 typed prompt modules │ ├── cli/ # podcast-guest-crm-cli: TypeScript CLI, wraps apps/api │ └── cli-pypi-wrapper/ # Thin pip/pipx wrapper, shells out to the npm CLI │ ├── .claude/ │ ├── commands/ # /new-feature · /review-pr │ └── agents/ # ui · db · ai-features · test. Scoped sub-agents │ ├── .github/ │ ├── workflows/ # ci.yml · security.yml (CodeQL) │ └── PULL_REQUEST_TEMPLATE.md # Includes MLP checklist │ └── docs/ ├── architecture/ # system-design.md · security.md · ai-layer.md └── decisions/ # ADR 001 (monorepo) · 002 (Drizzle) · 003 (Fastify)
OpenAPI documentation auto-generated athttp://localhost:3001/docs.
GET /health Health check + readiness probe GET /api/v1/guests List, paginated, filterable by stage/topic/priority POST /api/v1/guests Create guest, triggers async fit scoring GET /api/v1/guests/:id Guest detail PUT /api/v1/guests/:id Update fields PATCH /api/v1/guests/:id/stage Lifecycle transition, service validates allowed paths DELETE /api/v1/guests/:id Soft delete POST /api/v1/outreach/draft AI draft, JSON or streaming mode POST /api/v1/outreach/send Send via Resend (mocked in dev) GET /api/v1/outreach/:guestId Outreach history POST /api/v1/ai/fit-score Score 0–100 + rationale + red flags POST /api/v1/ai/interview-brief Pre-recording brief with question structure POST /api/v1/ai/social-post LinkedIn + Twitter thread + Instagram caption GET /api/v1/analytics/overview Dashboard metrics + recent activity feed GET /api/v1/analytics/pipeline Stage funnel + outreach activity timeline
[!WARNING]PATCH /guests/:id/stage,POST /guests, andGET /guests/:idcurrently declare their response shape as a bare{ type: 'object' }with no listed properties inapps/api/src/routes/guests.ts. Fastify's JSON serializer strips the body down to{}on success as a result, even though the operation succeeded.guest listis unaffected. See the FAQ for how the CLI works around this.
Lifecycle transitions enforced at the service layer:
discover → outreach → scheduled → recorded → published → follow_up ↑___________↑ ↑__________↑ (reschedule) (re-record needed)
podcast-guest-crm-cliis a real TypeScript CLI (packages/cli) that wraps the same API above. Every command maps to a real route, no invented endpoints.
npm install -g podcast-guest-crm-cli # or, for Python-first / pip environments (thin wrapper, shells out to the npm package via npx): pip install podcast-guest-crm-cli
podcast-guest-crm-cli login podcast-guest-crm-cli guest list --stage published --limit 5 podcast-guest-crm-cli guest show <id> podcast-guest-crm-cli guest add --name "Ada Lovelace" --email ada@example.com --title "Engineer" --company "Analytical Engines" podcast-guest-crm-cli guest stage <id> outreach --reason "replied positively" podcast-guest-crm-cli outreach draft <guest-id> --episode-angle "AI safety" podcast-guest-crm-cli analytics summary podcast-guest-crm-cli analytics pipeline
Add--jsonto any data-returning command for machine-readable output, meant for scripts and agents:
podcast-guest-crm-cli guest list --stage discover --json
loginauthenticates directly against Supabase's own REST auth endpoint (POST <SUPABASE_URL>/auth/v1/token?grant_type=password), the same identity provider the web app uses. It never uses the dev-onlyBearer dev-mock-tokenshortcut inapps/api/src/plugins/auth.ts, that bypass exists purely for local API testing. The resulting session is cached to~/.config/podcast-guest-crm-cli/credentials.json(permissions0600) and refreshed silently with the stored refresh token when it expires.
podcast-guest-crm-cliships a Model Context Protocol server (not to be confused with the third-party MCP servers this app can integratewith, listed above).podcast-guest-crm-cli mcpstarts it over stdio, exposing five tools that call straight into the same API seam every CLI command uses:list_guests,add_guest,update_guest_stage,draft_outreach_email, andget_analytics_summary.
npm install -g podcast-guest-crm-cli podcast-guest-crm-cli login
{ "mcpServers": { "podcast-guest-crm": { "command": "npx", "args": ["podcast-guest-crm-cli", "mcp"] } } }
A realtools/callfor the core lifecycle tool,{"name": "update_guest_stage", "arguments": {"id": "guest_1", "stage": "outreach"}}, returns the same envelopeguest stage <id> outreach --jsonprints on the CLI. Seepackages/cli's READMEfor the full tool reference.
Production-grade controls from day one. We don't retrofit security.
- MCP: Gmail + Google Calendar:outreach goes from drafted to sent in one click; booking confirmations create calendar events automatically
- MCP: Exa Search:guest fit scoring pulls live web data, not just bio text
- Stripe billing:Solo $29/mo, Agency $99/mo, usage-based AI credits above tier
- Transcript ingestion:upload episode, auto-generate social posts and follow-up email referencing specific highlights
- Client portal:token-based read-only view for agency clients, eliminating the weekly status report email
- Zapier / Make connector:two-way sync with Cal.com, Notion, HubSpot
- RSS extraction:input a podcast RSS URL, auto-populate host contact info and show stats
- Mobile (React Native):same API, native feel, for pipeline review on the go
- Multi-show dashboard:agency view across all managed shows in one screen
- Predictive follow-up:ML model trained on reply rate data to optimize outreach timing
Rudrendu PaulandSourav Nandyhave built this production-ready AI-native software.
- Full-stack TypeScript monorepos(Turborepo + pnpm) with shared type packages, enforced at the CI layer
- Fastify APIswith Zod-validated schemas, JWT authentication, and Row Level Security. No exceptions
- AI-powered feature layerswith typed prompt modules, streaming, JSON extraction, and exponential backoff
- Next.js 14 App Router frontendswith TanStack Query, Zustand, Framer Motion, and shadcn/ui
- Claude Code sub-agent architecturesthat enforce domain boundaries at the tooling layer
What ispodcast-guest-crm-cliand how is it different from using the web app?
It's a real TypeScript command-line client (packages/cli) for the same API the Next.js web app calls. It wraps the guest lifecycle endpoints (guest list/add/show/stage), the AI outreach drafting endpoint (outreach draft), and the analytics endpoints (analytics summary/pipeline). The differentiator is agent-native output: every data-returning command supports--json, so a script or an AI agent can drive the same pipeline a human would drive from the dashboard, without scraping HTML or maintaining its own HTTP client.
What platforms and runtimes does it support?
The npm package (podcast-guest-crm-clion npm, requires Node.js 20 or newer) runs on macOS, Linux, and Windows anywhere Node runs. A separate PyPI package of the same name (packages/cli-pypi-wrapper) is a thin wrapper for pip/pipx users: it doesn't reimplement the CLI in Python, it checks thatnodeandnpxare onPATHand shells out to the npm package, pinned to the wrapper's own version -- falling back to npm'slatestrelease if that exact version was never published to npm, rather than failing outright.
podcast-guest-crm-cli loginprompts for your email and password, then authenticates directly against your Supabase project's own REST endpoint (POST <SUPABASE_URL>/auth/v1/token?grant_type=password), the same identity provider the web app uses. You'll need your deployment's Supabase project URL and anon key (--supabase-url/--supabase-anon-key, orPODCAST_GUEST_CRM_SUPABASE_URL/PODCAST_GUEST_CRM_SUPABASE_ANON_KEY), matching the values your deployment already sets asNEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY. The resulting access and refresh tokens are cached to~/.config/podcast-guest-crm-cli/credentials.jsonwith0600permissions, and the access token refreshes silently once it expires.
Why did a command print{"data": {}}instead of the fields I expected?
That's a real, current gap in a few of the API's own Fastify response schemas (apps/api/src/routes/guests.ts), not a CLI bug: routes likePATCH /guests/:id/stage,POST /guests, andGET /guests/:iddeclare their response shape as a bare{ type: 'object' }with no listed properties, so Fastify's JSON serializer strips the body down to an empty object even on success. The CLI detects this and falls back to printing the raw (empty) response instead of crashing on a missing field.guest listisn't affected, since its schema declares an array with no fixed item shape.
Can I use this CLI in an automated pipeline or hand it to an AI agent?
Yes, that's the primary design goal. Every data-returning command accepts--jsonfor structured output, exit codes are nonzero on failure, and error responses are JSON objects witherror,message, andstatusCodefields when--jsonis set. There's no interactive-only path required for any command exceptlogin's password prompt, which also accepts--emailand--passwordflags for non-interactive use. For MCP-native agents (Claude Desktop, Claude Code),podcast-guest-crm-cli mcpstarts a stdio MCP server exposing the same guest-lifecycle, outreach-drafting, and analytics capability as callable tools, seeMCP Serverabove.
Can I use this CLI, or the rest of this codebase, commercially?
Yes. This repository (includingpackages/cliandpackages/cli-pypi-wrapper) is MIT licensed, jointly owned by Rudrendu Paul and Sourav Nandy. SeeLICENSEfor the full terms; commercial use, modification, and redistribution are all permitted.
Does the CLI ever store or transmit my password?
No. The password you enter at theloginprompt is sent once, over HTTPS, directly to Supabase's password grant endpoint, and is never written to disk. Only the resulting access token, refresh token, and their expiry are cached locally.
What happens if my session expires while I'm running a command?
The CLI checks the cached access token's expiry (with a 30-second buffer) before every request. If it's expired, the CLI calls Supabase's refresh-token grant with the stored refresh token, saves the new session, and retries, all without prompting you to log in again. You'll only seeloginerrors again once the refresh token itself is invalidated (for example, after a password change).
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.



